pi-mega-compact 0.20.6 → 0.20.8
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/dist/config/vector-cortex.js +12 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/api-contracts/vector-cortex-heal.js +11 -0
- package/dist/extensions/dashboard-server/route-dispatch.js +102 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +3 -32
- package/dist/extensions/dashboard-server/routes-rag-settings-types.js +11 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +44 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-heal.js +47 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +1 -0
- package/dist/extensions/dashboard-server/routes.js +1 -1
- package/dist/extensions/dashboard-server/server.js +3 -82
- package/dist/src/config/dedup.js +1 -0
- package/dist/src/config/vector-cortex.js +12 -0
- package/dist/src/config.js +1 -1
- package/dist/src/dedup-audit.test-helpers.js +43 -0
- package/dist/src/monitoring.js +9 -0
- package/dist/src/vector-cortex/heal/_acceptance-fixture.js +56 -0
- package/dist/src/vector-cortex/heal/closure-metrics.js +66 -0
- package/dist/src/vector-cortex/heal/closure-opt.js +242 -0
- package/dist/src/vector-cortex/heal/emit.js +69 -0
- package/dist/src/vector-cortex/heal/proof.js +210 -0
- package/dist/src/vector-cortex/heal/types.js +42 -0
- package/dist/src/vectorStore/add-l0.js +108 -0
- package/dist/src/vectorStore/add-l1.js +35 -0
- package/dist/src/vectorStore/add.js +229 -0
- package/dist/src/vectorStore/class.js +15 -291
- package/dist/src/vectorStore/dedup-audit.js +104 -0
- package/dist/src/vectorStore.js +1 -0
- package/dist/vector-cortex/heal/_acceptance-fixture.js +56 -0
- package/dist/vector-cortex/heal/closure-metrics.js +66 -0
- package/dist/vector-cortex/heal/closure-opt.js +242 -0
- package/dist/vector-cortex/heal/emit.js +69 -0
- package/dist/vector-cortex/heal/proof.js +210 -0
- package/dist/vector-cortex/heal/types.js +42 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-DqTbaSRA.js → AreaChart-5yxaI8IO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-DqTbaSRA.js.map → AreaChart-5yxaI8IO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-BfNBeOvF.js → BarChart-BoHhCqH-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-BfNBeOvF.js.map → BarChart-BoHhCqH-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-C9SOMYB5.js → CacheTab-Di6L3CsF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-C9SOMYB5.js.map → CacheTab-Di6L3CsF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-GsMKP10_.js → EventsTab-Cdff3h6s.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-GsMKP10_.js.map → EventsTab-Cdff3h6s.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-DAOCXwRP.js → HealthTab-MthYYVLw.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-DAOCXwRP.js.map → HealthTab-MthYYVLw.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BtBeMkB5.js → MaintenanceTab-D9G4ntsG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BtBeMkB5.js.map → MaintenanceTab-D9G4ntsG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DzndtgkZ.js → MemoryMapTab-CN8Lr5Qf.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DzndtgkZ.js.map → MemoryMapTab-CN8Lr5Qf.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DyxJ4eb8.js → MetricsTab-CgO32kSo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DyxJ4eb8.js.map → MetricsTab-CgO32kSo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DCiEdVWR.js → OverviewTab-Cb4kR0h1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DCiEdVWR.js.map → OverviewTab-Cb4kR0h1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-B5SIqVHA.js → ReposTab-Dt1FGnMR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-B5SIqVHA.js.map → ReposTab-Dt1FGnMR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DLWFJ9xI.js → SessionsTab-D7D2RW6U.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DLWFJ9xI.js.map → SessionsTab-D7D2RW6U.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-CvGXZyej.js → SetupTab-CgV5xCzz.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-CvGXZyej.js.map → SetupTab-CgV5xCzz.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-wZuQGxLS.js → TimeSavedCard-CCNuJjyO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-wZuQGxLS.js.map → TimeSavedCard-CCNuJjyO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-B7aOejch.js → TurnsTab-Cv1OCttJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-B7aOejch.js.map → TurnsTab-Cv1OCttJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DTabQo6h.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DTabQo6h.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-DhWHjAtt.js → WikiTab-B48QnQrK.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-DhWHjAtt.js.map → WikiTab-B48QnQrK.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-BrC-W87h.js → button-BBQl2MiF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-BrC-W87h.js.map → button-BBQl2MiF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-0rgfRgr3.js → card-BYbefUur.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-0rgfRgr3.js.map → card-BYbefUur.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DRresOQF.js → generateCategoricalChart-swm0YZe0.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DRresOQF.js.map → generateCategoricalChart-swm0YZe0.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DohmtZWL.js → index-DeIky7xm.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DohmtZWL.js.map → index-DeIky7xm.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-Bs8g_zB5.js → switch-CctU78ww.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-Bs8g_zB5.js.map → switch-CctU78ww.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-DVVauIo8.js → toggle-BRCNCoiI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-DVVauIo8.js.map → toggle-BRCNCoiI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-DxH1PTAR.js → useSSE-WfOiWDH_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-DxH1PTAR.js.map → useSSE-WfOiWDH_.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexClosureCard.tsx +46 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexLedgerCard.tsx +80 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +12 -164
- package/extensions/dashboard-client/src/tabs/VectorCortexTopologyCard.tsx +131 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +14 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex-heal.ts +43 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +2 -0
- package/extensions/dashboard-server/route-dispatch.ts +112 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +11 -135
- package/extensions/dashboard-server/routes-rag-settings-types.ts +34 -0
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +148 -0
- package/extensions/dashboard-server/routes-vector-cortex-heal.ts +56 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +1 -0
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +3 -86
- package/package.json +1 -1
- package/src/config/dedup.ts +9 -0
- package/src/config/vector-cortex.ts +13 -0
- package/src/config.ts +1 -0
- package/src/dedup-audit.test-helpers.ts +49 -0
- package/src/monitoring.ts +10 -0
- package/src/vector-cortex/heal/_acceptance-fixture.ts +108 -0
- package/src/vector-cortex/heal/closure-metrics.ts +73 -0
- package/src/vector-cortex/heal/closure-opt.ts +266 -0
- package/src/vector-cortex/heal/emit.ts +93 -0
- package/src/vector-cortex/heal/proof.ts +234 -0
- package/src/vector-cortex/heal/types.ts +226 -0
- package/src/vectorStore/add-l0.ts +152 -0
- package/src/vectorStore/add-l1.ts +57 -0
- package/src/vectorStore/add.ts +269 -0
- package/src/vectorStore/class.ts +17 -327
- package/src/vectorStore/dedup-audit.ts +199 -0
- package/src/vectorStore.ts +1 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-eHVt9yDx.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-eHVt9yDx.js.map +0 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/heal/proof.ts — VC6A proof verifier (task 4).
|
|
3
|
+
*
|
|
4
|
+
* The optimizer (`closure-opt.ts`) is PURE and cheap, but a proof is cheap to
|
|
5
|
+
* LIE about: a buggy or tampered producer could ship a proof that removes an
|
|
6
|
+
* edge the conservative closure actually needs. The verifier does not trust the
|
|
7
|
+
* proof — it REPLAYS it against the conservative oracle from scratch.
|
|
8
|
+
*
|
|
9
|
+
* REPLAY. From the conservative `ClosureResult` we rebuild the requirement
|
|
10
|
+
* adjacency (`to ⇒ from` for `depends`, both directions for `tool-pair`) over
|
|
11
|
+
* the in-selection edges — exactly the relation `closure-opt` reduced over.
|
|
12
|
+
* For every row the proof claims to REMOVE, the verifier independently confirms
|
|
13
|
+
* the same alternate path exists. For every row it claims to RETAIN, it confirms
|
|
14
|
+
* the protection reason holds (or that no alternate path exists). Any row that
|
|
15
|
+
* fails its own predicate is a witness violation.
|
|
16
|
+
*
|
|
17
|
+
* SELECTED-SET DIVERGENCE (the cardinal sin). The proof must contain the SAME
|
|
18
|
+
* selected set as the conservative oracle, byte for byte. Closure optimization
|
|
19
|
+
* is allowed to make the plan cheaper, never smaller — so if the proof's
|
|
20
|
+
* `selected` differs from the oracle's, that is `HEAL_PROOF_SET_MISMATCH` and the
|
|
21
|
+
* repaired triad must fall back to mode B (the conservative closure) and
|
|
22
|
+
* ultimately C (VC5C's legacy prompt) if even that fails.
|
|
23
|
+
*
|
|
24
|
+
* PURE / DETERMINISTIC / LOCAL, like everything in `heal/`: no storage, no
|
|
25
|
+
* console, no clock, no network (PREVENT-PI-004 / PREVENT-011). The verifier
|
|
26
|
+
* reads only types and the two ClosureEdge/ClosureGraph/ClosureResult shapes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { ClosureGraph, ClosureResult } from "../reconstruct/types.js";
|
|
30
|
+
import type {
|
|
31
|
+
ClosureProofRow,
|
|
32
|
+
ClosureProofV2,
|
|
33
|
+
HealFailureCode,
|
|
34
|
+
HealMode,
|
|
35
|
+
HealTriadOutcome,
|
|
36
|
+
ProofVerification,
|
|
37
|
+
} from "./types.js";
|
|
38
|
+
|
|
39
|
+
/** Bytewise id comparator — matches `closure-opt.ts` / `reconstruct/closure.ts`. */
|
|
40
|
+
function byBytes(a: string, b: string): number {
|
|
41
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Requirement adjacency over an edge list, identical to `closure-opt`. */
|
|
45
|
+
function requirementAdjacency(edges: readonly ClosureProofRow[]): Map<string, string[]> {
|
|
46
|
+
const adj = new Map<string, string[]>();
|
|
47
|
+
const add = (key: string, value: string): void => {
|
|
48
|
+
const list = adj.get(key);
|
|
49
|
+
if (list === undefined) adj.set(key, [value]);
|
|
50
|
+
else list.push(value);
|
|
51
|
+
};
|
|
52
|
+
for (const row of edges) {
|
|
53
|
+
if (row.kind === "contradicts") continue; // not a traversal edge
|
|
54
|
+
if (row.kind === "depends") add(row.to, row.from);
|
|
55
|
+
else if (row.kind === "tool-pair") {
|
|
56
|
+
add(row.to, row.from);
|
|
57
|
+
add(row.from, row.to);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const list of adj.values()) list.sort(byBytes);
|
|
61
|
+
return adj;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Does a detour `start ⇒ via ⇒ ... ⇒ target` exist, excluding the direct hop? */
|
|
65
|
+
function alternatePathVia(
|
|
66
|
+
adj: ReadonlyMap<string, string[]>,
|
|
67
|
+
start: string,
|
|
68
|
+
target: string,
|
|
69
|
+
): string | null {
|
|
70
|
+
for (const hop of (adj.get(start) ?? []).filter((n) => n !== target)) {
|
|
71
|
+
const seen = new Set<string>([start]);
|
|
72
|
+
const worklist: string[] = [hop];
|
|
73
|
+
while (worklist.length > 0) {
|
|
74
|
+
worklist.sort(byBytes);
|
|
75
|
+
const current = worklist.shift();
|
|
76
|
+
if (current === undefined) break;
|
|
77
|
+
if (current === target) return hop;
|
|
78
|
+
if (seen.has(current)) continue;
|
|
79
|
+
seen.add(current);
|
|
80
|
+
for (const next of adj.get(current) ?? []) {
|
|
81
|
+
if (!seen.has(next)) worklist.push(next);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Verify a `ClosureProofV2` produced for an already-closed graph.
|
|
90
|
+
*
|
|
91
|
+
* Returns `{ ok: true, selected }` when the replay confirms the optimized plan is
|
|
92
|
+
* provably equivalent to the conservative closure — mode A may be used. On any
|
|
93
|
+
* divergence it returns `{ ok: false, codes }` listing every failure code; the
|
|
94
|
+
* caller routes to mode B (conservative) and, if that also fails, mode C (legacy,
|
|
95
|
+
* with `semanticLossStated`). The verifier is idempotent — replaying the same
|
|
96
|
+
* proof twice yields the same verdict, which `vc6a-acceptance.test.ts` asserts.
|
|
97
|
+
*/
|
|
98
|
+
export function verifyProof(
|
|
99
|
+
proof: ClosureProofV2,
|
|
100
|
+
conservative: ClosureResult,
|
|
101
|
+
graph?: ClosureGraph,
|
|
102
|
+
): ProofVerification {
|
|
103
|
+
const codes: HealFailureCode[] = [];
|
|
104
|
+
const inSelection = new Set(conservative.selected);
|
|
105
|
+
const anchors = new Set(
|
|
106
|
+
(graph?.nodes ?? []).filter((n) => n.anchor === true).map((n) => n.id),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// 1. Selected-set divergence is the hard gate (task 4: HEAL_PROOF_SET_MISMATCH).
|
|
110
|
+
const oracleSelected = [...conservative.selected].sort(byBytes);
|
|
111
|
+
const proofSelected = [...proof.selected].sort(byBytes);
|
|
112
|
+
const selectedMatch =
|
|
113
|
+
oracleSelected.length === proofSelected.length &&
|
|
114
|
+
oracleSelected.every((id, i) => id === proofSelected[i]);
|
|
115
|
+
if (!selectedMatch) codes.push("HEAL_PROOF_SET_MISMATCH");
|
|
116
|
+
|
|
117
|
+
// 2. Re-check each row against its own predicate. For a removal, the witness
|
|
118
|
+
// is validated against the OTHER rows (the reduced graph), so a removal
|
|
119
|
+
// never justifies another's witness — this catches both a missing-witness
|
|
120
|
+
// removal (HEAL-PROOF-003) and a protected edge that was removed
|
|
121
|
+
// (HEAL_PROOF_PROTECTED_REMOVED).
|
|
122
|
+
|
|
123
|
+
for (const row of proof.rows) {
|
|
124
|
+
// Out-of-scope rows (touch an unselected node) should never have been
|
|
125
|
+
// emitted; treat as a witness violation so a malformed proof fails loudly.
|
|
126
|
+
if (!inSelection.has(row.from) || !inSelection.has(row.to)) {
|
|
127
|
+
codes.push("HEAL_PROOF_WITNESS_INVALID");
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (row.decision === "removed") {
|
|
131
|
+
// A removal MUST carry a witness and the witness MUST still validate
|
|
132
|
+
// against the OTHER rows (the reduced graph) — never the row itself, so a
|
|
133
|
+
// removal never justifies another's witness (matches the optimizer's
|
|
134
|
+
// exclusion rule).
|
|
135
|
+
if (row.via === undefined) {
|
|
136
|
+
codes.push("HEAL_PROOF_WITNESS_INVALID");
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const others = proof.rows.filter((r) => r !== row);
|
|
140
|
+
const witness = alternatePathVia(requirementAdjacency(others), row.to, row.from);
|
|
141
|
+
if (witness === null) {
|
|
142
|
+
codes.push("HEAL_PROOF_WITNESS_INVALID");
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (anchors.has(row.from) || anchors.has(row.to)) {
|
|
146
|
+
// Topology says removable, but an anchor edge is NEVER removable.
|
|
147
|
+
codes.push("HEAL_PROOF_PROTECTED_REMOVED");
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (row.kind === "tool-pair" || row.kind === "contradicts") {
|
|
151
|
+
// Both are protected regardless of topology.
|
|
152
|
+
codes.push("HEAL_PROOF_PROTECTED_REMOVED");
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// A retained plain depends edge that claims `no-alternate-path` but
|
|
156
|
+
// actually HAS one (through the other rows) is an internal inconsistency:
|
|
157
|
+
// the optimizer kept a removable edge without removing it. The proof is
|
|
158
|
+
// internally contradictory, so it fails replay.
|
|
159
|
+
if (row.kind === "depends" && row.reason === "no-alternate-path") {
|
|
160
|
+
const others = proof.rows.filter((r) => r !== row);
|
|
161
|
+
const hasPath = alternatePathVia(requirementAdjacency(others), row.to, row.from);
|
|
162
|
+
if (hasPath !== null) codes.push("HEAL_PROOF_WITNESS_INVALID");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 3. Completeness: every in-selection edge the optimizer considered must appear
|
|
168
|
+
// in the proof. A dropped row (the unique-injection test drops one) surfaces
|
|
169
|
+
// as HEAL_PROOF_INCOMPLETE even if the remaining rows replay fine — the proof
|
|
170
|
+
// must account for EVERY considered edge. `conservativeTraversals` is the
|
|
171
|
+
// optimizer's own record of how many edges it fed through, so `rows.length`
|
|
172
|
+
// must equal it exactly.
|
|
173
|
+
if (proof.rows.length < proof.conservativeTraversals) {
|
|
174
|
+
codes.push("HEAL_PROOF_INCOMPLETE");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (codes.length === 0) {
|
|
178
|
+
return { ok: true, selected: proofSelected };
|
|
179
|
+
}
|
|
180
|
+
return { ok: false, codes: [...new Set(codes)] };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Drive the A/B/C resilient triad for one closure (task 5 wiring; VC6A exposes
|
|
185
|
+
* the decision; the actual selection/budget work is VC4C/VC5A respectively).
|
|
186
|
+
*
|
|
187
|
+
* - A: the optimized proof verified cleanly → ship the optimized plan.
|
|
188
|
+
* - B: proof rejected → fall back to the conservative VC4C closure (no opt).
|
|
189
|
+
* - C: B also failed (should not happen for a sound conservative closure) →
|
|
190
|
+
* state the legacy prompt path with `semanticLossStated`.
|
|
191
|
+
*
|
|
192
|
+
* This is PURE: it returns the verdict and which mode to use; it does not emit,
|
|
193
|
+
* store, or touch the network. The emit seam (`emit.ts`) consumes the outcome.
|
|
194
|
+
*/
|
|
195
|
+
export function selectHealMode(
|
|
196
|
+
proof: ClosureProofV2,
|
|
197
|
+
conservative: ClosureResult,
|
|
198
|
+
graph?: ClosureGraph,
|
|
199
|
+
): HealTriadOutcome {
|
|
200
|
+
const verification = verifyProof(proof, conservative, graph);
|
|
201
|
+
if (verification.ok) {
|
|
202
|
+
return {
|
|
203
|
+
mode: "A" as HealMode,
|
|
204
|
+
proof,
|
|
205
|
+
selected: [...proof.selected],
|
|
206
|
+
codes: [],
|
|
207
|
+
semanticLossStated: false,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// Mode B: conservative closure is sound by construction (it is the oracle the
|
|
211
|
+
// proof was measured against). Use it directly.
|
|
212
|
+
return {
|
|
213
|
+
mode: "B" as HealMode,
|
|
214
|
+
proof: null,
|
|
215
|
+
selected: [...conservative.selected],
|
|
216
|
+
codes: verification.codes,
|
|
217
|
+
semanticLossStated: false,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Convenience for the rare B-failure path: if even the conservative oracle's
|
|
223
|
+
* selection cannot be honored, state the legacy prompt and MARK semantic loss.
|
|
224
|
+
* Mode C is the last resort and the only place semantic loss is ever admitted.
|
|
225
|
+
*/
|
|
226
|
+
export function legacyFallback(conservative: ClosureResult, reason: HealFailureCode): HealTriadOutcome {
|
|
227
|
+
return {
|
|
228
|
+
mode: "C" as HealMode,
|
|
229
|
+
proof: null,
|
|
230
|
+
selected: [...conservative.selected],
|
|
231
|
+
codes: [reason, "HEAL_CLOSURE_REJECTED"],
|
|
232
|
+
semanticLossStated: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/heal/types.ts — VC6A closure-optimization contract.
|
|
3
|
+
*
|
|
4
|
+
* Owns `ClosureProofV2` and `RestoreHintV1`.
|
|
5
|
+
*
|
|
6
|
+
* VC4C already performs the MANDATORY conservative closure (CONTRACTS §plan and
|
|
7
|
+
* closure): it recursively adds every `depends`/tool-pair predecessor to a fixed
|
|
8
|
+
* point, resolves contradictions, and preserves the anchor floor. VC6A does NOT
|
|
9
|
+
* re-close and does NOT change WHICH NODES ARE SELECTED — "VC6 only optimizes
|
|
10
|
+
* restoration/self-healing". It optimizes the PROOF EDGES of that already-closed
|
|
11
|
+
* graph by transitive reduction: an edge `a -> c` whose requirement is already
|
|
12
|
+
* carried by a path `a -> b -> c` is redundant and can be dropped from the
|
|
13
|
+
* traversal plan without changing the closed set.
|
|
14
|
+
*
|
|
15
|
+
* The optimization is therefore a claim, and a claim needs a receipt:
|
|
16
|
+
* `ClosureProofV2` records EVERY considered edge — retained or removed — with
|
|
17
|
+
* the reason. `proof.ts` replays those rows against the conservative VC4C oracle
|
|
18
|
+
* and rejects the optimization if the selected set diverges
|
|
19
|
+
* (`HEAL_PROOF_SET_MISMATCH`) or if a considered edge has no proof row
|
|
20
|
+
* (`HEAL_PROOF_INCOMPLETE`).
|
|
21
|
+
*
|
|
22
|
+
* PROTECTED EDGES ARE NEVER REMOVED. A tool-pair edge (PREVENT-PI-002), an edge
|
|
23
|
+
* touching an anchor node (PREVENT-PI-001's anchor floor), a
|
|
24
|
+
* contradiction-resolution edge, and a SOLE dependency edge (the only edge that
|
|
25
|
+
* pulls its prerequisite in) are all retained unconditionally, even when an
|
|
26
|
+
* alternate path exists. Only a redundant `depends` edge may go.
|
|
27
|
+
*
|
|
28
|
+
* Pure types + registered conformance IDs: no storage, no console, no network
|
|
29
|
+
* (PREVENT-PI-004 / PREVENT-011).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { ClosureEdge, ClosureEdgeKind } from "../reconstruct/types.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Why an edge survived the reduction. Ordered from strongest guarantee to
|
|
36
|
+
* weakest:
|
|
37
|
+
*
|
|
38
|
+
* - `tool-pair` — a tool call/result pair is atomic and never split
|
|
39
|
+
* (PREVENT-PI-002), so its edge is structural, not
|
|
40
|
+
* merely a traversal hint;
|
|
41
|
+
* - `anchor` — an endpoint is in the preserved anchor floor
|
|
42
|
+
* (PREVENT-PI-001), so the edge documents a floor
|
|
43
|
+
* obligation and is never elided;
|
|
44
|
+
* - `contradiction` — a `contradicts` edge participates in resolution; the
|
|
45
|
+
* resolver reads it directly, so removing it would
|
|
46
|
+
* change the resolved set, not just the traversal;
|
|
47
|
+
* - `sole-dependency`— the ONLY edge requiring this prerequisite; removing it
|
|
48
|
+
* would drop the prerequisite from the closure;
|
|
49
|
+
* - `no-alternate-path` — a plain `depends` edge with no `a -> ... -> c`
|
|
50
|
+
* detour, so it still carries its own requirement.
|
|
51
|
+
*/
|
|
52
|
+
export type RetainReason =
|
|
53
|
+
| "tool-pair"
|
|
54
|
+
| "anchor"
|
|
55
|
+
| "contradiction"
|
|
56
|
+
| "sole-dependency"
|
|
57
|
+
| "no-alternate-path";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Why an edge was removed. Exactly one reason exists today — a redundant
|
|
61
|
+
* `depends` edge shadowed by a longer path — and it is spelled out rather than
|
|
62
|
+
* implied so a proof row can never be "removed, reason unstated"
|
|
63
|
+
* (HEAL-PROOF-003).
|
|
64
|
+
*/
|
|
65
|
+
export type RemoveReason = "transitively-implied";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One proof row: an edge the optimizer CONSIDERED, and what it decided.
|
|
69
|
+
*
|
|
70
|
+
* Every considered edge produces exactly one row — including retained ones. A
|
|
71
|
+
* proof that lists only removals cannot be replayed against the oracle, because
|
|
72
|
+
* the verifier could not distinguish "retained deliberately" from "never
|
|
73
|
+
* examined" (the `HEAL_PROOF_INCOMPLETE` injection).
|
|
74
|
+
*
|
|
75
|
+
* `via` names the intermediate vertex of the witnessing path `from -> via -> to`
|
|
76
|
+
* for a removal. It is REQUIRED on a removal row (the witness IS the reason) and
|
|
77
|
+
* absent on a retention row.
|
|
78
|
+
*/
|
|
79
|
+
export interface ClosureProofRow {
|
|
80
|
+
readonly from: string;
|
|
81
|
+
readonly to: string;
|
|
82
|
+
readonly kind: ClosureEdgeKind;
|
|
83
|
+
readonly decision: "retained" | "removed";
|
|
84
|
+
/** The stated reason. A removal without a reason fails verification. */
|
|
85
|
+
readonly reason: RetainReason | RemoveReason;
|
|
86
|
+
/** Witness vertex of the implying path (removals only). */
|
|
87
|
+
readonly via?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The optimization receipt (`ClosureProofV2`).
|
|
92
|
+
*
|
|
93
|
+
* `selected` is copied UNCHANGED from the conservative VC4C closure — the
|
|
94
|
+
* optimizer never edits it, and the verifier re-derives it independently to
|
|
95
|
+
* prove that. `retainedEdges`/`removedEdges` are the optimized traversal plan;
|
|
96
|
+
* `rows` is the full considered-edge ledger, sorted deterministically.
|
|
97
|
+
*
|
|
98
|
+
* `conservativeTraversals` / `optimizedTraversals` are the edge-walk counts of
|
|
99
|
+
* the conservative and reduced plans; the sprint's acceptance bar is a >=20%
|
|
100
|
+
* median reduction across the fixture corpus, measured from these two numbers.
|
|
101
|
+
*/
|
|
102
|
+
export interface ClosureProofV2 {
|
|
103
|
+
readonly schema: "closure-proof-v2";
|
|
104
|
+
readonly sessionId: string;
|
|
105
|
+
/** The closed selection, byte-identical to the conservative VC4C result. */
|
|
106
|
+
readonly selected: readonly string[];
|
|
107
|
+
/** Edges kept in the optimized traversal plan (sorted). */
|
|
108
|
+
readonly retainedEdges: readonly ClosureEdge[];
|
|
109
|
+
/** Edges dropped as transitively implied (sorted). */
|
|
110
|
+
readonly removedEdges: readonly ClosureEdge[];
|
|
111
|
+
/** One row per CONSIDERED edge — retained and removed alike (sorted). */
|
|
112
|
+
readonly rows: readonly ClosureProofRow[];
|
|
113
|
+
/** Edge walks the conservative plan performs. */
|
|
114
|
+
readonly conservativeTraversals: number;
|
|
115
|
+
/** Edge walks the optimized plan performs. */
|
|
116
|
+
readonly optimizedTraversals: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* `RestoreHintV1` — the reader-only hint VC6B consumes to fetch missing sources.
|
|
121
|
+
*
|
|
122
|
+
* A hint names a node the optimized plan still needs and the CHEAPEST retained
|
|
123
|
+
* edge that reaches it (`viaEdgeFrom`), so the restorer can walk one edge rather
|
|
124
|
+
* than re-deriving the closure. `protectedNode` marks a node whose edge set was
|
|
125
|
+
* retained for a hard-safety reason (tool pair / anchor), telling VC6B to treat
|
|
126
|
+
* it as non-negotiable.
|
|
127
|
+
*
|
|
128
|
+
* Reader-only by contract: node IDENTITY plus the traversal hint, NEVER source
|
|
129
|
+
* bytes or prompt text (SECURITY_PRIVACY — the exact ledger is not training
|
|
130
|
+
* data and is never rendered through a diagnostic surface).
|
|
131
|
+
*/
|
|
132
|
+
export interface RestoreHintV1 {
|
|
133
|
+
readonly schema: "restore-hint-v1";
|
|
134
|
+
readonly nodeId: string;
|
|
135
|
+
/** The retained edge's prerequisite endpoint, if the node has one. */
|
|
136
|
+
readonly viaEdgeFrom?: string;
|
|
137
|
+
/** True when the node is anchor-floor or tool-pair protected. */
|
|
138
|
+
readonly protectedNode: boolean;
|
|
139
|
+
/** Deterministic reason label mirroring the retention reason. */
|
|
140
|
+
readonly reason: RetainReason;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** VC6A failure codes (registered HEAL codes). */
|
|
144
|
+
export type HealFailureCode =
|
|
145
|
+
/** The replayed selected set diverges from the conservative oracle. */
|
|
146
|
+
| "HEAL_PROOF_SET_MISMATCH"
|
|
147
|
+
/** A considered edge has no proof row (or a removal states no reason). */
|
|
148
|
+
| "HEAL_PROOF_INCOMPLETE"
|
|
149
|
+
/** A proof row removes an edge that is protected and must be retained. */
|
|
150
|
+
| "HEAL_PROOF_PROTECTED_REMOVED"
|
|
151
|
+
/** A removal row's witness path `from -> via -> to` does not exist. */
|
|
152
|
+
| "HEAL_PROOF_WITNESS_INVALID"
|
|
153
|
+
/** The underlying conservative closure itself failed — nothing to optimize. */
|
|
154
|
+
| "HEAL_CLOSURE_REJECTED";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Verifier verdict. `ok:true` means the optimized plan is provably equivalent to
|
|
158
|
+
* the conservative closure and mode A may be used. `ok:false` routes to mode B
|
|
159
|
+
* (the conservative VC4C closure, unoptimized) — never to a partially reduced
|
|
160
|
+
* plan.
|
|
161
|
+
*/
|
|
162
|
+
export type ProofVerification =
|
|
163
|
+
| { readonly ok: true; readonly selected: readonly string[] }
|
|
164
|
+
| { readonly ok: false; readonly codes: readonly HealFailureCode[] };
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The triad mode VC6A selects (TRIAD_RESILIENCE).
|
|
168
|
+
*
|
|
169
|
+
* A = the optimized closure whose proof verified;
|
|
170
|
+
* B = the VC4C conservative closure, forced by proof rejection — an
|
|
171
|
+
* INDEPENDENT algorithm (fixed-point worklist, no reduction) reached by a
|
|
172
|
+
* different code path in `reconstruct/closure.ts`;
|
|
173
|
+
* C = the legacy prompt, forced when BOTH closure paths fail (an unresolved
|
|
174
|
+
* contradiction or a structurally invalid graph), stating its loss of old
|
|
175
|
+
* semantic context.
|
|
176
|
+
*/
|
|
177
|
+
export type HealMode = "A" | "B" | "C";
|
|
178
|
+
|
|
179
|
+
/** The outcome of the VC6A triad selection. */
|
|
180
|
+
export interface HealTriadOutcome {
|
|
181
|
+
readonly mode: HealMode;
|
|
182
|
+
/** The verified optimized proof (mode A only). */
|
|
183
|
+
readonly proof: ClosureProofV2 | null;
|
|
184
|
+
/** The selected node set actually used (empty only in mode C). */
|
|
185
|
+
readonly selected: readonly string[];
|
|
186
|
+
readonly codes: readonly HealFailureCode[];
|
|
187
|
+
/** Set only in mode C (TRIAD_RESILIENCE — "C states its semantic loss"). */
|
|
188
|
+
readonly semanticLossStated: boolean;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The two structured events the VC6A reporter emits. */
|
|
192
|
+
export type HealEventName =
|
|
193
|
+
| "vector_cortex_closure_optimized"
|
|
194
|
+
| "vector_cortex_closure_proof_rejected";
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Aggregate-only closure-proof metrics for the dashboard (counts only, never
|
|
198
|
+
* node ids, edges, or source payloads).
|
|
199
|
+
*/
|
|
200
|
+
export interface HealMetricsV1 {
|
|
201
|
+
readonly optimizations: number;
|
|
202
|
+
readonly proofRejections: number;
|
|
203
|
+
readonly retainedEdgeTotal: number;
|
|
204
|
+
readonly removedEdgeTotal: number;
|
|
205
|
+
readonly conservativeTraversalTotal: number;
|
|
206
|
+
readonly optimizedTraversalTotal: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Registered HEAL conformance ID range (HEAL-001..015). The acceptance test
|
|
211
|
+
* reads these rows from the v2 manifest and asserts each returns its manifest
|
|
212
|
+
* `ok`/`code`.
|
|
213
|
+
*/
|
|
214
|
+
export const HEAL_IDS: readonly string[] = Array.from(
|
|
215
|
+
{ length: 15 },
|
|
216
|
+
(_v, i) => `HEAL-${String(i + 1).padStart(3, "0")}`,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
/** Named VC6A conformance assertions (the sprint's headline rows). */
|
|
220
|
+
export const HEAL_NAMED_IDS = [
|
|
221
|
+
"HEAL-REDUCE-001",
|
|
222
|
+
"HEAL-PROTECT-002",
|
|
223
|
+
"HEAL-PROOF-003",
|
|
224
|
+
] as const;
|
|
225
|
+
|
|
226
|
+
export type { ClosureEdge, ClosureEdgeKind };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* add-l0.ts — the L0 exact-match tier of the dedup cascade.
|
|
3
|
+
*
|
|
4
|
+
* L0 is three exact-hash probes sharing one enable flag and one MARK_ONLY flag:
|
|
5
|
+
* 1. contentHash — normalized dual-hash, bloom-accelerated (Sprint 9/10)
|
|
6
|
+
* 2. regionHash — the legacy pre-normalization hash (backward-compat)
|
|
7
|
+
* 3. summaryHash — same-topic incremental compactions
|
|
8
|
+
*
|
|
9
|
+
* Split out of add.ts so both files stay under the 300-line src/ soft limit.
|
|
10
|
+
* The tier is a pure decision: it either produces a collapse (returning the
|
|
11
|
+
* matched result) or reports the fall-through, and it never writes a new
|
|
12
|
+
* checkpoint — that stays in add.ts.
|
|
13
|
+
*
|
|
14
|
+
* All three probes are hash comparisons, so none of them computes a similarity
|
|
15
|
+
* score and none is invented for the audit trail.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import type { StoredCheckpoint } from "../store.js";
|
|
19
|
+
import type { ContentDigest } from "../dedup/digest.js";
|
|
20
|
+
import { upsertCheckpoint, addTokensSaved, bumpDedupStats } from "../store/sqlite.js";
|
|
21
|
+
import type { BloomFilter } from "../store/bloom.js";
|
|
22
|
+
import type { DedupAuditRecorder } from "./dedup-audit.js";
|
|
23
|
+
import type { AddInput, AddResult } from "./types.js";
|
|
24
|
+
import type { VectorStore } from "./class.js";
|
|
25
|
+
|
|
26
|
+
/** Everything the L0 tier needs from the enclosing add() cascade. */
|
|
27
|
+
export interface L0Context {
|
|
28
|
+
store: VectorStore;
|
|
29
|
+
input: AddInput;
|
|
30
|
+
/** Checkpoints already stored for this session. */
|
|
31
|
+
all: StoredCheckpoint[];
|
|
32
|
+
/** Normalized content digest of the incoming region. */
|
|
33
|
+
digest: ContentDigest;
|
|
34
|
+
/** Legacy region hash of the incoming region. */
|
|
35
|
+
regionHash: string;
|
|
36
|
+
/** SHA-256 of the topic summary, when one was supplied. */
|
|
37
|
+
summaryHash?: string;
|
|
38
|
+
/** Tokens the original region occupied (the dedup "saved" base). */
|
|
39
|
+
origTokens: number;
|
|
40
|
+
/** Warm bloom accelerator for the content-hash probe. */
|
|
41
|
+
bloom: BloomFilter;
|
|
42
|
+
/** Cascade start time, for the monitoring latency figure. */
|
|
43
|
+
t0: number;
|
|
44
|
+
audit: DedupAuditRecorder;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Outcome of the L0 tier.
|
|
49
|
+
*
|
|
50
|
+
* `result` set → the tier collapsed the region and add() returns immediately.
|
|
51
|
+
* `markOnly` set → a probe matched but MARK_ONLY policy said store anyway, so
|
|
52
|
+
* the cascade continues and records the mark at the end.
|
|
53
|
+
*/
|
|
54
|
+
export interface L0Outcome {
|
|
55
|
+
result?: AddResult;
|
|
56
|
+
markOnly: "L0" | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Run the L0 exact-match tier. */
|
|
60
|
+
export function runL0Tier(ctx: L0Context): L0Outcome {
|
|
61
|
+
const { store, input, all, digest, regionHash, origTokens, bloom, t0, audit } = ctx;
|
|
62
|
+
const cfg = store.cfg;
|
|
63
|
+
const onTier = input.onTier;
|
|
64
|
+
let markOnly: "L0" | null = null;
|
|
65
|
+
|
|
66
|
+
// 0. Content-hash dedup (Sprint 9) — catches identical content arriving under
|
|
67
|
+
// different regionText. Normalization handles case/whitespace/ANSI so
|
|
68
|
+
// variants collapse to one row. Dual-hash guards a single-hash collision.
|
|
69
|
+
// Sprint 10: bloom is the accelerator — a miss means "definitely new" and
|
|
70
|
+
// skips the scan; a hit is only a candidate, confirmed against `all` here.
|
|
71
|
+
// MARK_ONLY_L0 records the decision but does not collapse.
|
|
72
|
+
onTier?.({ tier: "L0", status: "scanning" });
|
|
73
|
+
if (cfg.L0_ENABLED && bloom.maybeHas(digest.contentHash)) {
|
|
74
|
+
const contentMatch = all.find(
|
|
75
|
+
(cp) =>
|
|
76
|
+
cp.contentHash === digest.contentHash &&
|
|
77
|
+
cp.contentHash2 === digest.contentHash2,
|
|
78
|
+
);
|
|
79
|
+
if (contentMatch) {
|
|
80
|
+
if (cfg.MARK_ONLY_L0) {
|
|
81
|
+
markOnly = "L0"; // Record-but-don't-collapse: fall through.
|
|
82
|
+
} else {
|
|
83
|
+
contentMatch.timestamp = input.timestamp;
|
|
84
|
+
upsertCheckpoint(contentMatch, store.stateDir);
|
|
85
|
+
bumpDedupStats(true, store.stateDir);
|
|
86
|
+
// Deduped: whole original region discarded, nothing new stored.
|
|
87
|
+
addTokensSaved(origTokens, store.stateDir);
|
|
88
|
+
store.record("L0", "deduped", "contentHash", Date.now() - t0, 1, contentMatch.checkpointId);
|
|
89
|
+
audit.deduped("L0", contentMatch.checkpointId, "contentHash");
|
|
90
|
+
onTier?.({ tier: "L0", status: "deduped", detail: "contentHash" });
|
|
91
|
+
return {
|
|
92
|
+
result: { checkpoint: contentMatch, deduped: true, reason: "contentHash" },
|
|
93
|
+
markOnly,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 1. Legacy regionHash dedup (backward-compat) — part of L0 tier gating.
|
|
100
|
+
if (cfg.L0_ENABLED) {
|
|
101
|
+
const regionMatch = all.find((cp) => cp.regionHash === regionHash);
|
|
102
|
+
if (regionMatch) {
|
|
103
|
+
if (cfg.MARK_ONLY_L0) {
|
|
104
|
+
markOnly = "L0"; // fall through
|
|
105
|
+
} else {
|
|
106
|
+
bumpDedupStats(true, store.stateDir);
|
|
107
|
+
// Deduped: whole original region discarded, nothing new stored.
|
|
108
|
+
addTokensSaved(origTokens, store.stateDir);
|
|
109
|
+
store.record("L0", "deduped", "regionHash", Date.now() - t0, 1, regionMatch.checkpointId);
|
|
110
|
+
audit.deduped("L0", regionMatch.checkpointId, "regionHash");
|
|
111
|
+
onTier?.({ tier: "L0", status: "deduped", detail: "regionHash" });
|
|
112
|
+
return {
|
|
113
|
+
result: { checkpoint: regionMatch, deduped: true, reason: "regionHash" },
|
|
114
|
+
markOnly,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. SummaryHash dedup — catches same-topic incremental compactions.
|
|
121
|
+
if (ctx.summaryHash && cfg.L0_ENABLED) {
|
|
122
|
+
const summaryMatch = all.find((cp) => cp.summaryHash === ctx.summaryHash);
|
|
123
|
+
if (summaryMatch) {
|
|
124
|
+
if (cfg.MARK_ONLY_L0) {
|
|
125
|
+
markOnly = "L0"; // fall through
|
|
126
|
+
} else {
|
|
127
|
+
summaryMatch.timestamp = input.timestamp;
|
|
128
|
+
upsertCheckpoint(summaryMatch, store.stateDir);
|
|
129
|
+
bumpDedupStats(true, store.stateDir);
|
|
130
|
+
// Deduped: whole original region discarded, nothing new stored.
|
|
131
|
+
addTokensSaved(origTokens, store.stateDir);
|
|
132
|
+
store.record("L0", "deduped", "summaryHash", Date.now() - t0, 1, summaryMatch.checkpointId);
|
|
133
|
+
audit.deduped("L0", summaryMatch.checkpointId, "summaryHash");
|
|
134
|
+
onTier?.({ tier: "L0", status: "deduped", detail: "summaryHash" });
|
|
135
|
+
return {
|
|
136
|
+
result: { checkpoint: summaryMatch, deduped: true, reason: "summaryHash" },
|
|
137
|
+
markOnly,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// L0 did not collapse this region.
|
|
143
|
+
onTier?.({ tier: "L0", status: "passed" });
|
|
144
|
+
return { markOnly };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** SHA-256 of the topic summary (full 64-hex; 16-hex was collision-prone). */
|
|
148
|
+
export function computeSummaryHash(topicSummary?: string): string | undefined {
|
|
149
|
+
return topicSummary
|
|
150
|
+
? createHash("sha256").update(topicSummary).digest("hex")
|
|
151
|
+
: undefined;
|
|
152
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* add-l1.ts — the L1 MinHash/LSH near-duplicate lookup.
|
|
3
|
+
*
|
|
4
|
+
* Split out of add.ts to keep both files under the 300-line src/ soft limit.
|
|
5
|
+
* L1 catches the one-word edits and rewordings L0's exact hashes miss: a cheap
|
|
6
|
+
* LSH bucket retrieval narrows the field, then a trigram verification
|
|
7
|
+
* (pg_trgm-equivalent) is the final gate. The verify is boolean, so the tier
|
|
8
|
+
* produces no similarity score.
|
|
9
|
+
*/
|
|
10
|
+
import type { StoredCheckpoint } from "../store.js";
|
|
11
|
+
import {
|
|
12
|
+
minhashSignature,
|
|
13
|
+
SIGNATURE_VERSION,
|
|
14
|
+
NUM_HASHES,
|
|
15
|
+
} from "../dedup/l1-minhash.js";
|
|
16
|
+
import { lshBands } from "../dedup/l1-lsh.js";
|
|
17
|
+
import { isNearDuplicate } from "../dedup/l1-verify.js";
|
|
18
|
+
import { lshCandidateChunks } from "../store/sqlite.js";
|
|
19
|
+
import type { VectorStore } from "./class.js";
|
|
20
|
+
|
|
21
|
+
export /**
|
|
22
|
+
* L1 near-duplicate lookup: MinHash → LSH candidate retrieval → trigram verify.
|
|
23
|
+
* Returns the matching checkpoint or undefined. Bounded by a 100-candidate cap
|
|
24
|
+
* and a 20ms verify budget (QA #7/#15) so it never hangs a large session.
|
|
25
|
+
*/
|
|
26
|
+
function findL1Duplicate(
|
|
27
|
+
store: VectorStore,
|
|
28
|
+
sessionId: string,
|
|
29
|
+
regionText: string,
|
|
30
|
+
all: StoredCheckpoint[],
|
|
31
|
+
): StoredCheckpoint | undefined {
|
|
32
|
+
if (all.length === 0) return undefined;
|
|
33
|
+
const sig = minhashSignature(regionText);
|
|
34
|
+
if (sig.length !== NUM_HASHES) return undefined;
|
|
35
|
+
const bands = lshBands(sig, sessionId, SIGNATURE_VERSION);
|
|
36
|
+
// Cheap candidate retrieval (single query, capped). Exclude nothing yet —
|
|
37
|
+
// the new checkpoint has no id, so pass a sentinel that never matches.
|
|
38
|
+
const candidateIds = lshCandidateChunks(
|
|
39
|
+
bands,
|
|
40
|
+
sessionId,
|
|
41
|
+
"__new__",
|
|
42
|
+
store.stateDir,
|
|
43
|
+
100,
|
|
44
|
+
);
|
|
45
|
+
if (candidateIds.length === 0) return undefined;
|
|
46
|
+
const byId = new Map(all.map((cp) => [cp.checkpointId, cp]));
|
|
47
|
+
const VERIFY_BUDGET_MS = 20;
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
for (const id of candidateIds) {
|
|
50
|
+
if (Date.now() - start > VERIFY_BUDGET_MS) break; // QA #15: abort → "not dup"
|
|
51
|
+
const cand = byId.get(id);
|
|
52
|
+
if (!cand) continue;
|
|
53
|
+
const candText = cand.normalizedText ?? cand.summary ?? "";
|
|
54
|
+
if (isNearDuplicate(regionText, candText)) return cand;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|