T-DJ5G-gate-create-allowlist-covers-schema-fields
Status: open/ready · Impact: medium · Complexity: small
AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-08-03 because the task was spawned non-interactively. Review the Goal, Approach, Today, Files-to-touch, and Acceptance-criteria carefully before trusting it.
Entity create ops carry a hand-maintained fields: allowlist that must be
edited in lockstep with the entity’s frontmatter schema. The shared factory
validates in one direction only — a declared field missing from the schema
throws, but a schema field missing from the declaration is silently invisible,
so the new flag never appears on the CLI and nothing fails. Add a gate that
checks the other direction: a schema field must be either declared creatable or
explicitly marked non-creatable, so the omission fails at commit time instead of
surfacing through a hand-run --help check.
Adding
kindto the Task schema did not make--kindappear onsdlc task create:apps/sdlc/lib/model/entities/task/ops/create.tscarries an explicitfields:allowlist that must be edited in lockstep with every new schema field, and the omission surfaced only through a manual--helpcheck. A gate asserting that each entity create op’s allowlist covers every creatable schema field would catch this class of omission automatically. This is an sdlc-general convention (create ops are sdlc’s own surface), so the gate belongs inapps/sdlc/lib/services/gate, not in repo-level tooling.
| Location | Role today |
|---|---|
apps/sdlc/lib/model/entities/task/ops/create.ts | The task create spec. Its fields: map names the nine creation-settable inputs by hand; a schema field absent here produces no CLI flag and no error. |
apps/sdlc/lib/model/ops/_create.ts | The shared defineCreateOp factory. resolveFieldSchema throws when a DECLARED field is absent from the entity schema, but nothing walks the schema looking for undeclared fields. |
apps/sdlc/lib/model/entities/task/schema.ts | TaskSchema — the source of truth for task field types and static defaults. Gaining a key here changes validation but not the create surface. |
apps/sdlc/lib/model/entities/_registry.ts | ENTITY_SCHEMAS maps each entity type name to its Zod schema; the enumeration a coverage check would walk. |
apps/sdlc/lib/model/entities/task/ops/_lint_state_origin_core.ts | Holds TASK_STATE_FIELDS, the existing hand-listed set of workflow-owned task fields — prior art for naming fields that are not author-set. |
apps/sdlc/lib/services/gate/ops/ | The sdlc-general gate ops (skill-prose, worktree-scope, markdown-fixtures, corpus-invocation) — where this check belongs per the root CLAUDE.md split. |
apps/sdlc/lib/_generated/op-manifest.ts | Generated static barrel that self-registers op modules; regenerated by apps/sdlc/scripts/gen-op-manifest.ts. |
lefthook.yml | Pre-commit wiring. Existing gates run as project-check-* entries shelling out to bun apps/sdlc/cli/sdlc.ts gate .... |
Proposed
Section titled “Proposed”sdlc gate create-fields-coverage walks the entity-schema registry and, for
each type that registered a create spec, diffs the schema’s frontmatter keys
against the spec’s declared fields: keys. A key that is neither declared
creatable nor explicitly listed as non-creatable is reported as uncovered and
the gate exits non-zero, naming the entity, the field, and the create-op file to
edit. Non-creatable keys become an explicit nonCreatable: declaration on the
create spec, sitting next to the allowlist it complements, so adding a schema
field forces a conscious choice between “expose it at birth” and “workflow owns
it” instead of defaulting to silence. The gate runs in lefthook.yml alongside
the other pre-commit checks.
Approach
Section titled “Approach”- In
apps/sdlc/lib/model/ops/_create.ts, add an optionalnonCreatable?: string[]key toCreateOpSpec, documented as the schema keys deliberately not settable at birth (workflow-owned state, legacy fields). Add a module-levelCOMMON_NON_CREATABLEset for the keys the authoring pipeline itself owns and no create spec should declare:id,type,schema_version,title,last_reviewed. - In the same file, have
defineCreateOprecord each spec into an exported registry —CREATE_SPECS: Map<string, { fieldKeys: string[]; nonCreatable: string[] }>keyed by entity type — so the gate reads declarations from the live op definitions rather than re-parsing TypeScript source. Note thatfmKeyis the schema-side name when it differs from the input key, so recordfield.fmKey ?? key; skipnoFrontmatterfields (task’sheadline), which are op-only and have no schema key. - Add the gate op at
apps/sdlc/lib/services/gate/ops/create-fields-coverage.tsusingdefineOpwithpath: ["gate", "create-fields-coverage"]andhidden: true(matchingworktree-scope.ts). Its handler importsENTITY_SCHEMASandCREATE_SPECS, and for each entry present in both, computesobjectShape(schema)keys minus declared keys minus that spec’snonCreatableminusCOMMON_NON_CREATABLE. Types with no create spec are skipped, not failed. - Give it output
{ checked: string[]; uncovered: Array<{ type, field, opPath }> }and arenderhook that prints one line per uncovered field on stderr — naming the entity type, the field, and the create-op path to edit — and returns 1 whenuncoveredis non-empty, 0 otherwise. - Populate
nonCreatableon the six create specs found byls apps/sdlc/lib/model/entities/*/ops/create.tsso the gate is green at HEAD. For task that is the workflow-owned and legacy tail:readiness_verified_at,touchpoints_verified_at,definition_gap,completion_note,relevance_note,prs,parent_key,depends_on,resolution,resolution_date,resolution_commit. Reconcile the choice againstTASK_STATE_FIELDSin_lint_state_origin_core.tsand note in the spec’s doc-comment where the two sets differ and why. - Regenerate the op barrel by running
apps/sdlc/scripts/gen-op-manifest.tsunderbun run. - Wire the gate into
lefthook.ymlas apre-commitentry (project-check-create-fields-coverage) runningbun apps/sdlc/cli/sdlc.ts gate create-fields-coverage, placed beside the existingproject-check-skill-proseentry. - Add
apps/sdlc/lib/services/gate/tests/create_fields_coverage.test.tscovering: the live registry is clean; a synthetic schema/spec pair with an undeclared key reports exactly that key; a key listed innonCreatableis not reported; anfmKey-renamed field is matched by its schema-side name.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
apps/sdlc/lib/services/gate/ops/create-fields-coverage.ts | new | The gate op: diff each registered create spec’s declared keys against its entity schema’s keys. |
apps/sdlc/lib/services/gate/tests/create_fields_coverage.test.ts | new | Unit tests for the coverage diff, the nonCreatable exemption, and fmKey renames. |
apps/sdlc/lib/model/ops/_create.ts | modify | Add nonCreatable to CreateOpSpec, the COMMON_NON_CREATABLE set, and the exported CREATE_SPECS registry populated by defineCreateOp. |
apps/sdlc/lib/model/entities/task/ops/create.ts | modify | Declare the task schema’s workflow-owned and legacy keys as nonCreatable. |
apps/sdlc/lib/model/entities/backlog/ops/create.ts | modify | Declare that entity’s non-creatable schema keys. |
apps/sdlc/lib/model/entities/milestone/ops/create.ts | modify | Declare that entity’s non-creatable schema keys. |
apps/sdlc/lib/model/entities/reference/ops/create.ts | modify | Declare that entity’s non-creatable schema keys. |
apps/sdlc/lib/model/entities/standard/ops/create.ts | modify | Declare that entity’s non-creatable schema keys. |
apps/sdlc/lib/model/entities/term/ops/create.ts | modify | Declare that entity’s non-creatable schema keys. |
apps/sdlc/lib/_generated/op-manifest.ts | modify | Regenerated barrel picks up the new gate module. |
lefthook.yml | modify | Add the pre-commit entry that runs the new gate. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
bun apps/sdlc/cli/sdlc.ts gate create-fields-coverageexits 0 on a clean checkout, with the six specs listed byls apps/sdlc/lib/model/entities/*/ops/create.tseach declaring or exempting their schema’s keys. - AC-2: A test in
apps/sdlc/lib/services/gate/tests/create_fields_coverage.test.tsadds a schema key that is absent from both the spec’sfields:and itsnonCreatable:, and asserts the gate reports that key by name and returns a non-zero render exit code. - AC-3: A test in the same file asserts a key listed in
nonCreatable:is absent from theuncoveredoutput. - AC-4: A test in the same file asserts a field declared with
fmKey: "target_date"is matched against the schema keytarget_date, not the input key. - AC-5:
apps/sdlc/lib/_generated/op-manifest.tscontains an import of@lib/services/gate/ops/create-fields-coverage.ts, andlefthook.ymlcontains apre-commitentry invokinggate create-fields-coverage. - AC-6:
bun test apps/sdlcandbunx tsc --noEmitboth exit 0.
Out of scope
Section titled “Out of scope”- Auto-generating the
fields:allowlist from the schema. The gate reports the omission; the author still decides how the field is exposed. - The update-op field surface in
apps/sdlc/lib/model/ops/_update.ts. A sibling gap worth its own task once this direction is proven. - Template and skill-prose coverage — whether a newly creatable field also appears in the entity’s Eta template or its authoring skill.
- Backfilling missing flags discovered by the gate beyond what is needed to make the six current specs pass.
Dependencies
Section titled “Dependencies”- T-JOXA-task-kind-field-and-leaf-dispatch adds
kindto the task schema. If it merges first,kindmust be declared creatable or non-creatable here; if this task merges first, that task’s PR carries the declaration.
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-08-03 UTC from
T-JOXA-task-kind-field-and-leaf-dispatch in git@github.com:sksizer/dev.git.