FN-5696: backfill missing mission feature assertion links

Backfill and repair missing mission feature assertion links so validator coverage stays intact for legacy mission data.

- add MissionStore backfillFeatureAssertions with dry-run and mission-scoped repair support
- centralize feature assertion text derivation and reuse it for create/update/backfill flows
- add regression tests for repair behavior, idempotency, and dry-run semantics
- document the operator repair workflow and add a dedicated backfill script for FN-5696

Files changed:
 docs/missions.md                                  |   2 +
 packages/core/src/__tests__/mission-store.test.ts |  84 +++++++++++
 packages/core/src/mission-store.ts                | 116 ++++++++++++++-
 scripts/backfill-fn-5696-feature-assertions.mjs   | 174 ++++++++++++++++++++++
 4 files changed, 373 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-5696

Fusion-Task-Lineage: 483bf2cc-be03-4007-860b-66d841e6aa15
This commit is contained in:
gsxdsm
2026-05-29 13:58:13 -07:00
parent bac12d1e28
commit dae4c0ea5d
4 changed files with 373 additions and 3 deletions

View File

@@ -2948,6 +2948,90 @@ describe("MissionStore", () => {
});
});
describe("backfillFeatureAssertions", () => {
const makeLegacyFeature = (sliceId: string, input: { title: string; description?: string; acceptanceCriteria?: string }) => {
const feature = store.addFeature(sliceId, input);
const managed = store.listAssertionsForFeature(feature.id);
for (const assertion of managed) {
store.unlinkFeatureFromAssertion(feature.id, assertion.id);
store.deleteContractAssertion(assertion.id);
}
expect(store.listAssertionsForFeature(feature.id)).toHaveLength(0);
return feature;
};
it("repairs missing links using acceptance criteria, description, and fallback text", () => {
const mission = store.createMission({ title: "Repair Mission" });
const milestone = store.addMilestone(mission.id, { title: "MS" });
const slice = store.addSlice(milestone.id, { title: "SL" });
const fromAcceptance = makeLegacyFeature(slice.id, { title: "F-AC", acceptanceCriteria: "Ship AC" });
const fromDescription = makeLegacyFeature(slice.id, { title: "F-DESC", description: "Ship DESC" });
const fromFallback = makeLegacyFeature(slice.id, { title: "F-FALLBACK" });
const report = store.backfillFeatureAssertions({ dryRun: false });
expect(report.scanned).toBe(3);
expect(report.alreadyLinked).toBe(0);
expect(report.skippedErrors).toHaveLength(0);
expect(report.repaired).toHaveLength(3);
const acRow = report.repaired.find((row) => row.featureId === fromAcceptance.id)!;
const descRow = report.repaired.find((row) => row.featureId === fromDescription.id)!;
const fallbackRow = report.repaired.find((row) => row.featureId === fromFallback.id)!;
expect(acRow.milestoneId).toBe(milestone.id);
expect(acRow.textSource).toBe("acceptanceCriteria");
expect(store.listAssertionsForFeature(fromAcceptance.id)[0].assertion).toBe("Ship AC");
expect(descRow.textSource).toBe("description");
expect(store.listAssertionsForFeature(fromDescription.id)[0].assertion).toBe("Ship DESC");
expect(fallbackRow.textSource).toBe("fallback");
expect(store.listAssertionsForFeature(fromFallback.id)[0].assertion).toBe("Verify implementation of: F-FALLBACK");
});
it("skips already linked features and remains idempotent", () => {
const mission = store.createMission({ title: "Repair Mission" });
const milestone = store.addMilestone(mission.id, { title: "MS" });
const slice = store.addSlice(milestone.id, { title: "SL" });
const legacy = makeLegacyFeature(slice.id, { title: "Legacy", acceptanceCriteria: "Legacy AC" });
const alreadyLinked = store.addFeature(slice.id, { title: "Already Linked", acceptanceCriteria: "Keep" });
const firstRun = store.backfillFeatureAssertions({ dryRun: false });
expect(firstRun.scanned).toBe(2);
expect(firstRun.alreadyLinked).toBe(1);
expect(firstRun.repaired).toHaveLength(1);
expect(firstRun.repaired[0]?.featureId).toBe(legacy.id);
const linkedAssertionIds = store.listAssertionsForFeature(alreadyLinked.id).map((assertion) => assertion.id);
expect(linkedAssertionIds).toHaveLength(1);
const secondRun = store.backfillFeatureAssertions({ dryRun: false });
expect(secondRun.scanned).toBe(2);
expect(secondRun.alreadyLinked).toBe(2);
expect(secondRun.repaired).toHaveLength(0);
});
it("supports dry-run mode without writing links", () => {
const mission = store.createMission({ title: "Repair Mission" });
const milestone = store.addMilestone(mission.id, { title: "MS" });
const slice = store.addSlice(milestone.id, { title: "SL" });
const legacy = makeLegacyFeature(slice.id, { title: "Legacy", description: "legacy description" });
const beforeLinks = db.prepare("SELECT COUNT(*) as count FROM mission_feature_assertions").get() as { count: number };
const report = store.backfillFeatureAssertions({ dryRun: true });
expect(report.repaired).toHaveLength(1);
expect(report.repaired[0]?.featureId).toBe(legacy.id);
expect(report.repaired[0]?.assertionId).toBe("(dry-run)");
expect(report.repaired[0]?.textSource).toBe("description");
const afterLinks = db.prepare("SELECT COUNT(*) as count FROM mission_feature_assertions").get() as { count: number };
expect(afterLinks.count).toBe(beforeLinks.count);
expect(store.listAssertionsForFeature(legacy.id)).toHaveLength(0);
});
});
// ── Loop State & Validator Run Schema Tests ───────────────────────────
describe("Loop State & Validator Run Schema (v31)", () => {