feat(extensions): ship ModelDefinition escape hatch for TS7006 (swamp-club#141)#1205
feat(extensions): ship ModelDefinition escape hatch for TS7006 (swamp-club#141)#1205
Conversation
…-club#141) Extension authors whose _test.ts imports the model source hit TS7006 (implicit any on execute parameters) under strict mode, because the unannotated default form has no type anchor. Ship a type-only `ModelDefinition<TGlobalArgs>` in @systeminit/swamp-testing that authors opt into via `satisfies ModelDefinition<typeof Schema>` (or the `defineModel` function-form alternative) — the literal stays identical, only a trailing satisfies and a type-only import are added, and `context.globalArgs` narrows to the schema-inferred shape. - Parameterise testing MethodContext on TGlobalArgs with a default of Record<string, unknown> (backward compatible). - Add ModelDefinition, MethodDefinition, CheckDefinition, ResourceOutputSpec, FileOutputSpec, VersionUpgrade, CheckResult, and the defineModel helper as type-only exports. - Extend testing_package_compat_test.ts to guard drift between the testing and canonical ModelDefinition / MethodDefinition. - Add a type-level regression test at packages/testing/model_definition_types_test.ts that asserts both the satisfies and defineModel paths narrow globalArgs correctly under strict+noImplicitAny. - Document the escape hatch in a new .claude/skills/swamp-extension-model/references/typing.md with before/after, a "when not to use" section, and cross-links to the compat test and issue #141. - Update Key Rule #5 in SKILL.md to point at typing.md. - Cross-link from references/testing.md since tests are where authors hit the problem. - Bump @systeminit/swamp-testing from 0.2.0 to 0.3.0 (additive, backward compatible). The default authoring experience is unchanged — unannotated execute params remain the Quick Start. Existing extensions do not need to migrate. The `: any` workaround keeps working for anyone already using it. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
Code Review
The core contribution — resolving TS7006 for execute parameters via satisfies ModelDefinition<typeof Schema> and the defineModel helper — is well-designed, correctly implemented, and thoroughly tested. The MethodContext<TGlobalArgs> parameterisation is backward compatible, the type-level regression tests are solid, and the compat test guards against drift on the main types. The skill documentation and README updates are clear and well-scoped.
Blocking Issues
None.
Suggestions
-
ResourceOutputSpec/FileOutputSpecfield mixing (packages/testing/model_definition_types.ts): The testingResourceOutputSpecincludesstreaming?: boolean(lines 55) which only exists on the canonicalFileOutputSpec, and is missingtags?: Record<string, string>which exists on the canonicalResourceOutputSpec. Conversely, the testingFileOutputSpecincludessensitiveOutput?: booleanandvaultName?: string(lines 68-69) which only exist on the canonicalResourceOutputSpec, and is also missingtags. It looks like some fields were accidentally swapped between the two specs. This means an extension author usingsatisfies ModelDefinition<...>who declares resource tags will get a spurious TS error, whilestreamingon a resource will silently compile but do nothing. Suggest fixing the field assignments to match the canonical types insrc/domain/models/model.ts:49-94. -
CheckResultextra field (packages/testing/model_definition_types.ts:109): The testingCheckResultincludeswarnings?: string[]which doesn't exist on the canonicalCheckResult(src/domain/models/model.ts:519-522). Extension authors returning warnings would think they're handled when they're not. -
Compat test gaps for output specs (
src/domain/models/testing_package_compat_test.ts): The compat test validatesModelDefinitionandMethodDefinitionfield shapes but doesn't coverResourceOutputSpec,FileOutputSpec, orCheckResult. Adding structural checks for these types (similar to_checkMethodDefinitionFields) would catch the field-mixing issues in suggestions 1-2 and prevent future drift. -
Minor: multi-line doc block in
model_definition_types.ts— The file has several multi-paragraph JSDoc blocks (e.g., lines 30-37, 107-130, 148-168). CLAUDE.md convention is "never write multi-paragraph docstrings or multi-line comment blocks — one short line max." The module-level doc block explaining the purpose is understandable given this is a public API for extension authors, but the internal implementation comments could be trimmed.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
Testing
CheckResultaddswarningsfield not present in canonical —packages/testing/model_definition_types.ts:108defineswarnings?: string[]onCheckResult, but the canonicalCheckResultatsrc/domain/models/model.ts:519-522has onlypassanderrors. The runtime inmethod_execution_service.ts:521-531andvalidation_service.ts:413-427only reads.passand.errors— anywarningsreturned by an extension author's check will be silently dropped. An author importingCheckResultfrom the testing package will seewarningsin autocomplete and could write code relying on it, not realizing the runtime ignores it. Additionally, no compat test coversCheckResult, so this drift won't be caught by CI.Suggested fix: Either remove
warningsfrom the testingCheckResultto match canonical, or add it to canonical and wire it through the runtime. Also add a_checkCheckResultFieldsfunction totesting_package_compat_test.ts.
Low
-
as nevercast defeats execute-signature compat check —src/domain/models/testing_package_compat_test.ts:170: the assignmentdef.execute as neveris a type-level no-op —neveris assignable to everything, so this will compile even if the testingMethodDefinition.executesignature diverges completely from canonical (e.g., returnsPromise<void>instead ofPromise<MethodResult>). The brandedDataIdworkaround is understandable, but a stronger check could use an intermediate type that erases the brand without erasing the entire assignment. -
Incorrect comment in compat test —
src/domain/models/testing_package_compat_test.ts:154says "The testing type omitskind" butpackages/testing/model_definition_types.ts:85includeskind?: "create" | "read" | "update" | "delete" | "list" | "action". The comment is factually wrong. The inline union also risks drifting from canonicalMethodKindif new kinds are added. -
Compat test coverage gap for new types —
ResourceOutputSpec,FileOutputSpec,CheckDefinition,CheckResult, andVersionUpgradefrommodel_definition_types.tsare exported to consumers but have no compat checks against their canonical counterparts. The comment attesting_package_compat_test.ts:201-203claims they're "covered transitively" but the function only checksmethodsentries andupgrades.toVersion— it doesn't verify anyresources,files, orchecksrecord value shapes. -
defineModelwidens literal types differently fromsatisfies— The docs atpackages/testing/model_definition_types.ts:160andreferences/typing.md:203claim the contract is "identical" tosatisfies. At runtime this is true, but at the type leveldefineModelreturnsModelDefinition<TGlobalArgs>, which widens.typefrom the literal (e.g.,"@myorg/my-model") tostringand.methodsfrom the literal record toRecord<string, MethodDefinition<...>>. Withsatisfies, TypeScript preserves the literal. This matters if downstream code relies on specific key names. Consider noting this distinction in the doc.
Verdict
PASS — This is a well-structured, additive change that solves a real DX pain point (TS7006 under strict mode) without breaking existing consumers. The generic parameterization of MethodContext is backward-compatible via defaults, the type-level tests are load-bearing, and the compat tests cover the critical paths. The medium finding (warnings on CheckResult) should be addressed before or shortly after merge to avoid misleading extension authors, but it doesn't block.
Summary
_test.tsimports the model source hitTS7006(implicitanyon execute parameters) under strict mode.ModelDefinition<TGlobalArgs>in@systeminit/swamp-testingthat authors opt into viasatisfies ModelDefinition<typeof Schema>(or thedefineModelfunction-form alternative). The literal stays identical — only a trailing satisfies and a type-only import are added.context.globalArgsnarrows to the schema-inferred shape (verified by probing an undeclared field → TS2339). This is a real type-safety upgrade, not a TS7006 silencer.: anyworkaround keeps working for anyone already using it.What's included
packages/testing/model_definition_types.ts— new type-only file withModelDefinition,MethodDefinition,CheckDefinition,ResourceOutputSpec,FileOutputSpec,VersionUpgrade,CheckResult, and thedefineModelhelper.packages/testing/types.ts—MethodContext<TGlobalArgs = Record<string, unknown>>(additive, backward compatible).packages/testing/mod.ts— new "Model authoring" export block.packages/testing/model_definition_types_test.ts— type-level regression test asserting bothsatisfiesanddefineModelpaths narrowglobalArgsunder strict+noImplicitAny.src/domain/models/testing_package_compat_test.ts— extended to guard drift between testing and canonicalModelDefinition/MethodDefinition..claude/skills/swamp-extension-model/references/typing.md— new reference with before/after, "when not to use," and cross-links to the compat test and issue chore: Write end to end tests for data commands #141..claude/skills/swamp-extension-model/SKILL.md— Key Rule feat: add model validate command with validate-all support #5 points at typing.md..claude/skills/swamp-extension-model/references/testing.md— cross-link banner at the top.packages/testing/README.md— new "Model authoring escape hatch" subsection.packages/testing/deno.json— bumped 0.2.0 → 0.3.0.Author-facing usage
Why not a per-method args-narrowing helper
The plan's initial
defineModelsketch intended per-methodargsinference via mapped-intersection generics. Both the F-bounded recursive constraint and theOmit-based override pattern failed contextual typing during inference — per-method args narrowing for inline literals requires a builder-style API (as used by tRPC / zod's own.input()chain), which is out of scope here. Authors narrowargsper method withMySchema.parse(args)at runtime (the canonical Zod way).typing.mddocuments this.Test plan
deno check main.ts packages/testing/mod.tspassesdeno run test— all 4505 existing tests pass; new 4 type-level + smoke tests passdeno lintcleandeno fmtapplieddeno run review-skills— all skills pass the 90% thresholdsatisfies ModelDefinition<typeof Schema>anddefineModel({ ... }); narrowing probe confirmscontext.globalArgs.regionis typed andcontext.globalArgs.undeclaredFielderrors as expected.description: numberinto testing-sideMethodDefinitionimmediately failedtesting_package_compat_test.tsas designed.🤖 Generated with Claude Code