T-5WOP-parity-fixture-derived-from-zod
Status: closed/superseded · Impact: medium · Complexity: small
AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-08-03 as part of the spawn flow’s readiness drive. Review the Goal, Approach, Today, Files-to-touch, and Acceptance-criteria carefully before trusting it.
The schema-parity gate has an unguarded edge between the zod schema and the JSON fixture it compares against the ontogen-generated TypeScript types, so a zod-side field addition can leave the fixture stale while the gate stays green. Closing that edge — deriving the fixture from the schema and failing on drift — makes the gate catch the class of miss that T-HTN8-lease-gate-memo-fields hit.
Nothing guards the zod-to-fixture edge of the schema-parity gate: apps/sdlc/desktop/scripts/fixtures/parity/task-lifecycle-lease.schema.json is a hand-maintained projection of TaskLifecycleLeaseSchema, and T-HTN8’s two new fields silently left it stale. The gate only compares that fixture against the generated Rust types, so it stayed green. A check that derives the fixture from the zod schema and fails on drift would have caught it at the gate.
— T-HTN8-lease-gate-memo-fields on git@github.com:sksizer/dev.git
| Location | Role today |
|---|---|
apps/sdlc/lib/services/lease/schemas.ts#TaskLifecycleLeaseSchema | Zod source of truth for the task-lifecycle lease payload; new fields land here first. |
apps/sdlc/desktop/scripts/fixtures/parity/task-lifecycle-lease.schema.json | Hand-written JSON projection of that zod schema. Its $comment asks a human to re-read schemas.ts when the schema changes; nothing enforces it. |
apps/sdlc/desktop/scripts/schema-parity.ts#runParity | Compares the checked-in fixture against the generated TypeScript types. It never reads the zod schema, so a stale fixture passes. |
apps/sdlc/desktop/scripts/schema-parity.ts#schemaFields | Treats a fixture field as optional when it is absent from required, matching the TS-side rule that T | null is optional. |
apps/sdlc/desktop/scripts/schema-parity.map.json | Maps the generated type name TaskLifecycleLease to the fixture path. |
apps/sdlc/lib/services/kind_render.ts#jsonSchemaFor | Existing zod-to-JSON-Schema projection (z.toJSONSchema(schema, { io: "input" })) behind sdlc commit get-schema. |
.claude/skills/project-check/check_apps_imports.ts | Repo-level gate enforcing S-0008-apps-consume-substrate-through-published-surfaces; it is why the fixture is hand-projected instead of imported. |
.claude/skills/project-check/check_util_redeclarations.ts | Repo-level gate that already carries the --write-baseline regeneration-flag convention this task copies. |
lefthook.yml | Runs the project-check-* gates pre-commit. |
.github/workflows/sdf-ci.yml | Runs moon run sdf:schema-parity and the parity self-test in CI. |
Proposed
Section titled “Proposed”A repo-level checker derives the lease fixture from
TaskLifecycleLeaseSchema and exits non-zero when the checked-in JSON no
longer matches — naming each field that differs — so a zod-side field addition
cannot leave the fixture stale and green. The checker runs pre-commit through
lefthook.yml and in sdf-ci.yml, and a --write flag rewrites the fixture
so the fix is mechanical rather than hand-edited.
The checker lives under .claude/skills/project-check/, which is repo-level
tooling rather than application code, so it may import
apps/sdlc/lib/services/lease/schemas.ts directly without weakening
S-0008-apps-consume-substrate-through-published-surfaces — the sibling gates there
already import the substrate the same way.
Approach
Section titled “Approach”- Add
.claude/skills/project-check/check_parity_fixture_drift.ts. It holds a small in-file binding table — today one entry: the fixture pathapps/sdlc/desktop/scripts/fixtures/parity/task-lifecycle-lease.schema.jsonpaired withTaskLifecycleLeaseSchema— so a second lease fixture is one table row, not a rewrite. - Project the bound zod schema with
z.toJSONSchema(schema, { io: "input" }), the same callapps/sdlc/lib/services/kind_render.ts#jsonSchemaFormakes. - Reduce the projection to the comparator’s shape: the property-name set, plus
a
requiredlist that drops nullable and defaulted fields. That is the fixture’s stated convention (expires_at,pr_number,notesare omitted) and it matches howapps/sdlc/desktop/scripts/schema-parity.ts#schemaFieldsreadsrequiredon the other side. - Read the checked-in fixture, diff its property-name set and
requiredset against the derived pair, print one line per difference (field name plus which side declares it), exit 1 when the diff is non-empty and 0 otherwise. - Add a
--writeflag that rewrites the fixture from the derivation, preserving the$commentheader and the file’s key order, following the--write-baselineconvention in.claude/skills/project-check/check_util_redeclarations.ts. - Add
.claude/skills/project-check/tests/check_parity_fixture_drift.test.tscovering three cases against temp copies: the in-tree pair passes; a fixture missing a zod-declared field fails naming that field; a fixture declaring a field the zod schema does not fails naming that field. - Wire the checker into
lefthook.ymlas aproject-check-parity-fixtureentry alongside the otherproject-check-*runs, and into.github/workflows/sdf-ci.ymlnext to the existing schema-parity steps. - Rewrite the fixture’s
$commentso it states the file is generated by the checker’s--writemode, replacing the current “regenerate by re-reading schemas.ts” instruction.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
.claude/skills/project-check/check_parity_fixture_drift.ts | new | Derives the fixture from the bound zod schema, diffs it against the checked-in JSON, --write regenerates. |
.claude/skills/project-check/tests/check_parity_fixture_drift.test.ts | new | Passes on the in-tree pair; fails on a seeded missing field and on a seeded extra field. |
lefthook.yml | modify | Adds the project-check-parity-fixture pre-commit entry with a comment explaining the guarded edge. |
.github/workflows/sdf-ci.yml | modify | Runs the checker alongside moon run sdf:schema-parity. |
apps/sdlc/desktop/scripts/fixtures/parity/task-lifecycle-lease.schema.json | modify | $comment rewritten to name the checker and its --write mode as the regeneration path. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
bun run .claude/skills/project-check/check_parity_fixture_drift.tsexits 0 against an unmodified checkout. - AC-2: With a field added to
TaskLifecycleLeaseSchemaand the fixture left untouched, that command exits non-zero and prints the added field’s name. - AC-3: With a field deleted from the fixture that the zod schema declares, that command exits non-zero and prints the deleted field’s name.
- AC-4: After a drift is introduced,
--writerewrites the fixture and a following run of the checker exits 0. - AC-5: The derived
requiredlist omitsexpires_at,pr_number, andnotes, so the regenerated fixture is byte-comparable to the committed one on an unmodified checkout. - AC-6:
bun test .claude/skills/project-check/tests/check_parity_fixture_drift.test.tspasses. - AC-7:
lefthook.ymland.github/workflows/sdf-ci.ymleach invoke the checker.
Out of scope
Section titled “Out of scope”- A general
sdlc lease get-schema <kind>verb mirroringsdlc commit get-schema. Worth adding when a consumer outside repo-level tooling needs the lease projection; the checker here imports the schema directly and does not need it. - The other lease payload schemas (operation lease, control-plane tooling). They have no parity fixture today, so there is no edge to guard.
- Widening what the parity comparator checks (types, enums, string formats).
This task guards field names and optionality only, matching what
apps/sdlc/desktop/scripts/schema-parity.ts#compareShapesalready compares.
Dependencies
Section titled “Dependencies”- none. T-HTN8-lease-gate-memo-fields is the originating task, not a blocker: this checker lands independently and will flag the fixture if that work merges without regenerating it.
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-08-03 UTC from
T-HTN8-lease-gate-memo-fields in git@github.com:sksizer/dev.git.