T-E9C9-project-check-flags-operations-column-drift
Status: closed/done · Impact: medium · Complexity: small
The entity corpus ships some definition.md Operations tables in the
legacy 3-column shape (Operation / CLI / What it does), while
D-0004-entity-definition-architecture fixes the canonical 5-column
shape (Name / Surface / Signature / Pointer / Description). The
parseOperationsTable() projection in plugin/lib/model/entity.ts
tolerates both via column aliasing, but the migration is otherwise
invisible. Surface a warning when any definition.md still uses the
legacy shape so the migration is visible in CI.
From the originating task’s post-mortem:
“The project-check declared-surface drift rule should explicitly cover Operations-table column drift — surface a warning when a definition.md still uses the legacy 3-column shape, so the migration is visible in CI rather than discovered incidentally during implementation.”
| Location | Role today |
|---|---|
.claude/skills/project-check/check_entities.ts#checkOneEntity | The TS entity-consistency checker’s per-entity loop. For each entity dir it walks schema.json plus every reference *.md (backticked field/status refs) and validates the shipped body template (configuration.entity.bodyTemplateFile = body-template.eta). It does NOT read definition.md at all — there is no Operations-table walk in the checker today, so this rule must ADD a definition.md read. |
plugin/lib/model/entity.ts#parseOperationsTable | Already parses a definition.md’s ## Operations table into Operation[], preserving the raw header→cell mapping in Operation.columns. On a legacy 3-col header it still parses, aliasing Signature ← CLI and Description ← What it does, leaving surface/pointer null and columns keyed by Operation / CLI / What it does. This is the general capability the check should consume; the project-check script stays a thin adapter over it. |
plugin/lib/model/entities/task/definition.md, plugin/lib/model/entities/backlog/definition.md | In-repo exemplars: both already carry the canonical 5-col | Name | Surface | Signature | Pointer | Description | header (landed with T-0010 / #241). |
plugin/lib/model/entities/decision/definition.md | Carries an ## Operations H2 with the legacy 3-column header (| Operation | CLI | What it does |). |
plugin/lib/model/entities/milestone/definition.md | Same legacy 3-column Operations header. |
plugin/lib/model/entities/principle/definition.md | Same legacy 3-column Operations header. |
plugin/lib/model/entities/standard/definition.md | Same legacy 3-column Operations header. |
docs/planning/decisions/D-0004-entity-definition-architecture.md | The ADR fixing the canonical 5-column shape (Name / Surface / Signature / Pointer / Description) in its “Operations declaration and consumption” section; the authority this check enforces drift against. |
.claude/skills/project-check/tests/check_entities.test.ts | The eval harness for the checker. Each fixture under tests/fixtures/<case>/ is a self-contained entities root with an expected exit code; new behaviour is proven by adding fixtures. |
.claude/skills/project-check/tests/fixtures/ | Fixture directory the harness walks; legacy-shape and canonical-shape fixtures land here. |
.claude/skills/project-check/SKILL.md | The skill spec. Section “1. Entity artifact consistency — structural” describes what check_entities.ts checks; gains a bullet for the new rule. |
Proposed
Section titled “Proposed”Extend check_entities.ts with a new deterministic check that reads each
entity definition.md, parses its ## Operations table header via
plugin/lib/model/entity.ts’s parseOperationsTable(), and flags drift from
the D-0004 canonical 5-column shape. The check emits a warning-class finding
(not a hard structural failure) for the legacy 3-column shape, so the
in-progress migration is visible in CI without blocking commits on the
not-yet-migrated entities (decision, standard, principle, milestone).
The general capability — parsing the Operations table — already lives in
lib (parseOperationsTable), so the project-check script stays a thin
adapter: it inspects the parsed Operation.columns keys (the raw header
names) and classifies the header shape. task and backlog definition.md
already carry the canonical header (T-0010 / #241), so they are the in-repo
exemplars the OK case is proven against.
Approach
Section titled “Approach”- Add a
checkOperationsTableShape(definitionPath)helper tocheck_entities.ts. For each entity dir, locatedefinition.md. If absent (todayepic/has none), skip silently — absence is not drift. - Parse the
## Operationstable viaparseOperationsTable()fromplugin/lib/model/entity.ts. The returnedOperation[]carries the raw header→cell mapping inOperation.columns; the ordered header names are the keys ofcolumnson any parsed row. (When the table has no body rows,parseOperationsTablereturns[]; the helper then falls back to reading the header line directly, or treats a literal “No operations” body as OK.) Normalize the header names (trim, lowercase) into an ordered list. - Classify the header against the two known shapes. Canonical (D-0004) is
the five columns Name / Surface / Signature / Pointer / Description → OK,
no finding. Legacy is the three columns Operation / CLI / What it does →
warning naming both the legacy columns found and the D-0004 target shape.
A header matching neither (partial migration / typo) → a distinct warning
naming the unrecognized columns. An
## Operationssection whose body is the literal “No operations” value (allowed by D-0004 for op-less types) → OK, no finding. - Route the finding through a warning channel, not the hard-fail error
list. Today
checkOneEntitypushes every problem onto oneerrors[]that drivestotalErrorsand exit 1. Add a parallelwarnings[]collection so legacy-shape drift is reported (printed under aWARNprefix inmain’s per-entity output) but does NOT change the exit code while the four entities are unmigrated. This keeps the lefthook gate green until the corpus migrates, matching the Goal’s “warning … so the migration is visible” intent. - Wire the warnings into
main’s output and summary. Print warnings per entity and add a trailingN Operations-table drift warning(s)line; warnings do not contribute tototalErrors(so the exit code is unaffected). - Add eval fixtures under
tests/fixtures/: one entities root whosedefinition.mdcarries the legacy 3-column header (expect exit 0 with the warning on stdout), one with the canonical 5-column header (expect exit 0, no warning), and one with an unrecognized header (expect exit 0, distinct warning). Extendtests/check_entities.test.tsto assert the warning text appears (or not) per fixture, since exit code alone no longer distinguishes the warning cases. - Document the new rule in
.claude/skills/project-check/SKILL.mdunder “1. Entity artifact consistency — structural”: one bullet stating the Operations-table header is checked against the D-0004 5-column shape, that legacy 3-column is a warning, and that the warning is informational (does not fail the gate) until the corpus migration completes.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
.claude/skills/project-check/check_entities.ts | modify | Add checkOperationsTableShape (reading definition.md and consuming parseOperationsTable from plugin/lib/model/entity.ts), a warnings[] channel in checkOneEntity, and warning printing plus a summary line in main. |
.claude/skills/project-check/tests/check_entities.test.ts | modify | Assert the warning text for the legacy-shape fixture and its absence for the canonical-shape fixture; exit codes stay 0. |
.claude/skills/project-check/SKILL.md | modify | Add a bullet describing the Operations-table column-drift check under the structural-consistency section. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: Running
check_entities.tsagainst the real entities tree emits an Operations-table drift warning for each ofdecision,milestone,principle, andstandard(all four still carry the legacy 3-column header today);taskandbacklogproduce no warning (already canonical). - AC-2: The warning text names the entity, the legacy header columns it found, and the D-0004 canonical 5-column shape (Name / Surface / Signature / Pointer / Description).
- AC-3: An entity whose
definition.mdOperations table uses the canonical 5-column header produces no Operations-table warning (proven against thetask/backlogexemplars and a canonical-shape fixture). - AC-4: The drift is reported as a warning, not a hard failure:
check_entities.tsagainst a fixtures root whose only issue is legacy Operations-table shape exits 0, so the lefthook pre-commit gate stays green while the four entities are unmigrated. - AC-5: An entity dir with no
definition.md(e.g.epic/) produces no Operations-table warning (absence is not drift). - AC-6:
bun test .claude/skills/project-check/tests/check_entities.test.tspasses, including the new legacy / canonical / unrecognized fixtures. - AC-7:
.claude/skills/project-check/SKILL.mddocuments the new check under the structural-consistency section, stating it is warning-class until the corpus migration completes.
Out of scope
Section titled “Out of scope”- Migrating the four entity
definition.mdfiles (decision, standard, principle, milestone) to the 5-column shape — that is the migration this check makes visible. - Promoting the warning to a hard structural failure — deferred until every shipped
definition.mdcarries the 5-column shape; until then a hard fail would break the gate on the unmigrated corpus. - Validating the Operations table’s row contents (Surface-value semantics, Pointer resolution per D-0004’s Surface semantics) — that is the declared-surface drift check owned by T-0001; this task checks only the column-header shape.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-05-31 UTC from T-0011 in git@github.com:sksizer/dev.git.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-06-04. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: agent-manual — ran
check_entities.tsagainst the real entities tree; oneWARN ... legacy 3-column headerline emitted for each ofdecision,milestone,principle,standard;taskandbacklogemitted no warning. Summary line4 Operations-table drift warning(s). - AC-2: agent-manual — each warning names the entity (
entities/<name>/definition.md), the legacy columns found (Operation / CLI / What it does), and the D-0004 target (Name / Surface / Signature / Pointer / Description); confirmed in the same real-tree run. - AC-3: auto —
ops-canonical-shapefixture asserts exit 0 withno driftand no WARN; real-treetask/backlogexemplars emitted no warning. - AC-4: auto —
ops-legacy-shapefixture exits 0 with the WARN line (bun testcase + a dedicated canonical-no-warning test); the real-tree run also exits 0 (no drift) despite four warnings. - AC-5: agent-manual —
capability,driver,product(no legacy## Operationsdrift) and any dir withoutdefinition.mdemit no warning in the real-tree run; absence short-circuits incheckOperationsTableShape. - AC-6: auto —
bun test .../check_entities.test.ts→ 14 pass / 0 fail, including the legacy / canonical / unrecognized fixtures. - AC-7: auto —
.claude/skills/project-check/SKILL.mdgained a bullet under the structural-consistency section stating the check is warning-class until the corpus migration completes (verified by reading the committed diff).
What worked
Section titled “What worked”- The implementation was committed by a prior (killed) run; resume picked up cleanly from a clean worktree with the feat commit in place.
- The shared
parseOperationsTable()capability inplugin/lib/model/entity.tswas reused as designed — the project-check side stayed a thin classifier over the parsed header, no table-parsing logic duplicated. - Fixtures + a dedicated canonical-no-warning test made the warning-class behavior (exit 0 with a finding) provable, which exit code alone can’t distinguish.
Friction and automation gaps
Section titled “Friction and automation gaps”- Resume after a killed run left an orphan heartbeat process and a stale
lease-heartbeat-<basename>.pidfile under the worktree’s.sdlc/runtime/; the resume path had to kill the PID and rm the file by hand — task-work’s resume-detection (Step 2) should reap an orphan heartbeat (kill the recorded PID + remove the stale.pid) as part of resume, the sametry/finallyreaping Step 6 does on the happy path. → T-IZLC-task-work-resume-reaps-orphan-heartbeat run_quality_checks.ts --diff-against-baselineagainstaudit_entities.tsproduced 12 false-positivenew-driftlines that were all benign- OKcorpus-listing rows and a file-count header, flagged only because the worktree’s corpus was behindorigin/main(250 vs 247 tasks) before the Step 9 rebase — the baseline line-diff is sensitive to corpus membership churn, not just schema drift. Already tracked by T-TWZD-normalize-baseline-diff-nondeterministic-output; the practical fix is to capture the baseline AFTER the Step 9 sync (or have the gate ignore audit’s per-file- OKlisting lines, per T-BCNP-quality-gate-ignores-summary-and-corpus-lines). → T-TWZD-normalize-baseline-diff-nondeterministic-output
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-IZLC-task-work-resume-reaps-orphan-heartbeat (https://github.com/sksizer/dev/pull/288) — spawned: task-work resume reaps the orphan heartbeat process + stale pid file (Upstream-plugin / sdlc-meta).
- T-TWZD-normalize-baseline-diff-nondeterministic-output — linked: existing tracker for the baseline-diff corpus-churn false-positive class.