Skip to content

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_dump re-emits files with keys in insertion order. The stamp path inserts schema_version right after type:; the transform-chain path inserts it at the bottom of the dict. Functional but visually inconsistent across migrated files. A small reorder_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.

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

  1. Move the readiness_verified_at property declaration to the END of plugin/entities/task/schema.json’s properties: 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.
  2. Add the helper to plugin/skills/entities-migrate/migrate_entities.py (co-located, per plugin/skills/CLAUDE.md’s co-locate-first rule — the script lives at that path, NOT at plugin/scripts/migrate_entities.py). Order keys by schema property order, with a stable tie-breaker for keys absent from the schema.
  3. Wire both the stamp path and the transform-chain path through the helper.
  4. 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.
  • plugin/entities/task/schema.json — reorder readiness_verified_at to last property; mirror the change in sibling schemas (epic, milestone, backlog) that carry the same field.
  • plugin/skills/entities-migrate/migrate_entities.py — new reorder_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.
  • 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, with readiness_verified_at: last.
  • AC-2: Running /sdlc:entities-migrate a 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.
  • Reordering keys in files that don’t otherwise need migration. The helper only fires when the file is being written for another reason.
  • none

Spawned by /sdlc:task-work post-mortem of T-T5RB-consolidate-task-status-enum on 2026-05-19.

Captured by /sdlc:task-work on 2026-05-21. PR: pending.

  • AC-1: auto — new canonical-key-order eval case in plugin/skills/entities-migrate/tests/run_evals.py asserts via expected_order that frontmatter keys after migrate land in the schema’s declared property order with readiness_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 reported fixed: 0 file(s) with only pre-existing prose drift surfaced for manual review.
  • AC-3: auto — readiness_verified_at is now declared LAST in plugin/entities/task/schema.json’s properties block, so reorder_frontmatter_keys deterministically 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.
  • 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.
  • 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 on last_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 leave last_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-review or /sdlc:task-ensure-ready could 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 add checked 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 from origin/main directly when local main is ahead with start-commit churn. — Step 4 could git fetch origin && git worktree add ... origin/main instead of main when contamination is detectable. → 2026-05-21-task-work-step-4-branches-from-origin-main

← Back to Tasks