docs(FN-4656): complete Step 6 — docs, changeset, and integrity sweep notes

Fusion-Task-Id: FN-4656
Fusion-Task-Lineage: 81e8bd91-bc02-4bb7-a590-0e6e84e3535f
This commit is contained in:
Fusion
2026-05-15 14:05:36 -07:00
committed by gsxdsm
parent 4ea1fa3e23
commit bd3809ed81
4 changed files with 41 additions and 36 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Finalize-to-done now requires ownership evidence: tasks only complete when a task-owned landed commit is proven or when no-op completion is proven against the merge target. Legitimate no-op finalize paths now reconcile stale metadata by clearing inherited `modifiedFiles` and stamping empty `landedFiles` markers. Unproven finalize cases are audit-logged and auto-retried by requeuing to `todo` for fresh execution instead of silently landing as done.

View File

@@ -1435,6 +1435,13 @@ When a tracked task transitions into `done`, Fusion closes the linked GitHub iss
- FN-4232/FN-4605 extends that bootstrap to cover stale dist consumers comprehensively: `@fusion/{core,dashboard,engine,plugin-sdk}` and `@fusion-plugin-examples/{dependency-graph,hermes-runtime,openclaw-runtime,paperclip-runtime}` are checked for missing/stale artifacts (staleness compares newest `src/` mtime against the oldest required `dist/` artifact mtime for configured packages). Package-level `pretest` hooks in `@fusion/dashboard` and `@fusion-plugin-examples/dependency-graph` invoke the same bootstrap for filtered test runs.
- When stale or missing artifacts are found, the preamble logs `[test-bootstrap] rebuilding workspace dist artifacts (missing or stale): ...`; if rebuild fails, remediation now prints exact artifact-path diagnostics (`[test-bootstrap] missing: ...` / `[test-bootstrap] stale (src newer than dist): ...`) plus the FN-4232/FN-4605 reference and recovery commands.
#### Finalize integrity gate
- Finalize-to-done now runs an ownership classifier with three outcomes: `owned-commit` (task trailer/subject commit proven landed on merge target), `proven-no-op` (zero-ahead branch plus start point reachable from target), and `unproven` (missing ownership evidence, including foreign start-point inheritance).
- `owned-commit` and `proven-no-op` can finalize. `proven-no-op` explicitly reconciles metadata by clearing stale `task.modifiedFiles` and stamping `mergeDetails.noOpMerge=true` with `landedFiles: []`.
- `unproven` no longer silently completes as done; merger/self-healing emit `task:finalize-unproven-blocked` audit events and auto-retry by requeuing to `todo` for a fresh execution pass.
- Historical cleanup is additive: `reconcileDoneTaskIntegrity()` scans done tasks missing `mergeDetails.commitSha` but still carrying `modifiedFiles`, then either recovers owned commit metadata, clears no-op stale files, or emits `task:integrity-warning` without regressing done tasks back to review.
- This integrity gate complements FN-4646 landed-file capture (metadata truth source) and FN-4647 dashboard labeling (UI presentation); gate enforcement is in merger/self-healing, while display semantics remain UI-owned.
#### Autostash lifecycle
- Before destructive merge prep, `stashUnrelatedRootDirChanges()` snapshots dirty root-dir edits into `fusion-merger-autostash:<taskId>:<ts>` (plus optional `race-rescue-*` stashes for late writes).
- During verification-fix finalize fallback, `commitOrAmendMergeWithFixes()` now snapshots any still-dirty root-dir state into `fusion-merger-autostash:<taskId>:finalize-reset:<ts>` *before* its hard reset/clean recovery path, preventing silent mixed-worktree leftovers from being discarded.

View File

@@ -125,7 +125,7 @@ describe("foreign start-point no-owned-commit interactions (real git)", () => {
baseCommitSha: foreignBaseSha,
modifiedFiles: ["foreign.txt"],
paused: false,
status: null,
status: undefined,
worktree: `${dir}/.worktrees/fn-b2`,
mergeDetails: undefined,
dependencies: [],

View File

@@ -2093,6 +2093,16 @@ export class SelfHealingManager {
}
}
private async recordIntegrityAudit(taskId: string, mutationType: "task:finalize-unproven-blocked" | "task:integrity-reconcile-modified-files" | "task:integrity-warning", metadata: Record<string, unknown>): Promise<void> {
const auditor = createRunAuditor(this.store, {
runId: generateSyntheticRunId("self-healing-integrity", taskId),
agentId: "self-healing",
taskId,
phase: "self-healing",
});
await auditor.database({ type: mutationType, target: taskId, metadata });
}
async finalizeNoOpReviewTasks(): Promise<number> {
try {
const settings = await this.store.getSettings();
@@ -2133,14 +2143,12 @@ export class SelfHealingManager {
JSON.stringify(classification.details, null, 2),
);
}
await (this.store as any).recordRunAuditEvent?.({
taskId: task.id,
domain: "database",
mutationType: "task:finalize-unproven-blocked",
target: task.id,
metadata: { reason: classification.reason, details: classification.details, autoRetry: true },
await this.recordIntegrityAudit(task.id, "task:finalize-unproven-blocked", {
reason: classification.reason,
details: classification.details,
autoRetry: true,
});
await this.store.moveTask(task.id, "todo", { preserveProgress: true, moveSource: "engine" } as any);
await this.store.moveTask(task.id, "todo", { preserveProgress: true, moveSource: "engine" });
continue;
}
@@ -2171,12 +2179,9 @@ export class SelfHealingManager {
mergeTargetBranch: classification.baseRef,
};
await this.store.updateTask(task.id, { mergeDetails, modifiedFiles: [] });
await (this.store as any).recordRunAuditEvent?.({
taskId: task.id,
domain: "database",
mutationType: "task:integrity-reconcile-modified-files",
target: task.id,
metadata: { reason: "proven-no-op-finalize", clearedCount: task.modifiedFiles?.length ?? 0 },
await this.recordIntegrityAudit(task.id, "task:integrity-reconcile-modified-files", {
reason: "proven-no-op-finalize",
clearedCount: task.modifiedFiles?.length ?? 0,
});
await this.store.logEntry(task.id, `Auto-finalized no-op (proven): start point on ${classification.baseRef}; modifiedFiles cleared`);
}
@@ -2225,12 +2230,9 @@ export class SelfHealingManager {
mergeCommitMessage: classification.commit.subject,
},
});
await (this.store as any).recordRunAuditEvent?.({
taskId: task.id,
domain: "database",
mutationType: "task:integrity-reconcile-modified-files",
target: task.id,
metadata: { reason: "recovered-owned-commit", commitSha: classification.commit.sha },
await this.recordIntegrityAudit(task.id, "task:integrity-reconcile-modified-files", {
reason: "recovered-owned-commit",
commitSha: classification.commit.sha,
});
reconciled++;
continue;
@@ -2247,12 +2249,9 @@ export class SelfHealingManager {
landedFiles: [],
},
});
await (this.store as any).recordRunAuditEvent?.({
taskId: task.id,
domain: "database",
mutationType: "task:integrity-reconcile-modified-files",
target: task.id,
metadata: { reason: "proven-no-op", clearedCount: task.modifiedFiles?.length ?? 0 },
await this.recordIntegrityAudit(task.id, "task:integrity-reconcile-modified-files", {
reason: "proven-no-op",
clearedCount: task.modifiedFiles?.length ?? 0,
});
reconciled++;
continue;
@@ -2266,16 +2265,10 @@ export class SelfHealingManager {
JSON.stringify(classification.details, null, 2),
);
}
await (this.store as any).recordRunAuditEvent?.({
taskId: task.id,
domain: "database",
mutationType: "task:integrity-warning",
target: task.id,
metadata: {
reason: classification.reason,
modifiedFilesCount: task.modifiedFiles?.length ?? 0,
details: classification.details,
},
await this.recordIntegrityAudit(task.id, "task:integrity-warning", {
reason: classification.reason,
modifiedFilesCount: task.modifiedFiles?.length ?? 0,
details: classification.details,
});
}