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:
@@ -137,6 +137,8 @@ Fusion keeps a canonical per-feature assertion invariant in `MissionStore`:
|
|||||||
- This applies to all creation paths (interview import, API, CLI, tools).
|
- This applies to all creation paths (interview import, API, CLI, tools).
|
||||||
|
|
||||||
Assertion text source priority is: `acceptanceCriteria` → `feature.description` → fallback text (`"Verify implementation of: {feature.title}"`).
|
Assertion text source priority is: `acceptanceCriteria` → `feature.description` → fallback text (`"Verify implementation of: {feature.title}"`).
|
||||||
|
|
||||||
|
**Operator repair note (FN-5696):** Some databases created before the feature-create-path fix could show feature `acceptanceCriteria`/`description` in the UI but still have zero `mission_feature_assertions` links, which caused validator auto-pass short-circuits. Run `node scripts/backfill-fn-5696-feature-assertions.mjs` to preview repairs, then `node scripts/backfill-fn-5696-feature-assertions.mjs --apply` to write links. Use `--mission=<missionId>` for scoped repair (for example the confirmed Goals mission `M-MP32KU9Y-0001-2ADN`).
|
||||||
- **Verification fields**: Milestone and slice verification criteria from the interview are stored in dedicated `verification` fields rather than concatenated into descriptions
|
- **Verification fields**: Milestone and slice verification criteria from the interview are stored in dedicated `verification` fields rather than concatenated into descriptions
|
||||||
- **Milestone acceptanceCriteria derivation**: explicit `milestone.acceptanceCriteria` from interview output is authoritative. When omitted/blank, Fusion derives a deterministic bulleted summary from child features after creation: prefer `feature.acceptanceCriteria`, fall back to `feature.description`, skip empty contributors, and leave milestone acceptance empty when nothing contributes
|
- **Milestone acceptanceCriteria derivation**: explicit `milestone.acceptanceCriteria` from interview output is authoritative. When omitted/blank, Fusion derives a deterministic bulleted summary from child features after creation: prefer `feature.acceptanceCriteria`, fall back to `feature.description`, skip empty contributors, and leave milestone acceptance empty when nothing contributes
|
||||||
- **Partial plans handled**: Auto-generation is robust to partial plans (missing slices/features or empty criteria) without throwing errors
|
- **Partial plans handled**: Auto-generation is robust to partial plans (missing slices/features or empty criteria) without throwing errors
|
||||||
|
|||||||
@@ -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 ───────────────────────────
|
// ── Loop State & Validator Run Schema Tests ───────────────────────────
|
||||||
|
|
||||||
describe("Loop State & Validator Run Schema (v31)", () => {
|
describe("Loop State & Validator Run Schema (v31)", () => {
|
||||||
|
|||||||
@@ -105,6 +105,27 @@ export interface MissionSummary {
|
|||||||
progressPercent: number;
|
progressPercent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MissionAssertionTextSource = "acceptanceCriteria" | "description" | "fallback";
|
||||||
|
|
||||||
|
export interface MissionAssertionBackfillRepairRow {
|
||||||
|
featureId: string;
|
||||||
|
milestoneId: string;
|
||||||
|
assertionId: string;
|
||||||
|
textSource: MissionAssertionTextSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MissionAssertionBackfillErrorRow {
|
||||||
|
featureId: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MissionAssertionBackfillReport {
|
||||||
|
scanned: number;
|
||||||
|
alreadyLinked: number;
|
||||||
|
repaired: MissionAssertionBackfillRepairRow[];
|
||||||
|
skippedErrors: MissionAssertionBackfillErrorRow[];
|
||||||
|
}
|
||||||
|
|
||||||
// ── Event Types ─────────────────────────────────────────────────────
|
// ── Event Types ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface MissionStoreEvents {
|
export interface MissionStoreEvents {
|
||||||
@@ -1865,6 +1886,23 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
|||||||
this.recomputeSliceStatus(sliceId);
|
this.recomputeSliceStatus(sliceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private deriveFeatureAssertion(feature: MissionFeature): { assertionText: string; textSource: MissionAssertionTextSource } {
|
||||||
|
const acceptanceCriteria = feature.acceptanceCriteria?.trim();
|
||||||
|
if (acceptanceCriteria) {
|
||||||
|
return { assertionText: acceptanceCriteria, textSource: "acceptanceCriteria" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = feature.description?.trim();
|
||||||
|
if (description) {
|
||||||
|
return { assertionText: description, textSource: "description" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
assertionText: `Verify implementation of: ${feature.title}`,
|
||||||
|
textSource: "fallback",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private ensureFeatureAssertion(feature: MissionFeature): void {
|
private ensureFeatureAssertion(feature: MissionFeature): void {
|
||||||
const slice = this.getSlice(feature.sliceId);
|
const slice = this.getSlice(feature.sliceId);
|
||||||
if (!slice) {
|
if (!slice) {
|
||||||
@@ -1872,9 +1910,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const milestoneId = slice.milestoneId;
|
const milestoneId = slice.milestoneId;
|
||||||
const assertionText = feature.acceptanceCriteria?.trim()
|
const { assertionText } = this.deriveFeatureAssertion(feature);
|
||||||
|| feature.description?.trim()
|
|
||||||
|| `Verify implementation of: ${feature.title}`;
|
|
||||||
|
|
||||||
const existing = this.listContractAssertions(milestoneId)
|
const existing = this.listContractAssertions(milestoneId)
|
||||||
.find((assertion) => assertion.sourceFeatureId === feature.id);
|
.find((assertion) => assertion.sourceFeatureId === feature.id);
|
||||||
@@ -1898,6 +1934,80 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill assertion links for legacy features that predate the FN-5695 creation-path fix.
|
||||||
|
* Reuses deriveFeatureAssertion()/ensureFeatureAssertion text-source rules so create/update
|
||||||
|
* and repair flows stay aligned on canonical assertion content.
|
||||||
|
*/
|
||||||
|
backfillFeatureAssertions(options?: { missionId?: string; dryRun?: boolean }): MissionAssertionBackfillReport {
|
||||||
|
const dryRun = options?.dryRun ?? true;
|
||||||
|
const missionFilter = options?.missionId;
|
||||||
|
|
||||||
|
const features = missionFilter
|
||||||
|
? this.listMilestones(missionFilter)
|
||||||
|
.flatMap((milestone) => this.listSlices(milestone.id))
|
||||||
|
.flatMap((slice) => this.listFeatures(slice.id))
|
||||||
|
: this.listMissions()
|
||||||
|
.flatMap((mission) => this.listMilestones(mission.id))
|
||||||
|
.flatMap((milestone) => this.listSlices(milestone.id))
|
||||||
|
.flatMap((slice) => this.listFeatures(slice.id));
|
||||||
|
|
||||||
|
const report: MissionAssertionBackfillReport = {
|
||||||
|
scanned: features.length,
|
||||||
|
alreadyLinked: 0,
|
||||||
|
repaired: [],
|
||||||
|
skippedErrors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const feature of features) {
|
||||||
|
try {
|
||||||
|
const linkedAssertions = this.listAssertionsForFeature(feature.id);
|
||||||
|
if (linkedAssertions.length > 0) {
|
||||||
|
report.alreadyLinked += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slice = this.getSlice(feature.sliceId);
|
||||||
|
if (!slice) {
|
||||||
|
throw new Error(`Slice ${feature.sliceId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const milestoneId = slice.milestoneId;
|
||||||
|
const { assertionText, textSource } = this.deriveFeatureAssertion(feature);
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
report.repaired.push({
|
||||||
|
featureId: feature.id,
|
||||||
|
milestoneId,
|
||||||
|
assertionId: "(dry-run)",
|
||||||
|
textSource,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = this.addContractAssertion(milestoneId, {
|
||||||
|
title: feature.title,
|
||||||
|
assertion: assertionText,
|
||||||
|
status: "pending",
|
||||||
|
sourceFeatureId: feature.id,
|
||||||
|
});
|
||||||
|
this.linkFeatureToAssertion(feature.id, created.id);
|
||||||
|
|
||||||
|
report.repaired.push({
|
||||||
|
featureId: feature.id,
|
||||||
|
milestoneId,
|
||||||
|
assertionId: created.id,
|
||||||
|
textSource,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
report.skippedErrors.push({ featureId: feature.id, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the mission hierarchy for a slice.
|
* Resolve the mission hierarchy for a slice.
|
||||||
*
|
*
|
||||||
|
|||||||
174
scripts/backfill-fn-5696-feature-assertions.mjs
Normal file
174
scripts/backfill-fn-5696-feature-assertions.mjs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { DatabaseSync } from "node:sqlite";
|
||||||
|
|
||||||
|
function parseArgs(argv = process.argv.slice(2)) {
|
||||||
|
const apply = argv.includes("--apply");
|
||||||
|
const missionArg = argv.find((arg) => arg.startsWith("--mission="));
|
||||||
|
return {
|
||||||
|
dryRun: !apply,
|
||||||
|
apply,
|
||||||
|
missionId: missionArg ? missionArg.slice("--mission=".length).trim() || undefined : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProjectRoot() {
|
||||||
|
const commonDir = execSync("git rev-parse --git-common-dir", { encoding: "utf8" }).trim();
|
||||||
|
return path.resolve(commonDir, "..");
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFeatures(db, missionId) {
|
||||||
|
const sql = `
|
||||||
|
SELECT f.id, f.title, f.description, f.acceptanceCriteria, s.milestoneId
|
||||||
|
FROM mission_features f
|
||||||
|
INNER JOIN slices s ON s.id = f.sliceId
|
||||||
|
INNER JOIN milestones m ON m.id = s.milestoneId
|
||||||
|
${missionId ? "WHERE m.missionId = ?" : ""}
|
||||||
|
ORDER BY f.createdAt ASC
|
||||||
|
`;
|
||||||
|
return missionId ? db.prepare(sql).all(missionId) : db.prepare(sql).all();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveAssertionText(feature) {
|
||||||
|
const acceptanceCriteria = typeof feature.acceptanceCriteria === "string" ? feature.acceptanceCriteria.trim() : "";
|
||||||
|
if (acceptanceCriteria.length > 0) {
|
||||||
|
return { text: acceptanceCriteria, textSource: "acceptanceCriteria" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = typeof feature.description === "string" ? feature.description.trim() : "";
|
||||||
|
if (description.length > 0) {
|
||||||
|
return { text: description, textSource: "description" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: `Verify implementation of: ${feature.title}`,
|
||||||
|
textSource: "fallback",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAssertionId(db) {
|
||||||
|
const row = db.prepare("SELECT COALESCE(MAX(CAST(SUBSTR(id, 3) AS INTEGER)), 0) AS maxId FROM mission_contract_assertions").get();
|
||||||
|
const next = Number(row?.maxId ?? 0) + 1;
|
||||||
|
return `CA${String(next).padStart(4, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSourceFeatureIdColumn(db) {
|
||||||
|
const columns = db.prepare("PRAGMA table_info('mission_contract_assertions')").all();
|
||||||
|
return columns.some((column) => column?.name === "sourceFeatureId");
|
||||||
|
}
|
||||||
|
|
||||||
|
function backfillFeatureAssertions({ db, dryRun = true, missionId }) {
|
||||||
|
const features = loadFeatures(db, missionId);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const report = {
|
||||||
|
scanned: features.length,
|
||||||
|
alreadyLinked: 0,
|
||||||
|
repaired: [],
|
||||||
|
skippedErrors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const listLinks = db.prepare("SELECT assertionId FROM mission_feature_assertions WHERE featureId = ?");
|
||||||
|
const includeSourceFeatureId = hasSourceFeatureIdColumn(db);
|
||||||
|
const insertAssertion = includeSourceFeatureId
|
||||||
|
? db.prepare(`
|
||||||
|
INSERT INTO mission_contract_assertions
|
||||||
|
(id, milestoneId, title, assertion, status, orderIndex, sourceFeatureId, createdAt, updatedAt)
|
||||||
|
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
|
||||||
|
`)
|
||||||
|
: db.prepare(`
|
||||||
|
INSERT INTO mission_contract_assertions
|
||||||
|
(id, milestoneId, title, assertion, status, orderIndex, createdAt, updatedAt)
|
||||||
|
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)
|
||||||
|
`);
|
||||||
|
const nextOrder = db.prepare("SELECT COALESCE(MAX(orderIndex), -1) + 1 AS nextOrder FROM mission_contract_assertions WHERE milestoneId = ?");
|
||||||
|
const insertLink = db.prepare(
|
||||||
|
"INSERT OR IGNORE INTO mission_feature_assertions (featureId, assertionId, createdAt) VALUES (?, ?, ?)"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const feature of features) {
|
||||||
|
try {
|
||||||
|
const links = listLinks.all(feature.id);
|
||||||
|
if (links.length > 0) {
|
||||||
|
report.alreadyLinked += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { text, textSource } = deriveAssertionText(feature);
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
report.repaired.push({
|
||||||
|
featureId: feature.id,
|
||||||
|
milestoneId: feature.milestoneId,
|
||||||
|
assertionId: "(dry-run)",
|
||||||
|
textSource,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertionId = createAssertionId(db);
|
||||||
|
const orderIndex = Number(nextOrder.get(feature.milestoneId)?.nextOrder ?? 0);
|
||||||
|
if (includeSourceFeatureId) {
|
||||||
|
insertAssertion.run(assertionId, feature.milestoneId, feature.title, text, orderIndex, feature.id, now, now);
|
||||||
|
} else {
|
||||||
|
insertAssertion.run(assertionId, feature.milestoneId, feature.title, text, orderIndex, now, now);
|
||||||
|
}
|
||||||
|
insertLink.run(feature.id, assertionId, now);
|
||||||
|
report.repaired.push({
|
||||||
|
featureId: feature.id,
|
||||||
|
milestoneId: feature.milestoneId,
|
||||||
|
assertionId,
|
||||||
|
textSource,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
report.skippedErrors.push({
|
||||||
|
featureId: feature.id,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printReport(report, { dryRun, missionId }) {
|
||||||
|
console.log(dryRun ? "Mode: DRY RUN" : "Mode: APPLY");
|
||||||
|
console.log(`Mission scope: ${missionId ?? "all"}`);
|
||||||
|
console.log(`Scanned: ${report.scanned}`);
|
||||||
|
console.log(`Already linked: ${report.alreadyLinked}`);
|
||||||
|
console.log(`Repaired: ${report.repaired.length}`);
|
||||||
|
for (const row of report.repaired) {
|
||||||
|
console.log(` - ${row.featureId} -> ${row.assertionId} (${row.textSource})`);
|
||||||
|
}
|
||||||
|
console.log(`Errors: ${report.skippedErrors.length}`);
|
||||||
|
for (const row of report.skippedErrors) {
|
||||||
|
console.log(` - ${row.featureId}: ${row.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main(argv = process.argv.slice(2)) {
|
||||||
|
const { dryRun, missionId } = parseArgs(argv);
|
||||||
|
const projectRoot = resolveProjectRoot();
|
||||||
|
const dbPath = path.join(projectRoot, ".fusion", "fusion.db");
|
||||||
|
const db = new DatabaseSync(dbPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const report = backfillFeatureAssertions({ db, dryRun, missionId });
|
||||||
|
printReport(report, { dryRun, missionId });
|
||||||
|
if (report.skippedErrors.length > 0) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
try {
|
||||||
|
main();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user