Skip to content

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

LocationRole today
.claude/skills/project-check/check_entities.ts#checkOneEntityThe 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#parseOperationsTableAlready 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.mdIn-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.mdCarries an ## Operations H2 with the legacy 3-column header (| Operation | CLI | What it does |).
plugin/lib/model/entities/milestone/definition.mdSame legacy 3-column Operations header.
plugin/lib/model/entities/principle/definition.mdSame legacy 3-column Operations header.
plugin/lib/model/entities/standard/definition.mdSame legacy 3-column Operations header.
docs/planning/decisions/D-0004-entity-definition-architecture.mdThe 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.tsThe 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.mdThe skill spec. Section “1. Entity artifact consistency — structural” describes what check_entities.ts checks; gains a bullet for the new rule.

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.

  1. Add a checkOperationsTableShape(definitionPath) helper to check_entities.ts. For each entity dir, locate definition.md. If absent (today epic/ has none), skip silently — absence is not drift.
  2. Parse the ## Operations table via parseOperationsTable() from plugin/lib/model/entity.ts. The returned Operation[] carries the raw header→cell mapping in Operation.columns; the ordered header names are the keys of columns on any parsed row. (When the table has no body rows, parseOperationsTable returns []; 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.
  3. 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 ## Operations section whose body is the literal “No operations” value (allowed by D-0004 for op-less types) → OK, no finding.
  4. Route the finding through a warning channel, not the hard-fail error list. Today checkOneEntity pushes every problem onto one errors[] that drives totalErrors and exit 1. Add a parallel warnings[] collection so legacy-shape drift is reported (printed under a WARN prefix in main’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.
  5. Wire the warnings into main’s output and summary. Print warnings per entity and add a trailing N Operations-table drift warning(s) line; warnings do not contribute to totalErrors (so the exit code is unaffected).
  6. Add eval fixtures under tests/fixtures/: one entities root whose definition.md carries 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). Extend tests/check_entities.test.ts to assert the warning text appears (or not) per fixture, since exit code alone no longer distinguishes the warning cases.
  7. Document the new rule in .claude/skills/project-check/SKILL.md under “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.
LocationKindChange
.claude/skills/project-check/check_entities.tsmodifyAdd 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.tsmodifyAssert 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.mdmodifyAdd a bullet describing the Operations-table column-drift check under the structural-consistency section.
  • AC-1: Running check_entities.ts against the real entities tree emits an Operations-table drift warning for each of decision, milestone, principle, and standard (all four still carry the legacy 3-column header today); task and backlog produce 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.md Operations table uses the canonical 5-column header produces no Operations-table warning (proven against the task / backlog exemplars and a canonical-shape fixture).
  • AC-4: The drift is reported as a warning, not a hard failure: check_entities.ts against 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.ts passes, including the new legacy / canonical / unrecognized fixtures.
  • AC-7: .claude/skills/project-check/SKILL.md documents the new check under the structural-consistency section, stating it is warning-class until the corpus migration completes.
  • Migrating the four entity definition.md files (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.md carries 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.
  • none

Spawned by /sdlc:spawn-task-pr on 2026-05-31 UTC from T-0011 in git@github.com:sksizer/dev.git.

Captured by /sdlc:task-work on 2026-06-04. PR: pending.

  • AC-1: agent-manual — ran check_entities.ts against the real entities tree; one WARN ... legacy 3-column header line emitted for each of decision, milestone, principle, standard; task and backlog emitted no warning. Summary line 4 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-shape fixture asserts exit 0 with no drift and no WARN; real-tree task/backlog exemplars emitted no warning.
  • AC-4: auto — ops-legacy-shape fixture exits 0 with the WARN line (bun test case + 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 ## Operations drift) and any dir without definition.md emit no warning in the real-tree run; absence short-circuits in checkOperationsTableShape.
  • 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.md gained a bullet under the structural-consistency section stating the check is warning-class until the corpus migration completes (verified by reading the committed diff).
  • 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 in plugin/lib/model/entity.ts was 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.
  • Resume after a killed run left an orphan heartbeat process and a stale lease-heartbeat-<basename>.pid file 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 same try/finally reaping Step 6 does on the happy path. → T-IZLC-task-work-resume-reaps-orphan-heartbeat
  • run_quality_checks.ts --diff-against-baseline against audit_entities.ts produced 12 false-positive new-drift lines that were all benign - OK corpus-listing rows and a file-count header, flagged only because the worktree’s corpus was behind origin/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 - OK listing lines, per T-BCNP-quality-gate-ignores-summary-and-corpus-lines). → T-TWZD-normalize-baseline-diff-nondeterministic-output

← Back to Tasks