@ijfw/memory-server 1.4.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +1 -1
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +550 -14
- package/src/cross-orchestrator.js +1016 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +554 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +152 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/obsidian-parser.js +91 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +10 -0
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +249 -0
- package/src/orchestrator/review.js +38 -3
- package/src/orchestrator/runtime-loop.js +430 -0
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1764 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +656 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -0
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +603 -59
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +94 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +41 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +41 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +278 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* code-fixer.js — v1.5.0 T27 / G4: cross-AI consensus code-fixer loop.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the ijfw-code-fixer agent (claude/agents/ijfw-code-fixer.md, T24) with
|
|
5
|
+
* the flagship Trident-verified per-finding atomic-commit loop. This is Moat
|
|
6
|
+
* #1 of the v1.5.0 gap-closure brief: review → fix → Trident-verify → atomic
|
|
7
|
+
* commit, with crash-safe sentinel recovery on every destructive boundary.
|
|
8
|
+
*
|
|
9
|
+
* Contract (per finding):
|
|
10
|
+
* 1. Triage — DEFER logic-bugs, ambiguous findings, stale findings.
|
|
11
|
+
* 2. Snapshot — capture pre-edit file content for rollback.
|
|
12
|
+
* 3. Apply — minimal Edit (`old_string` → `new_string` in target file).
|
|
13
|
+
* 4. Tier-1 — re-read; confirm change landed character-for-character.
|
|
14
|
+
* 5. Tier-2 — per-language syntax check (node/python/bash/json/tsc).
|
|
15
|
+
* 6. Tier-3 — optional fallback (project verify_cmd / package.json test).
|
|
16
|
+
* 7. Trident — run runPhaseEConverge over the edited file; require PASS.
|
|
17
|
+
* 8. Commit — one atomic commit per finding in the isolated worktree.
|
|
18
|
+
* ✗ ANY failure rolls back the edit and emits a structured failure record.
|
|
19
|
+
*
|
|
20
|
+
* Worktree lifecycle is delegated to `lib/worktree-recovery.js`'s sentinel
|
|
21
|
+
* pattern — if the process crashes mid-fix the next run prunes the dangling
|
|
22
|
+
* worktree and reports the survivor.
|
|
23
|
+
*
|
|
24
|
+
* Zero new prod deps. ESM. Node ≥18.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync } from 'node:fs';
|
|
28
|
+
import { readFile, writeFile, mkdtemp, rm } from 'node:fs/promises';
|
|
29
|
+
import { join, extname, relative, isAbsolute, resolve as resolvePath } from 'node:path';
|
|
30
|
+
import { tmpdir } from 'node:os';
|
|
31
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
32
|
+
import { promisify } from 'node:util';
|
|
33
|
+
|
|
34
|
+
import { withRecoverySentinel } from '../lib/worktree-recovery.js';
|
|
35
|
+
|
|
36
|
+
const execFileAsync = promisify(execFile);
|
|
37
|
+
|
|
38
|
+
/* ────────────────────────────── status codes ────────────────────────────── */
|
|
39
|
+
|
|
40
|
+
export const STATUS = Object.freeze({
|
|
41
|
+
VERIFIED: 'VERIFIED',
|
|
42
|
+
DEFERRED: 'DEFERRED',
|
|
43
|
+
STALE: 'STALE',
|
|
44
|
+
VERIFY_FAIL: 'VERIFY_FAIL',
|
|
45
|
+
SYNTAX_FAIL: 'SYNTAX_FAIL',
|
|
46
|
+
FALLBACK_FAIL: 'FALLBACK_FAIL',
|
|
47
|
+
TRIDENT_FAIL: 'TRIDENT_FAIL',
|
|
48
|
+
COMMIT_FAIL: 'COMMIT_FAIL',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/* ────────────────────────────── logic-bug heuristic ─────────────────────── */
|
|
52
|
+
|
|
53
|
+
// Phrases that signal a logic bug a mechanical fixer can't responsibly patch.
|
|
54
|
+
// Lifted from the v1.5.0 G4 brief + the ijfw-code-fixer agent's "DO NOT" list.
|
|
55
|
+
const LOGIC_BUG_PHRASES = [
|
|
56
|
+
'off-by-one',
|
|
57
|
+
'off by one',
|
|
58
|
+
'incorrect condition',
|
|
59
|
+
'wrong condition',
|
|
60
|
+
'wrong order',
|
|
61
|
+
'wrong logic',
|
|
62
|
+
'race condition',
|
|
63
|
+
'business logic',
|
|
64
|
+
'semantic bug',
|
|
65
|
+
'edge case',
|
|
66
|
+
'intent unclear',
|
|
67
|
+
'may not be correct',
|
|
68
|
+
'might be wrong',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* isLogicBug(finding) — return {logic: boolean, reason: string}.
|
|
73
|
+
*
|
|
74
|
+
* Two-layer check:
|
|
75
|
+
* 1. Explicit category tag (`category: logic-bug`) — exact match.
|
|
76
|
+
* 2. Substring scan of `description` against LOGIC_BUG_PHRASES.
|
|
77
|
+
*
|
|
78
|
+
* Conservative-by-design: false positives are cheap (we defer instead of
|
|
79
|
+
* patching), false negatives are expensive (we patch a real logic bug).
|
|
80
|
+
*/
|
|
81
|
+
export function isLogicBug(finding) {
|
|
82
|
+
if (!finding || typeof finding !== 'object') {
|
|
83
|
+
return { logic: false, reason: '' };
|
|
84
|
+
}
|
|
85
|
+
const cat = String(finding.category || '').toLowerCase().trim();
|
|
86
|
+
if (cat === 'logic-bug' || cat === 'logic_bug' || cat === 'logic') {
|
|
87
|
+
return { logic: true, reason: `category=${cat}` };
|
|
88
|
+
}
|
|
89
|
+
const desc = String(finding.description || '').toLowerCase();
|
|
90
|
+
for (const phrase of LOGIC_BUG_PHRASES) {
|
|
91
|
+
if (desc.includes(phrase)) {
|
|
92
|
+
return { logic: true, reason: `phrase="${phrase}"` };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// `missing-await` with no concrete line/range = ambiguous → defer.
|
|
96
|
+
if (cat === 'missing-await' && (!finding.line || !finding.fix)) {
|
|
97
|
+
return { logic: true, reason: 'missing-await without concrete boundary' };
|
|
98
|
+
}
|
|
99
|
+
return { logic: false, reason: '' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ────────────────────────────── triage ──────────────────────────────────── */
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* triage(finding) — pre-flight gate. Returns one of:
|
|
106
|
+
* { proceed: true } — go ahead and patch
|
|
107
|
+
* { proceed: false, status, reason } — short-circuit with DEFERRED|STALE
|
|
108
|
+
*/
|
|
109
|
+
export function triage(finding) {
|
|
110
|
+
if (!finding || typeof finding !== 'object') {
|
|
111
|
+
return { proceed: false, status: STATUS.DEFERRED, reason: 'malformed-finding' };
|
|
112
|
+
}
|
|
113
|
+
if (!finding.file || typeof finding.file !== 'string') {
|
|
114
|
+
return { proceed: false, status: STATUS.DEFERRED, reason: 'no-file-path' };
|
|
115
|
+
}
|
|
116
|
+
// Logic bug → defer (the contract is explicit: humans only).
|
|
117
|
+
const lb = isLogicBug(finding);
|
|
118
|
+
if (lb.logic) {
|
|
119
|
+
return { proceed: false, status: STATUS.DEFERRED, reason: `logic-bug: ${lb.reason}` };
|
|
120
|
+
}
|
|
121
|
+
// Need a concrete edit operation. Either { fix: { old_string, new_string } }
|
|
122
|
+
// or a precise suggested_fix string we can wrap. Without it the fixer is
|
|
123
|
+
// guessing, and guessing is logic-bug territory.
|
|
124
|
+
const fix = finding.fix || finding.suggested_fix;
|
|
125
|
+
if (!fix || (typeof fix === 'object' && (!fix.old_string || fix.new_string === undefined))) {
|
|
126
|
+
return { proceed: false, status: STATUS.DEFERRED, reason: 'no-concrete-fix' };
|
|
127
|
+
}
|
|
128
|
+
return { proceed: true };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ────────────────────────────── tier 1 — re-read ────────────────────────── */
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* verifyTier1(filePath, newString) — confirm the edit actually landed.
|
|
135
|
+
* Returns { ok: boolean, evidence: string }.
|
|
136
|
+
*/
|
|
137
|
+
export async function verifyTier1(filePath, newString) {
|
|
138
|
+
try {
|
|
139
|
+
const content = await readFile(filePath, 'utf8');
|
|
140
|
+
if (newString === '' || content.includes(newString)) {
|
|
141
|
+
return { ok: true, evidence: '' };
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
evidence: `tier-1: expected substring not present in ${filePath}`,
|
|
146
|
+
};
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return { ok: false, evidence: `tier-1: read failed: ${err.message}` };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ────────────────────────────── tier 2 — syntax check ───────────────────── */
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* tier2SyntaxCheckCmd(filePath) — return the per-language check command, or
|
|
156
|
+
* null if the extension isn't on the supported list (caller SKIPs tier 2).
|
|
157
|
+
*
|
|
158
|
+
* Per ijfw-code-fixer.md contract:
|
|
159
|
+
* .js/.mjs/.cjs → node --check
|
|
160
|
+
* .ts/.tsx → tsc --noEmit --allowJs (only if tsc on PATH; else SKIP)
|
|
161
|
+
* .py → python3 -m py_compile
|
|
162
|
+
* .json → node -e JSON.parse
|
|
163
|
+
* .sh/.bash → bash -n
|
|
164
|
+
* others → SKIP
|
|
165
|
+
*/
|
|
166
|
+
export function tier2SyntaxCheckCmd(filePath) {
|
|
167
|
+
const ext = extname(filePath).toLowerCase();
|
|
168
|
+
switch (ext) {
|
|
169
|
+
case '.js':
|
|
170
|
+
case '.mjs':
|
|
171
|
+
case '.cjs':
|
|
172
|
+
return { cmd: 'node', args: ['--check', filePath] };
|
|
173
|
+
case '.json':
|
|
174
|
+
return {
|
|
175
|
+
cmd: 'node',
|
|
176
|
+
args: [
|
|
177
|
+
'-e',
|
|
178
|
+
`JSON.parse(require('fs').readFileSync(${JSON.stringify(filePath)},'utf8'))`,
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
case '.py':
|
|
182
|
+
return { cmd: 'python3', args: ['-m', 'py_compile', filePath] };
|
|
183
|
+
case '.sh':
|
|
184
|
+
case '.bash':
|
|
185
|
+
return { cmd: 'bash', args: ['-n', filePath] };
|
|
186
|
+
case '.ts':
|
|
187
|
+
case '.tsx': {
|
|
188
|
+
// Only if tsc on PATH. The agent contract says SKIP when absent.
|
|
189
|
+
const which = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['tsc'], {
|
|
190
|
+
encoding: 'utf8',
|
|
191
|
+
});
|
|
192
|
+
if (which.status === 0 && which.stdout.trim()) {
|
|
193
|
+
return { cmd: 'tsc', args: ['--noEmit', '--allowJs', filePath] };
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* verifyTier2(filePath) — per-language syntax check. Returns one of:
|
|
204
|
+
* { ok: true, skipped: false }
|
|
205
|
+
* { ok: true, skipped: true } — extension not on the list
|
|
206
|
+
* { ok: false, evidence: string } — syntax error captured
|
|
207
|
+
*/
|
|
208
|
+
export async function verifyTier2(filePath) {
|
|
209
|
+
const spec = tier2SyntaxCheckCmd(filePath);
|
|
210
|
+
if (!spec) return { ok: true, skipped: true };
|
|
211
|
+
try {
|
|
212
|
+
await execFileAsync(spec.cmd, spec.args, { timeout: 15_000 });
|
|
213
|
+
return { ok: true, skipped: false };
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const stderr = err.stderr || err.stdout || err.message || '';
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
evidence: `tier-2 (${spec.cmd}): ${String(stderr).split('\n').slice(0, 5).join('\n')}`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ────────────────────────────── tier 3 — fallback project verify ────────── */
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* resolveProjectVerifyCmd(projectRoot, verifyCmdOverride) — pick the verify
|
|
227
|
+
* command. Priority: explicit override → package.json.scripts.test → Makefile
|
|
228
|
+
* `test:` target → null (caller SKIPs tier 3).
|
|
229
|
+
*/
|
|
230
|
+
async function resolveProjectVerifyCmd(projectRoot, verifyCmdOverride) {
|
|
231
|
+
if (verifyCmdOverride && typeof verifyCmdOverride === 'string' && verifyCmdOverride.trim()) {
|
|
232
|
+
return verifyCmdOverride.trim();
|
|
233
|
+
}
|
|
234
|
+
// package.json.scripts.test
|
|
235
|
+
const pkgPath = join(projectRoot, 'package.json');
|
|
236
|
+
if (existsSync(pkgPath)) {
|
|
237
|
+
try {
|
|
238
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
239
|
+
if (pkg.scripts && typeof pkg.scripts.test === 'string' && pkg.scripts.test.trim()) {
|
|
240
|
+
return 'npm test --silent';
|
|
241
|
+
}
|
|
242
|
+
} catch { /* swallow malformed package.json */ }
|
|
243
|
+
}
|
|
244
|
+
// Makefile test target
|
|
245
|
+
const mkPath = join(projectRoot, 'Makefile');
|
|
246
|
+
if (existsSync(mkPath)) {
|
|
247
|
+
try {
|
|
248
|
+
const mk = await readFile(mkPath, 'utf8');
|
|
249
|
+
if (/^test\s*:/m.test(mk)) return 'make test';
|
|
250
|
+
} catch { /* swallow */ }
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* verifyTier3(projectRoot, verifyCmdOverride) — run the project's documented
|
|
257
|
+
* verify command. Returns one of:
|
|
258
|
+
* { ok: true, skipped: false }
|
|
259
|
+
* { ok: true, skipped: true } — no command discovered
|
|
260
|
+
* { ok: false, evidence: string } — first 20 lines of failure output
|
|
261
|
+
*/
|
|
262
|
+
export async function verifyTier3(projectRoot, verifyCmdOverride) {
|
|
263
|
+
const cmd = await resolveProjectVerifyCmd(projectRoot, verifyCmdOverride);
|
|
264
|
+
if (!cmd) return { ok: true, skipped: true };
|
|
265
|
+
// Run the command via `sh -c` so script lines like `npm test --silent` work
|
|
266
|
+
// verbatim. Timeout is generous (5 min) because real test suites can be slow.
|
|
267
|
+
return new Promise((resolve) => {
|
|
268
|
+
execFile('sh', ['-c', cmd], { cwd: projectRoot, timeout: 5 * 60_000 }, (err, stdout, stderr) => {
|
|
269
|
+
if (!err) return resolve({ ok: true, skipped: false });
|
|
270
|
+
const blob = String(stderr || stdout || err.message || '');
|
|
271
|
+
const evidence = blob.split('\n').slice(0, 20).join('\n');
|
|
272
|
+
resolve({ ok: false, evidence: `tier-3 (${cmd}): ${evidence}` });
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* ────────────────────────────── trident verify ──────────────────────────── */
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* runTridentVerify({ commitRange, dispatch, projectRoot, lenses }) — wrap
|
|
281
|
+
* cross-orchestrator's runPhaseEConverge with the code-fixer's PASS-required
|
|
282
|
+
* contract. The verdict shape from runPhaseEConverge is:
|
|
283
|
+
* { verdict: 'PASS' | 'consensus_failed' | ..., iterations, findings, ... }
|
|
284
|
+
*
|
|
285
|
+
* The fixer only commits on `verdict === 'PASS'`. Anything else rolls back.
|
|
286
|
+
*
|
|
287
|
+
* `dispatch` is injectable (and required) so tests can drive scripted lens
|
|
288
|
+
* responses without spawning the real codex/gemini/claude CLIs.
|
|
289
|
+
*/
|
|
290
|
+
export async function runTridentVerify({
|
|
291
|
+
commitRange,
|
|
292
|
+
dispatch,
|
|
293
|
+
projectRoot,
|
|
294
|
+
lenses,
|
|
295
|
+
maxIterations = 3,
|
|
296
|
+
}) {
|
|
297
|
+
if (typeof dispatch !== 'function') {
|
|
298
|
+
return {
|
|
299
|
+
verdict: 'TRIDENT_DISPATCH_MISSING',
|
|
300
|
+
passed: false,
|
|
301
|
+
evidence: 'no dispatch function supplied',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
// Lazy-import to keep this module loadable in environments where
|
|
305
|
+
// cross-orchestrator's transitive deps (state-sdk, receipts) aren't wired.
|
|
306
|
+
// Test fixtures can always supply a custom `dispatch`, so the orchestrator
|
|
307
|
+
// call works against scripted lens responses with no real CLI spawn.
|
|
308
|
+
const { runPhaseEConverge } = await import('../cross-orchestrator.js');
|
|
309
|
+
const result = await runPhaseEConverge({
|
|
310
|
+
commitRange: commitRange || 'HEAD~1..HEAD',
|
|
311
|
+
dispatch,
|
|
312
|
+
lenses: Array.isArray(lenses) && lenses.length ? lenses : undefined,
|
|
313
|
+
maxIterations,
|
|
314
|
+
projectRoot,
|
|
315
|
+
projectDir: projectRoot,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
verdict: result.verdict,
|
|
319
|
+
passed: result.verdict === 'PASS',
|
|
320
|
+
iterations: result.iterations,
|
|
321
|
+
evidence: result.verdict === 'PASS' ? '' : `trident: verdict=${result.verdict}`,
|
|
322
|
+
raw: result,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* ────────────────────────────── git helpers ─────────────────────────────── */
|
|
327
|
+
|
|
328
|
+
function git(cwd, args, { allowFail = false } = {}) {
|
|
329
|
+
const res = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
330
|
+
if (res.status !== 0 && !allowFail) {
|
|
331
|
+
const err = new Error(`git ${args.join(' ')} failed: ${res.stderr || res.stdout}`);
|
|
332
|
+
err.stdout = res.stdout;
|
|
333
|
+
err.stderr = res.stderr;
|
|
334
|
+
throw err;
|
|
335
|
+
}
|
|
336
|
+
return { status: res.status, stdout: res.stdout, stderr: res.stderr };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* atomicCommit({ projectRoot, file, finding }) — stage the one edited file +
|
|
341
|
+
* commit. Per-finding atomicity is the wrapping-loop guarantee from the agent
|
|
342
|
+
* contract; bundling defeats the rollback story.
|
|
343
|
+
*
|
|
344
|
+
* Returns { ok: boolean, sha?: string, evidence?: string }.
|
|
345
|
+
*/
|
|
346
|
+
export function atomicCommit({ projectRoot, file, finding }) {
|
|
347
|
+
try {
|
|
348
|
+
// Explicit-file stage; never `git add -A` (catches stray edits).
|
|
349
|
+
const rel = isAbsolute(file) ? relative(projectRoot, file) : file;
|
|
350
|
+
git(projectRoot, ['add', '--', rel]);
|
|
351
|
+
const id = finding.finding_id || finding.id || 'unknown';
|
|
352
|
+
const sev = (finding.severity || 'unknown').toUpperCase();
|
|
353
|
+
const desc = String(finding.description || '').split('\n')[0].slice(0, 120);
|
|
354
|
+
const msg = `fix(code-fixer): ${id} [${sev}] ${desc}`.trim();
|
|
355
|
+
git(projectRoot, ['commit', '-m', msg]);
|
|
356
|
+
const sha = git(projectRoot, ['rev-parse', 'HEAD']).stdout.trim();
|
|
357
|
+
return { ok: true, sha };
|
|
358
|
+
} catch (err) {
|
|
359
|
+
return { ok: false, evidence: err.message };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* ────────────────────────────── apply + rollback ────────────────────────── */
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* applyEdit(filePath, fix) — one minimal substitution, captures pre-edit
|
|
367
|
+
* content for rollback. Supports the two fix shapes the contract recognises:
|
|
368
|
+
* { old_string, new_string } — exact substring substitution
|
|
369
|
+
* string — interpreted as the new full content (rare)
|
|
370
|
+
*/
|
|
371
|
+
async function applyEdit(filePath, fix) {
|
|
372
|
+
const before = await readFile(filePath, 'utf8');
|
|
373
|
+
let after;
|
|
374
|
+
if (typeof fix === 'object' && fix.old_string !== undefined) {
|
|
375
|
+
if (!before.includes(fix.old_string)) {
|
|
376
|
+
return { ok: false, evidence: 'old_string not found in file', before };
|
|
377
|
+
}
|
|
378
|
+
// Exactly-one occurrence guarantee — same rule the Edit tool uses.
|
|
379
|
+
const occurrences = before.split(fix.old_string).length - 1;
|
|
380
|
+
if (occurrences > 1 && !fix.replace_all) {
|
|
381
|
+
return { ok: false, evidence: `old_string occurs ${occurrences}×; ambiguous`, before };
|
|
382
|
+
}
|
|
383
|
+
after = fix.replace_all
|
|
384
|
+
? before.split(fix.old_string).join(fix.new_string)
|
|
385
|
+
: before.replace(fix.old_string, fix.new_string);
|
|
386
|
+
} else if (typeof fix === 'string') {
|
|
387
|
+
after = fix;
|
|
388
|
+
} else {
|
|
389
|
+
return { ok: false, evidence: 'unsupported fix shape', before };
|
|
390
|
+
}
|
|
391
|
+
if (after === before) {
|
|
392
|
+
return { ok: false, evidence: 'edit was a no-op (after === before)', before };
|
|
393
|
+
}
|
|
394
|
+
await writeFile(filePath, after, 'utf8');
|
|
395
|
+
return { ok: true, before, after };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function rollback(filePath, originalContent) {
|
|
399
|
+
try {
|
|
400
|
+
await writeFile(filePath, originalContent, 'utf8');
|
|
401
|
+
return { ok: true };
|
|
402
|
+
} catch (err) {
|
|
403
|
+
return { ok: false, error: err.message };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* ────────────────────────────── main entry point ────────────────────────── */
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* fixFinding({ finding, projectRoot, dispatch, verifyCmd, dryRun, lenses,
|
|
411
|
+
* commitRange, maxConvergeIter, skipTrident, skipCommit })
|
|
412
|
+
*
|
|
413
|
+
* The flagship loop. Returns a structured record:
|
|
414
|
+
* {
|
|
415
|
+
* status: STATUS.*,
|
|
416
|
+
* tier_reached: 1 | 2 | 3 | 'trident' | 'commit' | 'n/a',
|
|
417
|
+
* finding_id, file, evidence?, sha?, trident?
|
|
418
|
+
* }
|
|
419
|
+
*
|
|
420
|
+
* `dispatch` is required for Trident verify; tests inject scripted responses.
|
|
421
|
+
* `skipTrident` is for unit-test-only flows that exercise the 3-tier matrix
|
|
422
|
+
* in isolation; production callers MUST run Trident.
|
|
423
|
+
*
|
|
424
|
+
* `skipCommit` is for dry-run / unit-test flows; production callers MUST
|
|
425
|
+
* commit (the recovery-sentinel is wrapped around the commit boundary).
|
|
426
|
+
*/
|
|
427
|
+
export async function fixFinding({
|
|
428
|
+
finding,
|
|
429
|
+
projectRoot,
|
|
430
|
+
dispatch,
|
|
431
|
+
verifyCmd,
|
|
432
|
+
dryRun = false,
|
|
433
|
+
lenses,
|
|
434
|
+
commitRange,
|
|
435
|
+
maxConvergeIter = 3,
|
|
436
|
+
skipTrident = false,
|
|
437
|
+
skipCommit = false,
|
|
438
|
+
} = {}) {
|
|
439
|
+
const findingId = finding?.finding_id || finding?.id || 'unknown';
|
|
440
|
+
const base = { finding_id: findingId, file: finding?.file };
|
|
441
|
+
|
|
442
|
+
// 1. triage
|
|
443
|
+
const t = triage(finding);
|
|
444
|
+
if (!t.proceed) {
|
|
445
|
+
return { ...base, status: t.status, tier_reached: 'n/a', deferred_reason: t.reason };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 2. confirm target exists + snapshot
|
|
449
|
+
const filePath = isAbsolute(finding.file)
|
|
450
|
+
? finding.file
|
|
451
|
+
: resolvePath(projectRoot || process.cwd(), finding.file);
|
|
452
|
+
if (!existsSync(filePath)) {
|
|
453
|
+
return { ...base, status: STATUS.STALE, tier_reached: 'n/a',
|
|
454
|
+
evidence: `target file does not exist: ${filePath}` };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (dryRun) {
|
|
458
|
+
return { ...base, status: STATUS.DEFERRED, tier_reached: 'n/a',
|
|
459
|
+
deferred_reason: 'dry-run' };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 3. apply
|
|
463
|
+
const fix = finding.fix || finding.suggested_fix;
|
|
464
|
+
const edit = await applyEdit(filePath, fix);
|
|
465
|
+
if (!edit.ok) {
|
|
466
|
+
return { ...base, status: STATUS.VERIFY_FAIL, tier_reached: 1, evidence: edit.evidence };
|
|
467
|
+
}
|
|
468
|
+
const originalContent = edit.before;
|
|
469
|
+
const expectedNewString = typeof fix === 'object' ? fix.new_string : null;
|
|
470
|
+
|
|
471
|
+
// 4. tier 1 — re-read
|
|
472
|
+
const t1 = await verifyTier1(
|
|
473
|
+
filePath,
|
|
474
|
+
expectedNewString !== null && expectedNewString !== undefined ? expectedNewString : '',
|
|
475
|
+
);
|
|
476
|
+
if (!t1.ok) {
|
|
477
|
+
await rollback(filePath, originalContent);
|
|
478
|
+
return { ...base, status: STATUS.VERIFY_FAIL, tier_reached: 1, evidence: t1.evidence };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 5. tier 2 — syntax check (per-language)
|
|
482
|
+
const t2 = await verifyTier2(filePath);
|
|
483
|
+
if (!t2.ok) {
|
|
484
|
+
await rollback(filePath, originalContent);
|
|
485
|
+
return { ...base, status: STATUS.SYNTAX_FAIL, tier_reached: 2, evidence: t2.evidence };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 6. tier 3 — fallback project verify
|
|
489
|
+
// Only run if tier 2 passed (or skipped). If tier 2 was non-skipped + passed
|
|
490
|
+
// we still run tier 3 as a deeper net.
|
|
491
|
+
const t3 = await verifyTier3(projectRoot || process.cwd(), verifyCmd);
|
|
492
|
+
if (!t3.ok) {
|
|
493
|
+
await rollback(filePath, originalContent);
|
|
494
|
+
return { ...base, status: STATUS.FALLBACK_FAIL, tier_reached: 3, evidence: t3.evidence };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 7. trident verify
|
|
498
|
+
let tridentRec = null;
|
|
499
|
+
if (!skipTrident) {
|
|
500
|
+
const trident = await runTridentVerify({
|
|
501
|
+
commitRange,
|
|
502
|
+
dispatch,
|
|
503
|
+
projectRoot: projectRoot || process.cwd(),
|
|
504
|
+
lenses,
|
|
505
|
+
maxIterations: maxConvergeIter,
|
|
506
|
+
});
|
|
507
|
+
tridentRec = {
|
|
508
|
+
verdict: trident.verdict,
|
|
509
|
+
iterations: trident.iterations,
|
|
510
|
+
passed: trident.passed,
|
|
511
|
+
};
|
|
512
|
+
if (!trident.passed) {
|
|
513
|
+
await rollback(filePath, originalContent);
|
|
514
|
+
return {
|
|
515
|
+
...base,
|
|
516
|
+
status: STATUS.TRIDENT_FAIL,
|
|
517
|
+
tier_reached: 'trident',
|
|
518
|
+
evidence: trident.evidence,
|
|
519
|
+
trident: tridentRec,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 8. atomic commit (sentinel-wrapped — the destructive crash window).
|
|
525
|
+
if (skipCommit) {
|
|
526
|
+
return {
|
|
527
|
+
...base,
|
|
528
|
+
status: STATUS.VERIFIED,
|
|
529
|
+
tier_reached: skipTrident ? 3 : 'trident',
|
|
530
|
+
trident: tridentRec,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
const sentinelData = {
|
|
536
|
+
op: 'code-fixer-commit',
|
|
537
|
+
finding_id: findingId,
|
|
538
|
+
file: filePath,
|
|
539
|
+
worktreePath: projectRoot, // re-used for surface compat with recoverPending
|
|
540
|
+
};
|
|
541
|
+
const commitRes = await withRecoverySentinel(
|
|
542
|
+
sentinelData,
|
|
543
|
+
async () => atomicCommit({ projectRoot, file: filePath, finding }),
|
|
544
|
+
projectRoot || process.cwd(),
|
|
545
|
+
);
|
|
546
|
+
if (!commitRes.ok) {
|
|
547
|
+
await rollback(filePath, originalContent);
|
|
548
|
+
return {
|
|
549
|
+
...base,
|
|
550
|
+
status: STATUS.COMMIT_FAIL,
|
|
551
|
+
tier_reached: 'commit',
|
|
552
|
+
evidence: commitRes.evidence,
|
|
553
|
+
trident: tridentRec,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
...base,
|
|
558
|
+
status: STATUS.VERIFIED,
|
|
559
|
+
tier_reached: skipTrident ? 3 : 'trident',
|
|
560
|
+
sha: commitRes.sha,
|
|
561
|
+
trident: tridentRec,
|
|
562
|
+
};
|
|
563
|
+
} catch (err) {
|
|
564
|
+
// Sentinel remains on disk for next-run recovery (worktree-recovery.js).
|
|
565
|
+
await rollback(filePath, originalContent);
|
|
566
|
+
return {
|
|
567
|
+
...base,
|
|
568
|
+
status: STATUS.COMMIT_FAIL,
|
|
569
|
+
tier_reached: 'commit',
|
|
570
|
+
evidence: `sentinel-wrapped commit threw: ${err.message}`,
|
|
571
|
+
sentinel_path: err.sentinelPath,
|
|
572
|
+
trident: tridentRec,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* ────────────────────────────── batch runner ────────────────────────────── */
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* fixFindings(findings, opts) — sequential per-finding atomic loop.
|
|
581
|
+
*
|
|
582
|
+
* Sequential is intentional: each fix mutates the same worktree HEAD and
|
|
583
|
+
* the Trident step needs a stable commit range. Parallel fixes would shred
|
|
584
|
+
* the atomicity guarantee.
|
|
585
|
+
*
|
|
586
|
+
* Returns { results: Array<fixFinding-record>, summary: { verified, deferred,
|
|
587
|
+
* stale, verify_fail, syntax_fail, fallback_fail, trident_fail,
|
|
588
|
+
* commit_fail } }.
|
|
589
|
+
*/
|
|
590
|
+
export async function fixFindings(findings, opts = {}) {
|
|
591
|
+
const results = [];
|
|
592
|
+
const summary = {
|
|
593
|
+
verified: 0, deferred: 0, stale: 0,
|
|
594
|
+
verify_fail: 0, syntax_fail: 0, fallback_fail: 0,
|
|
595
|
+
trident_fail: 0, commit_fail: 0,
|
|
596
|
+
};
|
|
597
|
+
for (const finding of (findings || [])) {
|
|
598
|
+
// eslint-disable-next-line no-await-in-loop -- sequential is the contract
|
|
599
|
+
const r = await fixFinding({ ...opts, finding });
|
|
600
|
+
results.push(r);
|
|
601
|
+
const k = String(r.status || '').toLowerCase();
|
|
602
|
+
if (k in summary) summary[k] += 1;
|
|
603
|
+
}
|
|
604
|
+
return { results, summary };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* ────────────────────────────── test helpers ────────────────────────────── */
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* _makeTridentDispatch — convenience builder for unit + e2e tests.
|
|
611
|
+
*
|
|
612
|
+
* Mode 'pass' → every lens returns PASS, empty findings, first iter.
|
|
613
|
+
* Mode 'fail-then-pass' → first iter FAIL on one lens, second PASS.
|
|
614
|
+
* Mode 'fail' → every iter has codex FAIL; convergence stalls.
|
|
615
|
+
*
|
|
616
|
+
* Production callers MUST inject a real lens dispatcher. This is exported
|
|
617
|
+
* (underscore-prefixed) only for the test harness.
|
|
618
|
+
*/
|
|
619
|
+
export function _makeTridentDispatch(mode = 'pass') {
|
|
620
|
+
const scripts = {
|
|
621
|
+
pass: () => ({ verdict: 'PASS', findings: [] }),
|
|
622
|
+
};
|
|
623
|
+
if (mode === 'pass') {
|
|
624
|
+
return async ({ lens }) => ({ lens, ...scripts.pass() });
|
|
625
|
+
}
|
|
626
|
+
if (mode === 'fail-then-pass') {
|
|
627
|
+
return async ({ lens, iteration }) => {
|
|
628
|
+
if (lens === 'codex' && iteration === 1) {
|
|
629
|
+
return { lens, verdict: 'FAIL', findings: [{ severity: 'high', text: 'demo' }] };
|
|
630
|
+
}
|
|
631
|
+
return { lens, verdict: 'PASS', findings: [] };
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
if (mode === 'fail') {
|
|
635
|
+
let counter = 0;
|
|
636
|
+
return async ({ lens }) => {
|
|
637
|
+
counter += 1;
|
|
638
|
+
if (lens === 'codex') {
|
|
639
|
+
return { lens, verdict: 'FAIL', findings: [{ severity: 'high', text: `t-${counter}` }] };
|
|
640
|
+
}
|
|
641
|
+
return { lens, verdict: 'PASS', findings: [{ text: `c-${counter}` }] };
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
throw new Error(`_makeTridentDispatch: unknown mode "${mode}"`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* _freshTmpRoot(prefix) — mkdtemp helper for tests; returns absolute path.
|
|
649
|
+
*/
|
|
650
|
+
export async function _freshTmpRoot(prefix = 'ijfw-code-fixer-test-') {
|
|
651
|
+
return mkdtemp(join(tmpdir(), prefix));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function _cleanupTmpRoot(root) {
|
|
655
|
+
try { await rm(root, { recursive: true, force: true }); } catch { /* swallow */ }
|
|
656
|
+
}
|