@ijfw/memory-server 1.5.0 → 1.5.3
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/bin/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- package/package.json +8 -4
- package/src/brain/budget-guard.js +86 -0
- package/src/brain/citation-resolver.js +41 -0
- package/src/brain/context-injection.js +69 -0
- package/src/brain/discovery.js +83 -0
- package/src/brain/dream-pipeline.js +324 -0
- package/src/brain/dump-ingest.js +88 -0
- package/src/brain/entity-collapse.js +28 -0
- package/src/brain/export.js +112 -0
- package/src/brain/extractors/index.js +24 -0
- package/src/brain/extractors/markdown.js +27 -0
- package/src/brain/extractors/pdf.js +31 -0
- package/src/brain/extractors/transcript.js +38 -0
- package/src/brain/first-run-scan.js +61 -0
- package/src/brain/index.js +1 -0
- package/src/brain/layout-sentinel.js +29 -0
- package/src/brain/migrate-facts-internal-once.js +87 -0
- package/src/brain/path-guard.js +103 -0
- package/src/brain/paths.js +26 -0
- package/src/brain/promotion-suggester.js +41 -0
- package/src/brain/stub-detector.js +33 -0
- package/src/brain/tiered-llm.js +83 -0
- package/src/brain/wiki-compiler.js +144 -0
- package/src/brain/wiki-sentinels.js +45 -0
- package/src/brain/wiki-templates.js +94 -0
- package/src/cross-orchestrator-cli.js +336 -150
- package/src/cross-orchestrator.js +52 -3
- package/src/dashboard-server.js +1 -1
- package/src/dispatch/extension.js +1 -1
- package/src/dream/runner.mjs +21 -0
- package/src/extension-registry.js +2 -2
- package/src/handlers/brain-handler.js +319 -0
- package/src/hardware-signer.js +4 -2
- package/src/lib/ui-review-runner.js +48 -7
- package/src/memory/auto-linker.js +121 -2
- package/src/memory/benchmark.js +4 -3
- package/src/memory/layout-migrations/001-visible-layer.js +131 -0
- package/src/memory/layout-migrations/index.js +50 -0
- package/src/memory/migration-runner.js +37 -3
- package/src/memory/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +65 -2
- package/src/memory/reader.js +2 -1
- package/src/memory/search.js +190 -41
- package/src/memory/temporal.js +40 -1
- package/src/orchestrator/agents-md-blackboard.js +114 -1
- package/src/orchestrator/debug-trident-trigger.js +374 -0
- package/src/orchestrator/discipline-selector.js +276 -0
- package/src/orchestrator/merge-block-aware.js +15 -5
- package/src/orchestrator/post-done-runner.js +36 -8
- package/src/orchestrator/state-sdk.js +216 -10
- package/src/orchestrator/subagent-telemetry.js +19 -0
- package/src/orchestrator/wave-state.js +38 -0
- package/src/override-resolver.js +5 -3
- package/src/recovery/code-fixer.js +311 -6
- package/src/runtime-mediator.js +0 -1
- package/src/server.js +486 -132
- package/src/swarm-config.js +30 -22
- package/src/team/domain-templates/business.json +4 -1
- package/src/team/domain-templates/research.json +4 -1
- package/src/team/generator.js +162 -0
- package/src/update-apply.js +1 -1
- package/src/dashboard-charts.js +0 -239
- package/src/orchestrator/runtime-loop.js +0 -430
|
@@ -21,12 +21,22 @@
|
|
|
21
21
|
* pattern — if the process crashes mid-fix the next run prunes the dangling
|
|
22
22
|
* worktree and reports the survivor.
|
|
23
23
|
*
|
|
24
|
+
* WIRE-UP (v1.5.1 C2): the consensus entry point `runConsensusFix` is invoked
|
|
25
|
+
* by `cross-orchestrator.js#runPhaseEConverge` when its caller passes
|
|
26
|
+
* `autoFix: true`. After a non-PASS convergence the orchestrator extracts the
|
|
27
|
+
* HIGH findings that 2+ lenses agreed on (`consensusHighFindings`) and runs
|
|
28
|
+
* the atomic per-finding fix loop over them — the "2+ lenses agree → fixer
|
|
29
|
+
* fires automatically" contract from the T27 brief. This module is therefore
|
|
30
|
+
* a live, reachable production path, not an orphan.
|
|
31
|
+
*
|
|
24
32
|
* Zero new prod deps. ESM. Node ≥18.
|
|
25
33
|
*/
|
|
26
34
|
|
|
27
|
-
import { existsSync } from 'node:fs';
|
|
35
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
28
36
|
import { readFile, writeFile, mkdtemp, rm } from 'node:fs/promises';
|
|
29
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
join, extname, relative, isAbsolute, resolve as resolvePath, dirname,
|
|
39
|
+
} from 'node:path';
|
|
30
40
|
import { tmpdir } from 'node:os';
|
|
31
41
|
import { execFile, spawnSync } from 'node:child_process';
|
|
32
42
|
import { promisify } from 'node:util';
|
|
@@ -35,6 +45,82 @@ import { withRecoverySentinel } from '../lib/worktree-recovery.js';
|
|
|
35
45
|
|
|
36
46
|
const execFileAsync = promisify(execFile);
|
|
37
47
|
|
|
48
|
+
/* ────────────────────── R5-1.10: auto-fix safety boundary ────────────────── */
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default ceiling on the number of distinct files a single `runConsensusFix`
|
|
52
|
+
* call may write. Auto-fix is opt-in, but even when enabled it must not be
|
|
53
|
+
* able to mass-rewrite a repository — past this cap the loop stops and
|
|
54
|
+
* reports the remainder rather than continuing to mutate. Callers can lower
|
|
55
|
+
* (never silently raise past sanity) this via `maxAutoFixFiles`.
|
|
56
|
+
*/
|
|
57
|
+
export const DEFAULT_MAX_AUTOFIX_FILES = 10;
|
|
58
|
+
// Hard ceiling — a caller cannot pass `maxAutoFixFiles` above this.
|
|
59
|
+
const MAX_AUTOFIX_FILES_CEILING = 50;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* isPathContained(filePath, projectRoot) — true iff `filePath` resolves to a
|
|
63
|
+
* location at or under `projectRoot`. Symlinks are resolved on BOTH sides
|
|
64
|
+
* (realpath) so a symlink inside the repo that points outside it is rejected.
|
|
65
|
+
*
|
|
66
|
+
* Mirrors the containment prior art in cross-project-search.js#isUnder /
|
|
67
|
+
* safeResolveProjectPath: realpath the root, realpath the entry (falling back
|
|
68
|
+
* to the un-resolved absolute when the entry doesn't exist yet — e.g. a fix
|
|
69
|
+
* that would create a file — and in that case realpath the parent dir), then
|
|
70
|
+
* a trailing-separator boundary check so `/repo-evil` is NOT inside `/repo`.
|
|
71
|
+
*
|
|
72
|
+
* Returns { ok, reason, canonical? }.
|
|
73
|
+
*/
|
|
74
|
+
export function isPathContained(filePath, projectRoot) {
|
|
75
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
76
|
+
return { ok: false, reason: 'no-file-path' };
|
|
77
|
+
}
|
|
78
|
+
if (!projectRoot || typeof projectRoot !== 'string') {
|
|
79
|
+
return { ok: false, reason: 'no-project-root' };
|
|
80
|
+
}
|
|
81
|
+
// Canonicalise the root. If it doesn't resolve, fall back to absolute.
|
|
82
|
+
let canonRoot;
|
|
83
|
+
try {
|
|
84
|
+
canonRoot = realpathSync(resolvePath(projectRoot));
|
|
85
|
+
} catch {
|
|
86
|
+
canonRoot = resolvePath(projectRoot);
|
|
87
|
+
}
|
|
88
|
+
// Canonicalise the target. The file may not exist yet (a creating fix), so
|
|
89
|
+
// realpath the deepest existing ancestor and re-join the missing tail.
|
|
90
|
+
const absTarget = isAbsolute(filePath)
|
|
91
|
+
? filePath
|
|
92
|
+
: resolvePath(canonRoot, filePath);
|
|
93
|
+
let canonTarget;
|
|
94
|
+
try {
|
|
95
|
+
canonTarget = realpathSync(absTarget);
|
|
96
|
+
} catch {
|
|
97
|
+
let probe = absTarget;
|
|
98
|
+
const tail = [];
|
|
99
|
+
// Walk up until we hit something that exists (or the fs root).
|
|
100
|
+
while (probe && !existsSync(probe) && dirname(probe) !== probe) {
|
|
101
|
+
tail.unshift(probe.slice(dirname(probe).length).replace(/^[\\/]/, ''));
|
|
102
|
+
probe = dirname(probe);
|
|
103
|
+
}
|
|
104
|
+
let canonProbe;
|
|
105
|
+
try { canonProbe = realpathSync(probe); }
|
|
106
|
+
catch { canonProbe = resolvePath(probe); }
|
|
107
|
+
canonTarget = tail.length ? join(canonProbe, ...tail) : canonProbe;
|
|
108
|
+
}
|
|
109
|
+
// Boundary check with trailing separator so siblings can't impersonate.
|
|
110
|
+
const rel = relative(canonRoot, canonTarget);
|
|
111
|
+
const escapes = rel === '..'
|
|
112
|
+
|| rel.startsWith(`..${'/'}`) || rel.startsWith(`..${'\\'}`)
|
|
113
|
+
|| isAbsolute(rel);
|
|
114
|
+
if (escapes) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
reason: `path escapes project root (${canonTarget} not under ${canonRoot})`,
|
|
118
|
+
canonical: canonTarget,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, reason: '', canonical: canonTarget };
|
|
122
|
+
}
|
|
123
|
+
|
|
38
124
|
/* ────────────────────────────── status codes ────────────────────────────── */
|
|
39
125
|
|
|
40
126
|
export const STATUS = Object.freeze({
|
|
@@ -46,6 +132,12 @@ export const STATUS = Object.freeze({
|
|
|
46
132
|
FALLBACK_FAIL: 'FALLBACK_FAIL',
|
|
47
133
|
TRIDENT_FAIL: 'TRIDENT_FAIL',
|
|
48
134
|
COMMIT_FAIL: 'COMMIT_FAIL',
|
|
135
|
+
// R5-1.10 — finding's target file resolves outside the audited project
|
|
136
|
+
// root. The fixer refuses to touch it (path-containment guard).
|
|
137
|
+
OUT_OF_ROOT: 'OUT_OF_ROOT',
|
|
138
|
+
// R5-1.10 — the per-run change cap was reached; this finding (and any
|
|
139
|
+
// after it) was skipped without being applied.
|
|
140
|
+
CAP_REACHED: 'CAP_REACHED',
|
|
49
141
|
});
|
|
50
142
|
|
|
51
143
|
/* ────────────────────────────── logic-bug heuristic ─────────────────────── */
|
|
@@ -378,7 +470,7 @@ async function applyEdit(filePath, fix) {
|
|
|
378
470
|
// Exactly-one occurrence guarantee — same rule the Edit tool uses.
|
|
379
471
|
const occurrences = before.split(fix.old_string).length - 1;
|
|
380
472
|
if (occurrences > 1 && !fix.replace_all) {
|
|
381
|
-
return { ok: false, evidence: `old_string occurs ${occurrences}
|
|
473
|
+
return { ok: false, evidence: `old_string occurs ${occurrences}x; ambiguous`, before };
|
|
382
474
|
}
|
|
383
475
|
after = fix.replace_all
|
|
384
476
|
? before.split(fix.old_string).join(fix.new_string)
|
|
@@ -446,9 +538,27 @@ export async function fixFinding({
|
|
|
446
538
|
}
|
|
447
539
|
|
|
448
540
|
// 2. confirm target exists + snapshot
|
|
541
|
+
const root = projectRoot || process.cwd();
|
|
449
542
|
const filePath = isAbsolute(finding.file)
|
|
450
543
|
? finding.file
|
|
451
|
-
: resolvePath(
|
|
544
|
+
: resolvePath(root, finding.file);
|
|
545
|
+
|
|
546
|
+
// R5-1.10 — PATH CONTAINMENT. Auto-fix mutates the working tree; it must
|
|
547
|
+
// only ever touch files inside the project root being audited. A finding
|
|
548
|
+
// whose `file` resolves outside the root (absolute escape, `../` traversal,
|
|
549
|
+
// or a symlink pointing out) is REFUSED before any read/write happens.
|
|
550
|
+
// This is checked before existsSync so an out-of-root path can't even be
|
|
551
|
+
// probed for existence.
|
|
552
|
+
const contained = isPathContained(filePath, root);
|
|
553
|
+
if (!contained.ok) {
|
|
554
|
+
return {
|
|
555
|
+
...base,
|
|
556
|
+
status: STATUS.OUT_OF_ROOT,
|
|
557
|
+
tier_reached: 'n/a',
|
|
558
|
+
evidence: `auto-fix refused: ${contained.reason}`,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
452
562
|
if (!existsSync(filePath)) {
|
|
453
563
|
return { ...base, status: STATUS.STALE, tier_reached: 'n/a',
|
|
454
564
|
evidence: `target file does not exist: ${filePath}` };
|
|
@@ -583,9 +693,21 @@ export async function fixFinding({
|
|
|
583
693
|
* the Trident step needs a stable commit range. Parallel fixes would shred
|
|
584
694
|
* the atomicity guarantee.
|
|
585
695
|
*
|
|
696
|
+
* R5-1.10 — CHANGE CAP. `opts.maxAutoFixFiles` (default
|
|
697
|
+
* DEFAULT_MAX_AUTOFIX_FILES = 10, hard-ceilinged at 50) bounds the number of
|
|
698
|
+
* DISTINCT files this batch may successfully apply a fix to. Once that many
|
|
699
|
+
* files have been touched, every remaining finding that targets a not-yet-
|
|
700
|
+
* seen file is short-circuited with status CAP_REACHED (no read, no write) —
|
|
701
|
+
* the loop stops mutating and reports the remainder instead of mass-
|
|
702
|
+
* rewriting the repo. Findings that re-target an already-fixed file are
|
|
703
|
+
* still allowed through (they don't grow the blast radius). Statuses that
|
|
704
|
+
* don't write a file (DEFERRED / STALE / OUT_OF_ROOT / *_FAIL) never count
|
|
705
|
+
* against the cap.
|
|
706
|
+
*
|
|
586
707
|
* Returns { results: Array<fixFinding-record>, summary: { verified, deferred,
|
|
587
708
|
* stale, verify_fail, syntax_fail, fallback_fail, trident_fail,
|
|
588
|
-
* commit_fail }
|
|
709
|
+
* commit_fail, out_of_root, cap_reached }, capped: boolean,
|
|
710
|
+
* filesTouched: number, maxAutoFixFiles: number }.
|
|
589
711
|
*/
|
|
590
712
|
export async function fixFindings(findings, opts = {}) {
|
|
591
713
|
const results = [];
|
|
@@ -593,15 +715,198 @@ export async function fixFindings(findings, opts = {}) {
|
|
|
593
715
|
verified: 0, deferred: 0, stale: 0,
|
|
594
716
|
verify_fail: 0, syntax_fail: 0, fallback_fail: 0,
|
|
595
717
|
trident_fail: 0, commit_fail: 0,
|
|
718
|
+
out_of_root: 0, cap_reached: 0,
|
|
596
719
|
};
|
|
720
|
+
|
|
721
|
+
// Resolve the per-run change cap. Clamp to [1, MAX_AUTOFIX_FILES_CEILING];
|
|
722
|
+
// a non-positive or non-numeric value falls back to the default.
|
|
723
|
+
const reqCap = Number(opts.maxAutoFixFiles);
|
|
724
|
+
const cap = Number.isFinite(reqCap) && reqCap > 0
|
|
725
|
+
? Math.min(Math.floor(reqCap), MAX_AUTOFIX_FILES_CEILING)
|
|
726
|
+
: DEFAULT_MAX_AUTOFIX_FILES;
|
|
727
|
+
|
|
728
|
+
// Distinct files this batch has successfully written to. A fix counts as
|
|
729
|
+
// "touching" a file only if it actually applied an edit — VERIFIED or any
|
|
730
|
+
// failure mode that happens AFTER applyEdit (VERIFY_FAIL/SYNTAX_FAIL/
|
|
731
|
+
// FALLBACK_FAIL/TRIDENT_FAIL/COMMIT_FAIL all roll the file back, but the
|
|
732
|
+
// file WAS written then reverted, so they still count toward blast radius).
|
|
733
|
+
const APPLIED = new Set([
|
|
734
|
+
STATUS.VERIFIED, STATUS.VERIFY_FAIL, STATUS.SYNTAX_FAIL,
|
|
735
|
+
STATUS.FALLBACK_FAIL, STATUS.TRIDENT_FAIL, STATUS.COMMIT_FAIL,
|
|
736
|
+
]);
|
|
737
|
+
const filesTouched = new Set();
|
|
738
|
+
let capped = false;
|
|
739
|
+
|
|
597
740
|
for (const finding of (findings || [])) {
|
|
741
|
+
const targetFile = finding && typeof finding.file === 'string'
|
|
742
|
+
? finding.file : null;
|
|
743
|
+
const alreadyTouched = targetFile && filesTouched.has(targetFile);
|
|
744
|
+
|
|
745
|
+
// Cap gate: if we've hit the ceiling AND this finding would touch a NEW
|
|
746
|
+
// file, refuse it without reading/writing anything.
|
|
747
|
+
if (filesTouched.size >= cap && !alreadyTouched) {
|
|
748
|
+
capped = true;
|
|
749
|
+
results.push({
|
|
750
|
+
finding_id: finding?.finding_id || finding?.id || 'unknown',
|
|
751
|
+
file: targetFile,
|
|
752
|
+
status: STATUS.CAP_REACHED,
|
|
753
|
+
tier_reached: 'n/a',
|
|
754
|
+
evidence: `auto-fix change cap reached (${cap} files); skipped without applying`,
|
|
755
|
+
});
|
|
756
|
+
summary.cap_reached += 1;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
598
760
|
// eslint-disable-next-line no-await-in-loop -- sequential is the contract
|
|
599
761
|
const r = await fixFinding({ ...opts, finding });
|
|
600
762
|
results.push(r);
|
|
601
763
|
const k = String(r.status || '').toLowerCase();
|
|
602
764
|
if (k in summary) summary[k] += 1;
|
|
765
|
+
|
|
766
|
+
// Record blast radius: any status that got past applyEdit touched a file.
|
|
767
|
+
if (APPLIED.has(r.status) && r.file) {
|
|
768
|
+
filesTouched.add(r.file);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
results,
|
|
773
|
+
summary,
|
|
774
|
+
capped,
|
|
775
|
+
filesTouched: filesTouched.size,
|
|
776
|
+
maxAutoFixFiles: cap,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/* ────────────────────── consensus-HIGH extraction (T27 wire-up) ──────────── */
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Normalise a finding's severity to a lowercase canonical token.
|
|
784
|
+
* Auditors emit `high` / `HIGH` / `High` / sometimes `severity: { level }`.
|
|
785
|
+
*/
|
|
786
|
+
function _severityOf(finding) {
|
|
787
|
+
if (!finding || typeof finding !== 'object') return '';
|
|
788
|
+
const raw = finding.severity ?? finding.level ?? '';
|
|
789
|
+
if (raw && typeof raw === 'object') {
|
|
790
|
+
return String(raw.level || raw.severity || '').toLowerCase().trim();
|
|
791
|
+
}
|
|
792
|
+
return String(raw).toLowerCase().trim();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* A stable identity key for cross-lens finding agreement. Two lenses "agree"
|
|
797
|
+
* on the same HIGH when their findings collapse to the same key. We use the
|
|
798
|
+
* file path + a normalised description prefix (whitespace-folded, lowercased,
|
|
799
|
+
* first 80 chars) — precise enough to cluster genuine duplicates, loose enough
|
|
800
|
+
* to survive trivial wording drift between lenses.
|
|
801
|
+
*/
|
|
802
|
+
function _consensusKey(finding) {
|
|
803
|
+
const file = String(finding.file || finding.location || finding.path || '').trim();
|
|
804
|
+
const descSource =
|
|
805
|
+
finding.description || finding.issue || finding.text ||
|
|
806
|
+
finding.message || finding.finding || finding.detail || '';
|
|
807
|
+
const desc = String(descSource).toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
808
|
+
return `${file}::${desc}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* consensusHighFindings(perIteration, opts) — given the `perIteration` array
|
|
813
|
+
* that `runPhaseEConverge` returns, extract the HIGH-severity findings on
|
|
814
|
+
* which `minLenses` (default 2) or more lenses agree.
|
|
815
|
+
*
|
|
816
|
+
* This is the bridge T27 was designed for: "when 2+ lenses agree on the same
|
|
817
|
+
* HIGH, the fixer fires automatically." The convergence loop produces
|
|
818
|
+
* `perIteration[*].lensResults[*].findings`; this collapses the final
|
|
819
|
+
* iteration's findings into per-lens-deduped consensus clusters.
|
|
820
|
+
*
|
|
821
|
+
* Returns Array<finding> — each carries `_consensusLenses` (the set of lens
|
|
822
|
+
* ids that flagged it) and `_consensusCount`. Only the LAST iteration is
|
|
823
|
+
* considered: convergence already re-evaluated earlier rounds, so the final
|
|
824
|
+
* round is the swarm's settled position.
|
|
825
|
+
*/
|
|
826
|
+
export function consensusHighFindings(perIteration, opts = {}) {
|
|
827
|
+
const minLenses = Number.isInteger(opts.minLenses) && opts.minLenses > 0
|
|
828
|
+
? opts.minLenses
|
|
829
|
+
: 2;
|
|
830
|
+
if (!Array.isArray(perIteration) || perIteration.length === 0) return [];
|
|
831
|
+
const last = perIteration[perIteration.length - 1];
|
|
832
|
+
if (!last || !Array.isArray(last.lensResults)) return [];
|
|
833
|
+
|
|
834
|
+
// key → { finding, lenses:Set }
|
|
835
|
+
const clusters = new Map();
|
|
836
|
+
for (const lr of last.lensResults) {
|
|
837
|
+
const lens = lr && lr.lens ? lr.lens : 'unknown';
|
|
838
|
+
const findings = Array.isArray(lr && lr.findings) ? lr.findings : [];
|
|
839
|
+
// De-dup within a single lens first so one lens flagging the same issue
|
|
840
|
+
// twice can't manufacture false consensus.
|
|
841
|
+
const seenThisLens = new Set();
|
|
842
|
+
for (const f of findings) {
|
|
843
|
+
if (_severityOf(f) !== 'high' && _severityOf(f) !== 'critical') continue;
|
|
844
|
+
const key = _consensusKey(f);
|
|
845
|
+
if (seenThisLens.has(key)) continue;
|
|
846
|
+
seenThisLens.add(key);
|
|
847
|
+
if (!clusters.has(key)) clusters.set(key, { finding: f, lenses: new Set() });
|
|
848
|
+
clusters.get(key).lenses.add(lens);
|
|
849
|
+
}
|
|
603
850
|
}
|
|
604
|
-
|
|
851
|
+
|
|
852
|
+
const out = [];
|
|
853
|
+
for (const { finding, lenses } of clusters.values()) {
|
|
854
|
+
if (lenses.size >= minLenses) {
|
|
855
|
+
out.push({ ...finding, _consensusLenses: [...lenses], _consensusCount: lenses.size });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* runConsensusFix({ perIteration, projectRoot, dispatch, ...fixOpts }) —
|
|
863
|
+
* the T27 auto-fix entry point. Extracts consensus HIGHs from a completed
|
|
864
|
+
* `runPhaseEConverge` run and runs the per-finding atomic fixer loop over
|
|
865
|
+
* them.
|
|
866
|
+
*
|
|
867
|
+
* R5-1.10 SAFETY BOUNDARY — auto-fix mutates code, so two hard guards apply
|
|
868
|
+
* (both inherited from `fixFindings` / `fixFinding`, surfaced here):
|
|
869
|
+
* • Path containment — any finding whose target file resolves outside
|
|
870
|
+
* `projectRoot` is REFUSED (status OUT_OF_ROOT); the fixer can never
|
|
871
|
+
* write outside the audited project.
|
|
872
|
+
* • Change cap — `maxAutoFixFiles` (default 10, ceiling 50) bounds the
|
|
873
|
+
* distinct files a single run may touch; beyond it the loop STOPS and
|
|
874
|
+
* reports the remainder (status CAP_REACHED) instead of mass-rewriting.
|
|
875
|
+
* Pass `dryRun: true` for detect-only (reports what it WOULD fix, no edits).
|
|
876
|
+
*
|
|
877
|
+
* Returns:
|
|
878
|
+
* { triggered: false, reason } — nothing to fix
|
|
879
|
+
* { triggered: true, consensusCount, results, summary, capped,
|
|
880
|
+
* filesTouched, maxAutoFixFiles } — fixer ran
|
|
881
|
+
*
|
|
882
|
+
* `dispatch` is required so each fix's Trident re-verify can run.
|
|
883
|
+
*/
|
|
884
|
+
export async function runConsensusFix({
|
|
885
|
+
perIteration,
|
|
886
|
+
projectRoot,
|
|
887
|
+
dispatch,
|
|
888
|
+
minLenses = 2,
|
|
889
|
+
...fixOpts
|
|
890
|
+
} = {}) {
|
|
891
|
+
const consensus = consensusHighFindings(perIteration, { minLenses });
|
|
892
|
+
if (consensus.length === 0) {
|
|
893
|
+
return { triggered: false, reason: 'no consensus HIGH findings' };
|
|
894
|
+
}
|
|
895
|
+
const { results, summary, capped, filesTouched, maxAutoFixFiles } =
|
|
896
|
+
await fixFindings(consensus, {
|
|
897
|
+
...fixOpts,
|
|
898
|
+
projectRoot,
|
|
899
|
+
dispatch,
|
|
900
|
+
});
|
|
901
|
+
return {
|
|
902
|
+
triggered: true,
|
|
903
|
+
consensusCount: consensus.length,
|
|
904
|
+
results,
|
|
905
|
+
summary,
|
|
906
|
+
capped,
|
|
907
|
+
filesTouched,
|
|
908
|
+
maxAutoFixFiles,
|
|
909
|
+
};
|
|
605
910
|
}
|
|
606
911
|
|
|
607
912
|
/* ────────────────────────────── test helpers ────────────────────────────── */
|
package/src/runtime-mediator.js
CHANGED
|
@@ -224,7 +224,6 @@ export function toolNameToActionTarget(toolName, args) {
|
|
|
224
224
|
return { action: 'write', target: 'memory:write' };
|
|
225
225
|
case 'ijfw_memory_recall':
|
|
226
226
|
case 'ijfw_memory_search':
|
|
227
|
-
case 'ijfw_memory_status':
|
|
228
227
|
case 'ijfw_memory_prelude':
|
|
229
228
|
case 'ijfw_cross_project_search':
|
|
230
229
|
return { action: 'read', target: 'memory:read' };
|