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:
5
.changeset/fn-4656-finalize-integrity-gate.md
Normal file
5
.changeset/fn-4656-finalize-integrity-gate.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user