T-NIPN-migrate-reorders-frontmatter-keys
Status: closed/done · Impact: low · Complexity: small
Auto-generated from a /sdlc:task-work post-mortem. Review and
promote to ready before picking up.
migrate_entities.py has two code paths that write schema_version
back into a frontmatter dict — the stamp path (inserts right after
type:) and the transform-chain path (insertion at the bottom).
Both are functionally correct but visually inconsistent across
migrated files, making diffs noisy. A shared
reorder_frontmatter_keys(...) helper called by both paths yields
deterministic, canonical key order. Originating incident:
T-T5RB-consolidate-task-status-enum.
“
yaml.safe_dumpre-emits files with keys in insertion order. The stamp path insertsschema_versionright aftertype:; the transform-chain path inserts it at the bottom of the dict. Functional but visually inconsistent across migrated files. A smallreorder_frontmatter_keys(...)helper called by both paths would close this.”
The two write paths in plugin/scripts/migrate_entities.py produce
different on-disk key order for the same logical state. Files that
went through one path look different from files that went through
the other.
Proposed
Section titled “Proposed”A single reorder_frontmatter_keys(frontmatter: dict, schema: dict) -> dict helper produces canonical key order (driven by schema
property declaration order or an explicit ordering list). Both
write paths route through it before yaml.safe_dump. Re-running
the migrate on already-migrated files is a no-op (idempotent).
Approach
Section titled “Approach”- Move the
readiness_verified_atproperty declaration to the END ofplugin/entities/task/schema.json’sproperties:block (and the same in sibling entity schemas that carry the field). This aligns with T-H0W9-task-work-rebase-frontmatter-conflict’s “stamp at bottom” rule — canonical schema-property order then naturally places the stamp last, so the rebase-conflict fix stays in force after migrate runs. - Add the helper to
plugin/skills/entities-migrate/migrate_entities.py(co-located, perplugin/skills/CLAUDE.md’s co-locate-first rule — the script lives at that path, NOT atplugin/scripts/migrate_entities.py). Order keys by schema property order, with a stable tie-breaker for keys absent from the schema. - Wire both the stamp path and the transform-chain path through the helper.
- Add an eval case asserting (a) two files with identical content
but different starting key order normalize to the same output,
and (b)
readiness_verified_at:lands last after migration.
Files to touch
Section titled “Files to touch”plugin/entities/task/schema.json— reorderreadiness_verified_atto last property; mirror the change in sibling schemas (epic, milestone, backlog) that carry the same field.plugin/skills/entities-migrate/migrate_entities.py— newreorder_frontmatter_keys()helper + call sites at the stamp and transform-chain write paths.plugin/skills/entities-migrate/tests/run_evals.py— idempotency + canonical-order + stamp-at-bottom regression cases.
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: After running
/sdlc:entities-migrate, every migrated file has its frontmatter keys in the same canonical order as defined by the entity schema’s property declaration, withreadiness_verified_at:last. - AC-2: Running
/sdlc:entities-migratea second time against the same repo produces no diff. - AC-3: The structural rebase-conflict scenario in T-H0W9-task-work-rebase-frontmatter-conflict does not re-appear after a migrate pass.
Out of scope
Section titled “Out of scope”- Reordering keys in files that don’t otherwise need migration. The helper only fires when the file is being written for another reason.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:task-work post-mortem of T-T5RB-consolidate-task-status-enum on 2026-05-19.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-21. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto — new
canonical-key-ordereval case inplugin/skills/entities-migrate/tests/run_evals.pyasserts viaexpected_orderthat frontmatter keys after migrate land in the schema’s declared property order withreadiness_verified_at:last. - AC-2: auto — same eval case sets
check_idempotent=True; the re-run path commits the first-run output and re-invokes migrate, asserting zero further file changes. Also exercised on real project data: running migrate against the worktree reportedfixed: 0 file(s)with only pre-existing prose drift surfaced for manual review. - AC-3: auto —
readiness_verified_atis now declared LAST inplugin/entities/task/schema.json’spropertiesblock, soreorder_frontmatter_keysdeterministically places the stamp at the bottom of the frontmatter for any file routed through either write path. The structural rebase-conflict scenario from T-H0W9-task-work-rebase-frontmatter-conflict cannot re-appear after a migrate pass.
What worked
Section titled “What worked”- Both write paths converged on the same final
reorder_frontmatter_keys(...) + yaml.safe_dump(...)step with minimal surface-area change. The transform-chain path took two added lines; the stamp path collapsed two divergent sub-paths (insert vs replace) into a single re-serialize path. - The fixture-driven eval suite caught the change without manual
spot-checking — the existing eight cases stayed green and the new
case asserted the canonical order via
expected_order.
Friction and automation gaps
Section titled “Friction and automation gaps”- The Step 5b rebase surfaced a frontmatter conflict on
last_reviewed:(HEAD: 2026-05-21 set by the start-commit on main; feat-branch verify commit: 2026-05-20). The conflict-resolution effectively dropped the verify commit because both lines landed at the same dict position. Mechanical resolution is straightforward (“take HEAD”), but the task-work skill could automate it: when the rebase conflict is purely onlast_reviewed:, prefer HEAD. Same shape as T-H0W9-task-work-rebase-frontmatter-conflict but one row up. — task-work Step 5b could detect this exact conflict shape and auto-resolve, or the start-commit on main could leavelast_reviewed:untouched and let ensure-ready own it. → T-H0W9-task-work-rebase-frontmatter-conflict - The task spec mentioned mirroring the schema change in “sibling
schemas (epic, milestone, backlog) that carry the same field,” but
none of those schemas actually declare
readiness_verified_at:— only task does. The relevance check noticed and the implementation scoped down. —/sdlc:task-reviewor/sdlc:task-ensure-readycould grep schema files for the cited fields and flag claims like “sibling schemas that carry the same field” as suspect when they don’t actually carry it. → T-ZJXU-task-ensure-ready-verifies-cited-schema-fields - The first
git worktree addchecked out commit f509181 (“chore: start …-extract-start-task-script”) because local main is ahead of origin by several parallel start-commits. The worktree’s feat branch therefore started with contamination from other in-flight tasks. The Step 9 rebase-onto-origin/main is the documented fix, but it would be cleaner if Step 4 branched fromorigin/maindirectly when local main is ahead with start-commit churn. — Step 4 couldgit fetch origin && git worktree add ... origin/maininstead ofmainwhen contamination is detectable. →2026-05-21-task-work-step-4-branches-from-origin-main
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-ZJXU-task-ensure-ready-verifies-cited-schema-fields — spawned: ensure-ready cross-checks schema-field claims in task bodies.
2026-05-21-task-work-step-4-branches-from-origin-main— spawned: Step 4 branches from origin/main to avoid start-commit contamination.- T-H0W9-task-work-rebase-frontmatter-conflict —
linked-existing: the structural rebase-conflict task already
covers the
last_reviewed:shape; this run’s friction is the same root cause one row up.