Skip to content

feat(extensions): ship ModelDefinition escape hatch for TS7006 (swamp-club#141)#1205

Merged
stack72 merged 1 commit intomainfrom
worktree-141
Apr 21, 2026
Merged

feat(extensions): ship ModelDefinition escape hatch for TS7006 (swamp-club#141)#1205
stack72 merged 1 commit intomainfrom
worktree-141

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented Apr 21, 2026

Summary

  • Fixes swamp-club#141: extension authors whose _test.ts imports the model source hit TS7006 (implicit any on execute parameters) under strict mode.
  • Ships 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.
  • context.globalArgs narrows to the schema-inferred shape (verified by probing an undeclared field → TS2339). This is a real type-safety upgrade, not a TS7006 silencer.
  • 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.

What's included

  • packages/testing/model_definition_types.ts — new type-only file with ModelDefinition, MethodDefinition, CheckDefinition, ResourceOutputSpec, FileOutputSpec, VersionUpgrade, CheckResult, and the defineModel helper.
  • packages/testing/types.tsMethodContext<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 both satisfies and defineModel paths narrow globalArgs under strict+noImplicitAny.
  • src/domain/models/testing_package_compat_test.ts — extended to guard drift between testing and canonical ModelDefinition / 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

import { z } from "npm:zod@4";
import type { ModelDefinition } from "jsr:@systeminit/swamp-testing";

const GlobalArgsSchema = z.object({ region: z.string() });

export const model = {
  type: "@myorg/my-model",
  version: "2026.04.21.1",
  globalArguments: GlobalArgsSchema,
  methods: {
    run: {
      description: "Run the model",
      arguments: z.object({ bucket: z.string() }),
      execute: async (_args, context) => {
        // context.globalArgs narrows to { region: string }
        return { dataHandles: [] };
      },
    },
  },
} satisfies ModelDefinition<typeof GlobalArgsSchema>;

Why not a per-method args-narrowing helper

The plan's initial defineModel sketch intended per-method args inference via mapped-intersection generics. Both the F-bounded recursive constraint and the Omit-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 narrow args per method with MySchema.parse(args) at runtime (the canonical Zod way). typing.md documents this.

Test plan

  • deno check main.ts packages/testing/mod.ts passes
  • deno run test — all 4505 existing tests pass; new 4 type-level + smoke tests pass
  • deno lint clean
  • deno fmt applied
  • deno run review-skills — all skills pass the 90% threshold
  • Scratch-repo verification: reproduces TS7006 on the pre-fix shape; compiles cleanly with satisfies ModelDefinition<typeof Schema> and defineModel({ ... }); narrowing probe confirms context.globalArgs.region is typed and context.globalArgs.undeclaredField errors as expected.
  • Drift-guard verification: injecting description: number into testing-side MethodDefinition immediately failed testing_package_compat_test.ts as designed.

🤖 Generated with Claude Code

…-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]>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. ResourceOutputSpec / FileOutputSpec field mixing (packages/testing/model_definition_types.ts): The testing ResourceOutputSpec includes streaming?: boolean (lines 55) which only exists on the canonical FileOutputSpec, and is missing tags?: Record<string, string> which exists on the canonical ResourceOutputSpec. Conversely, the testing FileOutputSpec includes sensitiveOutput?: boolean and vaultName?: string (lines 68-69) which only exist on the canonical ResourceOutputSpec, and is also missing tags. It looks like some fields were accidentally swapped between the two specs. This means an extension author using satisfies ModelDefinition<...> who declares resource tags will get a spurious TS error, while streaming on a resource will silently compile but do nothing. Suggest fixing the field assignments to match the canonical types in src/domain/models/model.ts:49-94.

  2. CheckResult extra field (packages/testing/model_definition_types.ts:109): The testing CheckResult includes warnings?: string[] which doesn't exist on the canonical CheckResult (src/domain/models/model.ts:519-522). Extension authors returning warnings would think they're handled when they're not.

  3. Compat test gaps for output specs (src/domain/models/testing_package_compat_test.ts): The compat test validates ModelDefinition and MethodDefinition field shapes but doesn't cover ResourceOutputSpec, FileOutputSpec, or CheckResult. Adding structural checks for these types (similar to _checkMethodDefinitionFields) would catch the field-mixing issues in suggestions 1-2 and prevent future drift.

  4. 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.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Review

Critical / High

None found.

Medium

  1. Testing CheckResult adds warnings field not present in canonicalpackages/testing/model_definition_types.ts:108 defines warnings?: string[] on CheckResult, but the canonical CheckResult at src/domain/models/model.ts:519-522 has only pass and errors. The runtime in method_execution_service.ts:521-531 and validation_service.ts:413-427 only reads .pass and .errors — any warnings returned by an extension author's check will be silently dropped. An author importing CheckResult from the testing package will see warnings in autocomplete and could write code relying on it, not realizing the runtime ignores it. Additionally, no compat test covers CheckResult, so this drift won't be caught by CI.

    Suggested fix: Either remove warnings from the testing CheckResult to match canonical, or add it to canonical and wire it through the runtime. Also add a _checkCheckResultFields function to testing_package_compat_test.ts.

Low

  1. as never cast defeats execute-signature compat checksrc/domain/models/testing_package_compat_test.ts:170: the assignment def.execute as never is a type-level no-op — never is assignable to everything, so this will compile even if the testing MethodDefinition.execute signature diverges completely from canonical (e.g., returns Promise<void> instead of Promise<MethodResult>). The branded DataId workaround is understandable, but a stronger check could use an intermediate type that erases the brand without erasing the entire assignment.

  2. Incorrect comment in compat testsrc/domain/models/testing_package_compat_test.ts:154 says "The testing type omits kind" but packages/testing/model_definition_types.ts:85 includes kind?: "create" | "read" | "update" | "delete" | "list" | "action". The comment is factually wrong. The inline union also risks drifting from canonical MethodKind if new kinds are added.

  3. Compat test coverage gap for new typesResourceOutputSpec, FileOutputSpec, CheckDefinition, CheckResult, and VersionUpgrade from model_definition_types.ts are exported to consumers but have no compat checks against their canonical counterparts. The comment at testing_package_compat_test.ts:201-203 claims they're "covered transitively" but the function only checks methods entries and upgrades.toVersion — it doesn't verify any resources, files, or checks record value shapes.

  4. defineModel widens literal types differently from satisfies — The docs at packages/testing/model_definition_types.ts:160 and references/typing.md:203 claim the contract is "identical" to satisfies. At runtime this is true, but at the type level defineModel returns ModelDefinition<TGlobalArgs>, which widens .type from the literal (e.g., "@myorg/my-model") to string and .methods from the literal record to Record<string, MethodDefinition<...>>. With satisfies, 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.

@stack72 stack72 merged commit 3822a93 into main Apr 21, 2026
10 checks passed
@stack72 stack72 deleted the worktree-141 branch April 21, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant