pi-mega-compact 0.16.3 → 0.17.1
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 +91 -0
- package/dist/dedup/digest.js +30 -0
- package/dist/dedup/l1-lsh-v2.js +60 -0
- package/dist/dedup/l1-lsh.js +52 -0
- package/dist/dedup/l1-minhash-v2.js +193 -0
- package/dist/dedup/l1-minhash.js +91 -0
- package/dist/dedup/l1-verify.js +54 -0
- package/dist/dedup/mmr.js +45 -0
- package/dist/dedup/normalize.js +39 -0
- package/dist/dedup/raptor/buildHistory.js +164 -0
- package/dist/dedup/raptor/guardrails.js +83 -0
- package/dist/dedup/raptor/incremental.js +304 -0
- package/dist/dedup/raptor/index.js +190 -0
- package/dist/dedup/raptor/kmeans.js +152 -0
- package/dist/dedup/raptor/multilevel.js +184 -0
- package/dist/dedup/raptor/retrieval.js +93 -0
- package/dist/dedup/raptor/summarizer.js +86 -0
- package/dist/dedup/raptor/tree.js +202 -0
- package/dist/dedup/topk.js +60 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry-ext.js +30 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +4 -14
- package/dist/extensions/dashboard-server/api-contracts/vector-cortex.js +12 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +11 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +292 -0
- package/dist/extensions/dashboard-server/routes.js +1 -0
- package/dist/extensions/dashboard-server/server.js +9 -1
- package/dist/extensions/mega-events/context-handler.js +11 -0
- package/dist/extensions/mega-runtime/vector-cortex-ledger.js +131 -0
- package/dist/extensions/mega-runtime/vector-cortex-safety.js +100 -0
- package/dist/src/config/vector-cortex.js +91 -0
- package/dist/src/config.js +6 -0
- package/dist/src/dedup/l1-lsh-v2.js +60 -0
- package/dist/src/dedup/l1-minhash-v2.js +193 -0
- package/dist/src/dedup/normalize.js +1 -1
- package/dist/src/vector-cortex/conformance/emit.js +46 -0
- package/dist/src/vector-cortex/conformance/manifest.js +235 -0
- package/dist/src/vector-cortex/conformance/runner.js +111 -0
- package/dist/src/vector-cortex/conformance/triadB-reader.js +95 -0
- package/dist/src/vector-cortex/eval/annotations.js +41 -0
- package/dist/src/vector-cortex/eval/metrics.js +86 -0
- package/dist/src/vector-cortex/eval/observer.js +61 -0
- package/dist/src/vector-cortex/eval/persist.js +67 -0
- package/dist/src/vector-cortex/eval/reader.js +29 -0
- package/dist/src/vector-cortex/eval/types.js +33 -0
- package/dist/src/vector-cortex/ledger/adapter.js +55 -0
- package/dist/src/vector-cortex/ledger/compat-journal.js +206 -0
- package/dist/src/vector-cortex/ledger/emit.js +53 -0
- package/dist/src/vector-cortex/ledger/event-codec.js +72 -0
- package/dist/src/vector-cortex/ledger/event-codecB.js +76 -0
- package/dist/src/vector-cortex/ledger/sqlite.js +200 -0
- package/dist/src/vector-cortex/ledger/store.js +184 -0
- package/dist/src/vector-cortex/ledger/types.js +52 -0
- package/dist/src/vector-cortex/ledger/validator.js +117 -0
- package/dist/src/vector-cortex/migrations/effective-cut-v2.js +103 -0
- package/dist/src/vector-cortex/migrations/minhash-v2.js +209 -0
- package/dist/src/vector-cortex/migrations/occurrence-v2.js +139 -0
- package/dist/src/vector-cortex/replay/cut.js +123 -0
- package/dist/src/vector-cortex/replay/emit.js +58 -0
- package/dist/src/vector-cortex/replay/replay.js +260 -0
- package/dist/src/vector-cortex/replay/replayB.js +73 -0
- package/dist/src/vector-cortex/replay/types.js +52 -0
- package/dist/src/vector-cortex/resilience/breaker-core.js +359 -0
- package/dist/src/vector-cortex/resilience/breaker.js +12 -0
- package/dist/src/vector-cortex/resilience/emit.js +56 -0
- package/dist/src/vector-cortex/resilience/spool-core.js +347 -0
- package/dist/src/vector-cortex/resilience/spool.js +12 -0
- package/dist/src/vector-cortex/resilience/types.js +32 -0
- package/dist/vector-cortex/conformance/emit.js +46 -0
- package/dist/vector-cortex/conformance/manifest.js +235 -0
- package/dist/vector-cortex/conformance/runner.js +111 -0
- package/dist/vector-cortex/conformance/triadB-reader.js +95 -0
- package/dist/vector-cortex/eval/annotations.js +41 -0
- package/dist/vector-cortex/eval/metrics.js +86 -0
- package/dist/vector-cortex/eval/observer.js +61 -0
- package/dist/vector-cortex/eval/persist.js +67 -0
- package/dist/vector-cortex/eval/reader.js +29 -0
- package/dist/vector-cortex/eval/types.js +33 -0
- package/dist/vector-cortex/ledger/adapter.js +55 -0
- package/dist/vector-cortex/ledger/compat-journal.js +206 -0
- package/dist/vector-cortex/ledger/emit.js +53 -0
- package/dist/vector-cortex/ledger/event-codec.js +72 -0
- package/dist/vector-cortex/ledger/event-codecB.js +76 -0
- package/dist/vector-cortex/ledger/sqlite.js +200 -0
- package/dist/vector-cortex/ledger/store.js +184 -0
- package/dist/vector-cortex/ledger/types.js +52 -0
- package/dist/vector-cortex/ledger/validator.js +117 -0
- package/dist/vector-cortex/migrations/effective-cut-v2.js +103 -0
- package/dist/vector-cortex/migrations/minhash-v2.js +209 -0
- package/dist/vector-cortex/migrations/occurrence-v2.js +139 -0
- package/dist/vector-cortex/replay/cut.js +123 -0
- package/dist/vector-cortex/replay/emit.js +58 -0
- package/dist/vector-cortex/replay/replay.js +260 -0
- package/dist/vector-cortex/replay/replayB.js +73 -0
- package/dist/vector-cortex/replay/types.js +52 -0
- package/dist/vector-cortex/resilience/breaker-core.js +359 -0
- package/dist/vector-cortex/resilience/breaker.js +12 -0
- package/dist/vector-cortex/resilience/emit.js +56 -0
- package/dist/vector-cortex/resilience/spool-core.js +347 -0
- package/dist/vector-cortex/resilience/spool.js +12 -0
- package/dist/vector-cortex/resilience/types.js +32 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-Y_X0Bbjd.js → AreaChart-BQXFF7UI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-Y_X0Bbjd.js.map → AreaChart-BQXFF7UI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-B9XDb-Ob.js → BarChart-BVtoz9Eg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-B9XDb-Ob.js.map → BarChart-BVtoz9Eg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-BUGVb1G1.js → CacheTab-trI2TIGy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-BUGVb1G1.js.map → CacheTab-trI2TIGy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-dOqLYTsy.js → EventsTab-CNUbk9__.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-dOqLYTsy.js.map → EventsTab-CNUbk9__.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-c2vezKg1.js → HealthTab-K-bd_CM-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-c2vezKg1.js.map → HealthTab-K-bd_CM-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-bQYHIcdx.js → MaintenanceTab-CoEioKzx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-bQYHIcdx.js.map → MaintenanceTab-CoEioKzx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-2L8pH6y2.js → MemoryMapTab-CJo24ulv.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-2L8pH6y2.js.map → MemoryMapTab-CJo24ulv.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CzUx1jpo.js → MetricsTab-DCNlQWWP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CzUx1jpo.js.map → MetricsTab-DCNlQWWP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-eCNoaclv.js → OverviewTab-CQQlyIQ6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-eCNoaclv.js.map → OverviewTab-CQQlyIQ6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-tb0IW3NF.js → ReposTab-Cqm82_yn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-tb0IW3NF.js.map → ReposTab-Cqm82_yn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C-IoLw5h.js → SessionsTab-A8xTAdET.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C-IoLw5h.js.map → SessionsTab-A8xTAdET.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-BvmFi8gf.js → SetupTab-CqaSgaZl.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-BvmFi8gf.js.map → SetupTab-CqaSgaZl.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DBH-cOlA.js → TimeSavedCard-C_f3q8GR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DBH-cOlA.js.map → TimeSavedCard-C_f3q8GR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DBMgmvnX.js → TurnsTab-BEsafACo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DBMgmvnX.js.map → TurnsTab-BEsafACo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-1LcQrXk3.js → WikiTab-Bf01M2_Z.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-1LcQrXk3.js.map → WikiTab-Bf01M2_Z.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-DtEsZTUE.js → button-BEJPTk_E.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-DtEsZTUE.js.map → button-BEJPTk_E.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-DcfTmw-4.js → card-DK1V382s.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-DcfTmw-4.js.map → card-DK1V382s.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BsIxUDyw.js → generateCategoricalChart-CRtaPphL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BsIxUDyw.js.map → generateCategoricalChart-CRtaPphL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-B93cewiX.js → index-CB6BslE4.js} +34 -29
- package/extensions/dashboard-client/dist/assets/index-CB6BslE4.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/index-CfvziyBi.css +1 -0
- package/extensions/dashboard-client/dist/assets/{switch-Bowd7jCD.js → switch-C4FV6B9k.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-Bowd7jCD.js.map → switch-C4FV6B9k.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-Bo7kJZ4u.js → toggle-GBmeENMd.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-Bo7kJZ4u.js.map → toggle-GBmeENMd.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-zNhb0c3u.js → useSSE-BOKZGSn1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-zNhb0c3u.js.map → useSSE-BOKZGSn1.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/App.tsx +2 -0
- package/extensions/dashboard-client/src/api/vector-cortex.ts +54 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +272 -0
- package/extensions/dashboard-client/src/tabs/registry.ts +4 -1
- package/extensions/dashboard-client/src/types/vector-cortex.ts +84 -0
- package/extensions/dashboard-client/tsconfig.json +1 -1
- package/extensions/dashboard-server/api-contracts/endpoints/registry-ext.ts +67 -0
- package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +4 -21
- package/extensions/dashboard-server/api-contracts/index.ts +3 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +136 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +41 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +332 -0
- package/extensions/dashboard-server/routes.ts +6 -0
- package/extensions/dashboard-server/server.ts +8 -0
- package/extensions/mega-events/context-handler.ts +16 -0
- package/extensions/mega-runtime/vector-cortex-ledger.ts +165 -0
- package/extensions/mega-runtime/vector-cortex-safety.ts +181 -0
- package/package.json +2 -1
- package/src/config/vector-cortex.ts +98 -0
- package/src/config.ts +28 -0
- package/src/dedup/l1-lsh-v2.ts +78 -0
- package/src/dedup/l1-minhash-v2.ts +221 -0
- package/src/dedup/normalize.ts +1 -1
- package/src/store/sqlite/game-scores.ts +2 -1
- package/src/vector-cortex/conformance/emit.ts +56 -0
- package/src/vector-cortex/conformance/manifest.ts +298 -0
- package/src/vector-cortex/conformance/runner.ts +165 -0
- package/src/vector-cortex/conformance/triadB-reader.ts +110 -0
- package/src/vector-cortex/eval/annotations.ts +70 -0
- package/src/vector-cortex/eval/metrics.ts +113 -0
- package/src/vector-cortex/eval/observer.ts +86 -0
- package/src/vector-cortex/eval/persist.ts +73 -0
- package/src/vector-cortex/eval/reader.ts +45 -0
- package/src/vector-cortex/eval/types.ts +89 -0
- package/src/vector-cortex/ledger/adapter.ts +71 -0
- package/src/vector-cortex/ledger/compat-journal.ts +234 -0
- package/src/vector-cortex/ledger/emit.ts +86 -0
- package/src/vector-cortex/ledger/event-codec.ts +79 -0
- package/src/vector-cortex/ledger/event-codecB.ts +87 -0
- package/src/vector-cortex/ledger/sqlite.ts +265 -0
- package/src/vector-cortex/ledger/store.ts +380 -0
- package/src/vector-cortex/ledger/types.ts +141 -0
- package/src/vector-cortex/ledger/validator.ts +124 -0
- package/src/vector-cortex/migrations/effective-cut-v2.ts +149 -0
- package/src/vector-cortex/migrations/minhash-v2.ts +263 -0
- package/src/vector-cortex/migrations/occurrence-v2.ts +192 -0
- package/src/vector-cortex/replay/cut.ts +169 -0
- package/src/vector-cortex/replay/emit.ts +89 -0
- package/src/vector-cortex/replay/replay.ts +308 -0
- package/src/vector-cortex/replay/replayB.ts +97 -0
- package/src/vector-cortex/replay/types.ts +145 -0
- package/src/vector-cortex/resilience/breaker-core.ts +444 -0
- package/src/vector-cortex/resilience/breaker.ts +22 -0
- package/src/vector-cortex/resilience/emit.ts +99 -0
- package/src/vector-cortex/resilience/spool-core.ts +411 -0
- package/src/vector-cortex/resilience/spool.ts +22 -0
- package/src/vector-cortex/resilience/types.ts +158 -0
- package/extensions/dashboard-client/dist/assets/index-B93cewiX.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-C_tCbOI0.css +0 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/vector-cortex.ts — vector-cortex feature flags + breaker/triad constants.
|
|
3
|
+
*
|
|
4
|
+
* Every sprint ships one positive `MEGACOMPACT_<SPRINT>` flag, default ON and
|
|
5
|
+
* `=0`/`_DISABLED` off. Flag-OFF is byte-identical to the predecessor sprint's
|
|
6
|
+
* behavior (for VC0A: mode C — observer absent, zero evaluation writes).
|
|
7
|
+
*
|
|
8
|
+
* The breaker/triad constants (TRIAD_RESILIENCE.md) live here so VC0C consumes
|
|
9
|
+
* them without re-declaring the ownership boundary. Pi-agnostic, dependency-free.
|
|
10
|
+
*/
|
|
11
|
+
/** Positive sprint flag: `=0` or `_DISABLED=true` disables (default ON). */
|
|
12
|
+
function sprintFlag(name) {
|
|
13
|
+
const v = process.env[name];
|
|
14
|
+
if (v === "0" || v === "false")
|
|
15
|
+
return false;
|
|
16
|
+
const disabled = process.env[name + "_DISABLED"];
|
|
17
|
+
if (disabled === "true" || disabled === "1")
|
|
18
|
+
return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
/** VC0A — baseline observability (MetricEventV1 / AnnotationV1). Default ON. */
|
|
22
|
+
export const VC0A_ENABLED = () => sprintFlag("MEGACOMPACT_VC0A");
|
|
23
|
+
/**
|
|
24
|
+
* VC0B — replay correctness (ReplayCutV2 / ReplayReportV2, M3 effective-cut-v2).
|
|
25
|
+
* Default ON. `MEGACOMPACT_VC0B=0` disables and is byte-identical to the
|
|
26
|
+
* predecessor (legacy capped-replay behavior preserved; the v2 cut/replay is
|
|
27
|
+
* only consulted on the vector-cortex path).
|
|
28
|
+
*/
|
|
29
|
+
export const VC0B_ENABLED = () => sprintFlag("MEGACOMPACT_VC0B");
|
|
30
|
+
/**
|
|
31
|
+
* VC1A — canonical byte events (EventV2 / EventCodec).
|
|
32
|
+
* Default ON. `MEGACOMPACT_VC1A=0` disables and is byte-identical to the
|
|
33
|
+
* predecessor (mode C: ledger absent, current transcript codec unchanged).
|
|
34
|
+
* The single real consumer is the ledger emit seam (`ledger/emit.ts`): flag OFF
|
|
35
|
+
* gates zero observability writes.
|
|
36
|
+
*/
|
|
37
|
+
export const VC1A_ENABLED = () => sprintFlag("MEGACOMPACT_VC1A");
|
|
38
|
+
/**
|
|
39
|
+
* VC1B — occurrence ledger + tool identity + compat journal (LedgerReader/
|
|
40
|
+
* Writer/Admin, CompatJournalV1, M2 occurrence-v2 migration).
|
|
41
|
+
* Default ON. `MEGACOMPACT_VC1B=0` disables and is byte-identical to the
|
|
42
|
+
* predecessor (mode C: the neutral occurrence ledger is not written, no
|
|
43
|
+
* journal rows, zero `vector_cortex_occurrence_appended` emissions). The real
|
|
44
|
+
* consumers are the ledger write integration seam and the compat-journal
|
|
45
|
+
* switch seam.
|
|
46
|
+
*/
|
|
47
|
+
export const VC1B_ENABLED = () => sprintFlag("MEGACOMPACT_VC1B");
|
|
48
|
+
/**
|
|
49
|
+
* VC0C — live safety envelope (TriadResult / Breaker / KillDecision + durable
|
|
50
|
+
* spool). Default ON. `MEGACOMPACT_VC0C=0` disables and is byte-identical to
|
|
51
|
+
* the predecessor (mode C: selected before provider invocation, unchanged host
|
|
52
|
+
* transcript, breaker/spool idle and emitting nothing). The single real
|
|
53
|
+
* consumer is the resilience emit seam + the safety adapter's triad selection.
|
|
54
|
+
*/
|
|
55
|
+
export const VC0C_ENABLED = () => sprintFlag("MEGACOMPACT_VC0C");
|
|
56
|
+
/**
|
|
57
|
+
* VC1C — cross-language conformance v2 (FixtureManifestV2 / DowngradeReport /
|
|
58
|
+
* MinHashV2 + M4 minhash-v2 migration).
|
|
59
|
+
* Default ON. `MEGACOMPACT_VC1C=0` disables and is byte-identical to the
|
|
60
|
+
* predecessor (mode C: a v2 conformance runner that accepts authority fixtures
|
|
61
|
+
* and the manifest validator idle; the sync dedup scan stays on the v1 path;
|
|
62
|
+
* zero `vector_cortex_*` VC1C emissions). The real consumers are the conformance
|
|
63
|
+
* emit seam, the minhash-v2 backfill seam and the downgrade-export seam.
|
|
64
|
+
*/
|
|
65
|
+
export const VC1C_ENABLED = () => sprintFlag("MEGACOMPACT_VC1C");
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Breaker state machine constants (TRIAD_RESILIENCE.md §breaker).
|
|
68
|
+
// Rolled numbers for one 60s window; VC0C consumes these at its breaker seam.
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
/** Rolling eligibility window (milliseconds). */
|
|
71
|
+
export const BREAKER_WINDOW_MS = 60_000;
|
|
72
|
+
/** Minimum attempts before a breaker may trip or promote. */
|
|
73
|
+
export const BREAKER_MIN_ATTEMPTS = 20;
|
|
74
|
+
/** Performance trip: ≥ this many failures in window, or ≥ this fraction. */
|
|
75
|
+
export const BREAKER_PERF_FAILURES = 5;
|
|
76
|
+
export const BREAKER_PERF_FAILURE_RATE = 0.1;
|
|
77
|
+
/** Correctness trip trips on the first correctness failure. */
|
|
78
|
+
export const BREAKER_CORRECTNESS_FAILURES = 1;
|
|
79
|
+
/** Cooldown before an open breaker may probe (milliseconds). */
|
|
80
|
+
export const BREAKER_COOLDOWN_MS = 30_000;
|
|
81
|
+
/** Consecutive successful probes required to advance a state. */
|
|
82
|
+
export const BREAKER_PROBE_COUNT = 3;
|
|
83
|
+
/** Exponential retry base: 30s * 2^attempt, capped, ±10% jitter. */
|
|
84
|
+
export const BREAKER_RETRY_BASE_MS = 30_000;
|
|
85
|
+
export const BREAKER_RETRY_CAP_MS = 15 * 60_000;
|
|
86
|
+
export const BREAKER_RETRY_JITTER = 0.1;
|
|
87
|
+
/** Promotion hysteresis: failure rate must be < this and p95 within budget. */
|
|
88
|
+
export const BREAKER_HYSTERESIS_FAILURE_RATE = 0.02;
|
|
89
|
+
export const BREAKER_HYSTERESIS_BUDGET_P95_MS = 50;
|
|
90
|
+
/** Minimum healthy residence before a further promotion (milliseconds). */
|
|
91
|
+
export const BREAKER_MIN_HEALTHY_RESIDENCE_MS = 5 * 60_000;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* digest.ts — content-addressable digest (Sprint 9).
|
|
3
|
+
*
|
|
4
|
+
* Dual-hash design (QA #2 spirit, local): a primary + a secondary hash guard
|
|
5
|
+
* against a single-hash collision silently merging distinct content. The L0 dedup
|
|
6
|
+
* key is `(content_hash, content_hash2)` — both must agree to declare a duplicate.
|
|
7
|
+
*
|
|
8
|
+
* `content_hash` is the full 64-hex SHA-256 of the normalized text. `content_hash2`
|
|
9
|
+
* is a second independent view (SHA-256 of the reversed normalized text) so a
|
|
10
|
+
* collision on one alone does not dedup. `content_hash_version` lets Sprint 11/12
|
|
11
|
+
* plug in stronger digests later without breaking old rows.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { normalize } from "./normalize.js";
|
|
15
|
+
export const CONTENT_HASH_VERSION = 1;
|
|
16
|
+
/** Compute the canonical content digest for a (possibly raw) region text. */
|
|
17
|
+
export function computeContentDigest(text) {
|
|
18
|
+
const normalizedText = normalize(text);
|
|
19
|
+
const contentHash = createHash("sha256").update(normalizedText).digest("hex");
|
|
20
|
+
// Secondary: hash the reversed normalized string so it's an independent view.
|
|
21
|
+
const contentHash2 = createHash("sha256")
|
|
22
|
+
.update(normalizedText.split("").reverse().join(""))
|
|
23
|
+
.digest("hex");
|
|
24
|
+
return {
|
|
25
|
+
contentHash,
|
|
26
|
+
contentHash2,
|
|
27
|
+
contentHashVersion: CONTENT_HASH_VERSION,
|
|
28
|
+
normalizedText,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* l1-lsh-v2.ts — Locality-Sensitive Hashing banding over MinHashV2 signatures
|
|
3
|
+
* (VC1C, M4 minhash-v2).
|
|
4
|
+
*
|
|
5
|
+
* Splits the frozen 2048-byte v2 signature (256 x u64 LE slots) into 64 bands
|
|
6
|
+
* of FOUR u64 values each. Each band yields a deterministic bucket key derived
|
|
7
|
+
* from its 4 u64 little-endian bytes (32 bytes) hashed with a 64-bit FNV-1a,
|
|
8
|
+
* scoped by session id and the frozen v2 version tag so buckets never collide
|
|
9
|
+
* across sessions or the v1 path. Deterministic: same (sessionId,
|
|
10
|
+
* signatureBytes) -> same 64 bucket keys, every run, every language.
|
|
11
|
+
*
|
|
12
|
+
* Mixed v1/v2 comparison and bucket mixing is REJECTED upstream with
|
|
13
|
+
* `MINHASH_VERSION_MISMATCH` (see minhash-v2 migration); this module only ever
|
|
14
|
+
* bands a v2 signature.
|
|
15
|
+
*
|
|
16
|
+
* Pure compute, no deps, no storage, no network (PREVENT-PI-004 / PREVENT-011).
|
|
17
|
+
*/
|
|
18
|
+
import { MINHASH_VERSION, NUM_HASHES_V2, U64_BYTES, } from "./l1-minhash-v2.js";
|
|
19
|
+
/** Number of bands (frozen). */
|
|
20
|
+
export const BANDS_V2 = 64;
|
|
21
|
+
/** u64 slots per band (frozen): 64 * 4 = 256 total slots. */
|
|
22
|
+
export const VALUES_PER_BAND_V2 = 4;
|
|
23
|
+
/** Band byte length: 4 u64 LE = 32 bytes. */
|
|
24
|
+
export const BAND_BYTES_V2 = VALUES_PER_BAND_V2 * U64_BYTES;
|
|
25
|
+
/** Total signature byte length this scheme expects (256 * 8 = 2048). */
|
|
26
|
+
export const SIGNATURE_BYTES_EXPECTED_V2 = NUM_HASHES_V2 * U64_BYTES;
|
|
27
|
+
const MASK64 = 0xffffffffffffffffn;
|
|
28
|
+
/** 64-bit FNV-1a over a byte buffer. */
|
|
29
|
+
function fnv1a64Bytes(buf) {
|
|
30
|
+
let h = 0xcbf29ce484222325n;
|
|
31
|
+
for (let i = 0; i < buf.length; i++) {
|
|
32
|
+
h ^= BigInt(buf[i] ?? 0);
|
|
33
|
+
h = (h * 0x100000001b3n) & MASK64;
|
|
34
|
+
}
|
|
35
|
+
return h;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Compute the 64 LSH bucket keys for a v2 signature within a session. `bytes`
|
|
39
|
+
* is the 2048-byte little-endian signature (see encodeSignatureV2). Each bucket
|
|
40
|
+
* key is the hex of an 8-byte little-endian FNV-1a-64 over the band's 32 bytes
|
|
41
|
+
* prefixed by `<sessionId>|v2|band-` — so the "bucket bytes" are deterministic
|
|
42
|
+
* and reproducible across languages.
|
|
43
|
+
*/
|
|
44
|
+
export function lshBandsV2(bytes, sessionId, version = MINHASH_VERSION) {
|
|
45
|
+
if (bytes.length !== SIGNATURE_BYTES_EXPECTED_V2) {
|
|
46
|
+
throw new Error(`lshBandsV2: expected ${SIGNATURE_BYTES_EXPECTED_V2} signature bytes, got ${bytes.length}`);
|
|
47
|
+
}
|
|
48
|
+
const keys = [];
|
|
49
|
+
const prefix = Buffer.from(`${sessionId}|v${version}|`, "utf8");
|
|
50
|
+
for (let band = 0; band < BANDS_V2; band++) {
|
|
51
|
+
const start = band * BAND_BYTES_V2;
|
|
52
|
+
const bandBytes = bytes.slice(start, start + BAND_BYTES_V2);
|
|
53
|
+
const combined = Buffer.concat([prefix, Buffer.from(bandBytes)]);
|
|
54
|
+
const h = fnv1a64Bytes(combined);
|
|
55
|
+
const out = Buffer.allocUnsafe(U64_BYTES);
|
|
56
|
+
out.writeBigUInt64LE(h, 0);
|
|
57
|
+
keys.push(`b${band}:${out.toString("hex")}`);
|
|
58
|
+
}
|
|
59
|
+
return keys;
|
|
60
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* l1-lsh.ts — Locality-Sensitive Hashing banding over MinHash signatures
|
|
3
|
+
* (Sprint 11).
|
|
4
|
+
*
|
|
5
|
+
* Splits the 256-slot signature into `BANDS` bands of `ROWS_PER_BAND` rows. Each
|
|
6
|
+
* band is hashed into a bucket key; rows sharing a band are likely near-duplicates.
|
|
7
|
+
* The bucket key INCLUDES the session_id so candidates are scoped per session
|
|
8
|
+
* (deterministic, no cross-session leakage — QA determinism fix).
|
|
9
|
+
*
|
|
10
|
+
* All hashing is deterministic given the signature + seed (PREVENT-PI-004, pure).
|
|
11
|
+
*/
|
|
12
|
+
import { SIGNATURE_VERSION, NUM_HASHES } from "./l1-minhash.js";
|
|
13
|
+
export const BANDS = 64;
|
|
14
|
+
export const ROWS_PER_BAND = 4; // 64 * 4 = 256 slots (matches minhashSignature length)
|
|
15
|
+
/** Stable 32-bit FNV-1a (buffer form) for band hashing. */
|
|
16
|
+
function fnv1aBuf(buf) {
|
|
17
|
+
let h = 0x811c9dc5;
|
|
18
|
+
for (let i = 0; i < buf.length; i++) {
|
|
19
|
+
h ^= buf[i];
|
|
20
|
+
h = Math.imul(h, 0x01000193);
|
|
21
|
+
}
|
|
22
|
+
return h >>> 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compute the list of LSH bucket keys for a signature within a session.
|
|
26
|
+
* Deterministic: same (session_id, signature) → same keys, every run.
|
|
27
|
+
*/
|
|
28
|
+
export function lshBands(signature, sessionId, version = SIGNATURE_VERSION) {
|
|
29
|
+
if (signature.length < BANDS * ROWS_PER_BAND) {
|
|
30
|
+
throw new Error(`lshBands: signature length ${signature.length} < required ${BANDS * ROWS_PER_BAND}`);
|
|
31
|
+
}
|
|
32
|
+
const keys = [];
|
|
33
|
+
const seedPrefix = Buffer.from(`${sessionId}|${version}|`, "utf-8");
|
|
34
|
+
for (let band = 0; band < BANDS; band++) {
|
|
35
|
+
const start = band * ROWS_PER_BAND;
|
|
36
|
+
const slice = signature.slice(start, start + ROWS_PER_BAND);
|
|
37
|
+
const body = Buffer.allocUnsafe(ROWS_PER_BAND * 4);
|
|
38
|
+
for (let r = 0; r < ROWS_PER_BAND; r++) {
|
|
39
|
+
body.writeUInt32LE(slice[r] >>> 0, r * 4);
|
|
40
|
+
}
|
|
41
|
+
const combined = Buffer.concat([seedPrefix, body]);
|
|
42
|
+
keys.push(`b${band}:${fnv1aBuf(combined).toString(16)}`);
|
|
43
|
+
}
|
|
44
|
+
return keys;
|
|
45
|
+
}
|
|
46
|
+
/** Convenience: derive bands directly from text (used by callers without a cached sig). */
|
|
47
|
+
export function bandsForText(signature, sessionId, version = SIGNATURE_VERSION) {
|
|
48
|
+
if (signature.length !== NUM_HASHES) {
|
|
49
|
+
throw new Error(`bandsForText: expected signature length ${NUM_HASHES}, got ${signature.length}`);
|
|
50
|
+
}
|
|
51
|
+
return lshBands(signature, sessionId, version);
|
|
52
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* l1-minhash-v2.ts — MinHash V2 signatures (VC1C, M4 minhash-v2).
|
|
3
|
+
*
|
|
4
|
+
* The v1 path (l1-minhash.ts) uses 32-bit universal hashing with `p=2^31-1`
|
|
5
|
+
* and Number math, which is exact because a*x < 2^62 can overflow double
|
|
6
|
+
* precision (>= 2^53) and is reduced too eagerly. MinHashV2 freezes a v2
|
|
7
|
+
* scheme that is CROSS-LANGUAGE deterministic and byte-exact:
|
|
8
|
+
*
|
|
9
|
+
* - normalization: the stable `normalize()` from normalize.ts (NFC, NFKC
|
|
10
|
+
* case-fold, CRLF->LF, whitespace collapse, 32K cap);
|
|
11
|
+
* - shingles: windows of FIVE CODE POINTS over the normalized text
|
|
12
|
+
* (SHINGLE_CP=5), each shingle hashed to a u64 with 64-bit FNV-1a over its
|
|
13
|
+
* UTF-8 bytes;
|
|
14
|
+
* - 256 PUBLISHED unsigned (a_i, b_i) seed pairs, derived deterministically
|
|
15
|
+
* via splitmix64 and listed in
|
|
16
|
+
* conformance/vector-cortex/v2/minhash/seeds-v2.json;
|
|
17
|
+
* - p = 2^61 - 1 (2305843009213693951), EXACT BigInt multiply/modulo — a_i*x
|
|
18
|
+
* can reach ~2^124 so Number math would silently corrupt high-bit products;
|
|
19
|
+
* - signature = 256 slots, slot i = min over shingles of (a_i*x + b_i) mod p;
|
|
20
|
+
* the empty signature is all-`p` sentinel slots;
|
|
21
|
+
* - encoding: 256 x u64 LITTLE-ENDIAN bytes (2048 bytes total);
|
|
22
|
+
* - bands: 64 bands of FOUR u64 slots each (256 total), see l1-lsh-v2.ts.
|
|
23
|
+
*
|
|
24
|
+
* NEVER compares a v2 signature against a v1 signature — cross-version compare
|
|
25
|
+
* is rejected with `MINHASH_VERSION_MISMATCH` (see minhash-v2 migration).
|
|
26
|
+
*
|
|
27
|
+
* Pure compute, no deps, no storage, no network (PREVENT-PI-004 / PREVENT-011).
|
|
28
|
+
*/
|
|
29
|
+
import { normalize } from "./normalize.js";
|
|
30
|
+
/** MinHashV2 scheme version tag (frozen). */
|
|
31
|
+
export const MINHASH_VERSION = 2;
|
|
32
|
+
/** The published count of slots / seed pairs (frozen). */
|
|
33
|
+
export const NUM_HASHES_V2 = 256;
|
|
34
|
+
/** Five-code-point shingle length (frozen). */
|
|
35
|
+
export const SHINGLE_CP = 5;
|
|
36
|
+
/** p = 2^61 - 1 (Mersenne prime), as exact BigInt. */
|
|
37
|
+
export const P_V2 = (1n << 61n) - 1n;
|
|
38
|
+
/** Little-endian bytes per u64 signature slot. */
|
|
39
|
+
export const U64_BYTES = 8;
|
|
40
|
+
/** Signature byte length: 256 slots x 8 LE bytes. */
|
|
41
|
+
export const SIGNATURE_BYTES_V2 = NUM_HASHES_V2 * U64_BYTES;
|
|
42
|
+
/** Fixed splitmix64 initial state so the seed table is reproducible everywhere. */
|
|
43
|
+
export const SEED_STATE_INIT = 0x5eedc0def001ba11n & 0xffffffffffffffffn;
|
|
44
|
+
/** splitmix64 mixing constants (frozen). */
|
|
45
|
+
const SM_GOLDEN = 0x9e3779b97f4a7c15n;
|
|
46
|
+
const SM_M1 = 0xbf58476d1ce4e5b9n;
|
|
47
|
+
const SM_M2 = 0x94d049bb133111ebn;
|
|
48
|
+
const MASK64 = 0xffffffffffffffffn;
|
|
49
|
+
function mix(z0) {
|
|
50
|
+
let z = z0;
|
|
51
|
+
z = (z ^ (z >> 30n)) * SM_M1;
|
|
52
|
+
z &= MASK64;
|
|
53
|
+
z = (z ^ (z >> 27n)) * SM_M2;
|
|
54
|
+
z &= MASK64;
|
|
55
|
+
return z ^ (z >> 31n);
|
|
56
|
+
}
|
|
57
|
+
/** Create a splitmix64 PRNG from a fixed unsigned 64-bit state. */
|
|
58
|
+
export function splitmix64(state = SEED_STATE_INIT) {
|
|
59
|
+
let s = state & MASK64;
|
|
60
|
+
return () => {
|
|
61
|
+
s = (s + SM_GOLDEN) & MASK64;
|
|
62
|
+
return mix(s) & MASK64;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Derive the frozen 256 published (a_i, b_i) seed pairs. Each pair maps two
|
|
67
|
+
* splitmix64 draws into [1, p-1]. Deterministic and cross-language reproducible
|
|
68
|
+
* from SEED_STATE_INIT — mirrors `seeds-v2.json`.
|
|
69
|
+
*
|
|
70
|
+
* The table is frozen: computed ONCE at module load and reused for every call.
|
|
71
|
+
* Recomputing the 256 splitmix64 pairs (and re-walking all shingles) per
|
|
72
|
+
* signature/backfill row is wasteful for a large v1 index backfill, and the
|
|
73
|
+
* table is immutable by construction, so a single shared instance is safe and
|
|
74
|
+
* byte-identical. The returned sub-arrays share the frozen pair objects; they
|
|
75
|
+
* are themselves frozen so no caller can mutate the shared cache.
|
|
76
|
+
*/
|
|
77
|
+
export function minhashV2Seeds(count = NUM_HASHES_V2) {
|
|
78
|
+
return FROZEN_SEEDS.slice(0, count);
|
|
79
|
+
}
|
|
80
|
+
/** Singleton seed table, computed exactly once and frozen (see minhashV2Seeds). */
|
|
81
|
+
const FROZEN_SEEDS = (() => {
|
|
82
|
+
const prng = splitmix64();
|
|
83
|
+
const pairs = [];
|
|
84
|
+
for (let i = 0; i < NUM_HASHES_V2; i++) {
|
|
85
|
+
const a = (prng() % (P_V2 - 1n)) + 1n;
|
|
86
|
+
const b = (prng() % (P_V2 - 1n)) + 1n;
|
|
87
|
+
pairs.push(Object.freeze({ a, b }));
|
|
88
|
+
}
|
|
89
|
+
return Object.freeze(pairs);
|
|
90
|
+
})();
|
|
91
|
+
/** 64-bit FNV-1a over a UTF-8 string (for shingle hashing). */
|
|
92
|
+
function fnv1a64String(s) {
|
|
93
|
+
let h = 0xcbf29ce484222325n;
|
|
94
|
+
const bytes = Buffer.from(s, "utf8");
|
|
95
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
96
|
+
h ^= BigInt(bytes[i] ?? 0);
|
|
97
|
+
h = (h * 0x100000001b3n) & MASK64;
|
|
98
|
+
}
|
|
99
|
+
return h;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Five-code-point shingle set (deduped, capped) of normalized text as u64
|
|
103
|
+
* hashes. Iterates over code points (not UTF-16 code units) so astral-plane
|
|
104
|
+
* characters do not split a shingle.
|
|
105
|
+
*/
|
|
106
|
+
export function shinglesV2(text, shingleCp = SHINGLE_CP, maxShingles = 50_000) {
|
|
107
|
+
const norm = normalize(text);
|
|
108
|
+
if (norm.length === 0)
|
|
109
|
+
return [];
|
|
110
|
+
const cps = Array.from(norm); // code points
|
|
111
|
+
const set = new Set();
|
|
112
|
+
if (cps.length < shingleCp) {
|
|
113
|
+
set.add(fnv1a64String(norm));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
for (let i = 0; i + shingleCp <= cps.length; i++) {
|
|
117
|
+
const sh = cps.slice(i, i + shingleCp).join("");
|
|
118
|
+
set.add(fnv1a64String(sh));
|
|
119
|
+
if (set.size >= maxShingles)
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...set];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Compute the 256-slot MinHashV2 signature of a text as an array of exact u64
|
|
127
|
+
* BigInt values (each in [0, p], p = sentinel when empty). Uses EXACT BigInt
|
|
128
|
+
* multiply/modulo so high-bit products (a*x ~ up to 2^124) are never corrupted.
|
|
129
|
+
*/
|
|
130
|
+
export function minhashV2Signature(text) {
|
|
131
|
+
const grams = shinglesV2(text);
|
|
132
|
+
const seeds = minhashV2Seeds();
|
|
133
|
+
const sig = new Array(NUM_HASHES_V2).fill(P_V2);
|
|
134
|
+
if (grams.length === 0)
|
|
135
|
+
return sig;
|
|
136
|
+
for (let i = 0; i < NUM_HASHES_V2; i++) {
|
|
137
|
+
const { a, b } = seeds[i];
|
|
138
|
+
let min = P_V2;
|
|
139
|
+
for (const x of grams) {
|
|
140
|
+
const h = (a * x + b) % P_V2;
|
|
141
|
+
if (h < min)
|
|
142
|
+
min = h;
|
|
143
|
+
}
|
|
144
|
+
sig[i] = min;
|
|
145
|
+
}
|
|
146
|
+
return sig;
|
|
147
|
+
}
|
|
148
|
+
/** Encode a 256-slot signature to 2048 bytes as 256 x u64 little-endian. */
|
|
149
|
+
export function encodeSignatureV2(sig) {
|
|
150
|
+
if (sig.length !== NUM_HASHES_V2) {
|
|
151
|
+
throw new Error(`encodeSignatureV2: expected ${NUM_HASHES_V2} slots, got ${sig.length}`);
|
|
152
|
+
}
|
|
153
|
+
const buf = Buffer.allocUnsafe(SIGNATURE_BYTES_V2);
|
|
154
|
+
for (let i = 0; i < NUM_HASHES_V2; i++) {
|
|
155
|
+
buf.writeBigUInt64LE(sig[i], i * U64_BYTES);
|
|
156
|
+
}
|
|
157
|
+
return new Uint8Array(buf);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Estimated Jaccard similarity of two v2 signatures (fraction of equal slots).
|
|
161
|
+
*
|
|
162
|
+
* THIS IS v2-ONLY. Cross-version compare is rejected INTERNALLY with
|
|
163
|
+
* `MINHASH_VERSION_MISMATCH`: both inputs must be exact 256-slot v2 signatures,
|
|
164
|
+
* i.e. every slot a `bigint` in [0, P_V2] (the empty sentinel is exactly P_V2).
|
|
165
|
+
* A v1 signature is a `number[]` of u31 values, so passing a v1 array fails the
|
|
166
|
+
* bigint type/range check and is rejected — a mixed v1/v2 compare can never
|
|
167
|
+
* produce a similarity number. Never call this with v1 arrays.
|
|
168
|
+
*/
|
|
169
|
+
export function signatureSimilarityV2(a, b) {
|
|
170
|
+
assertV2Signature(a);
|
|
171
|
+
assertV2Signature(b);
|
|
172
|
+
const n = Math.min(a.length, b.length);
|
|
173
|
+
if (n === 0)
|
|
174
|
+
return 0;
|
|
175
|
+
let equal = 0;
|
|
176
|
+
for (let i = 0; i < n; i++)
|
|
177
|
+
if (a[i] === b[i])
|
|
178
|
+
equal++;
|
|
179
|
+
return equal / n;
|
|
180
|
+
}
|
|
181
|
+
/** Reject any array that is not an exact 256-slot v2 signature (bigint in [0, P_V2]). */
|
|
182
|
+
function assertV2Signature(sig) {
|
|
183
|
+
if (sig.length !== NUM_HASHES_V2) {
|
|
184
|
+
throw new Error(MINHASH_VERSION_MISMATCH_MSG);
|
|
185
|
+
}
|
|
186
|
+
for (const s of sig) {
|
|
187
|
+
if (typeof s !== "bigint" || s < 0n || s > P_V2) {
|
|
188
|
+
throw new Error(MINHASH_VERSION_MISMATCH_MSG);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Frozen cross-version-reject message shared by the v2-only function guards. */
|
|
193
|
+
const MINHASH_VERSION_MISMATCH_MSG = "MINHASH_VERSION_MISMATCH";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* l1-minhash.ts — MinHash signatures for L1 near-duplicate detection (Sprint 11).
|
|
3
|
+
*
|
|
4
|
+
* Uses UNIVERSAL HASHING (QA #3), not the broken permutation scheme from the
|
|
5
|
+
* generic dedup plan. Each of the 256 hash functions is `h_i(x) = (a_i·x + b_i)
|
|
6
|
+
* mod p` with a fixed prime `p` and per-index coefficients derived from a pinned
|
|
7
|
+
* seed (0xDEADBEEF). This makes signatures DETERMINISTIC across process restarts
|
|
8
|
+
* (a hard requirement — non-determinism silently breaks dedup).
|
|
9
|
+
*
|
|
10
|
+
* `signatureVersion` lets Sprint 12+ swap the scheme without invalidating stored
|
|
11
|
+
* signatures: old buckets keep their version, new ones get the new one.
|
|
12
|
+
*
|
|
13
|
+
* Pure compute, no deps, no network (PREVENT-PI-004).
|
|
14
|
+
*/
|
|
15
|
+
import { normalize } from "./normalize.js";
|
|
16
|
+
export const SIGNATURE_VERSION = 1;
|
|
17
|
+
export const NUM_HASHES = 256;
|
|
18
|
+
export const SHINGLE_SIZE = 5; // char 5-grams
|
|
19
|
+
const MAX_SHINGLES = 50_000; // QA #7/#15 complexity cap
|
|
20
|
+
const SEED = 0xdeadbeef;
|
|
21
|
+
const P = 2147483647; // 2^31 - 1, Mersenne prime
|
|
22
|
+
/** Per-index universal-hashing coefficients, derived deterministically from SEED. */
|
|
23
|
+
function coeffA(i) {
|
|
24
|
+
return (SEED + i * 2 + 1) % P;
|
|
25
|
+
}
|
|
26
|
+
function coeffB(i) {
|
|
27
|
+
return (SEED * 3 + i * 7 + 13) % P;
|
|
28
|
+
}
|
|
29
|
+
/** Stable 32-bit FNV-1a of a shingle (the `x` fed to the universal hashes). */
|
|
30
|
+
function shingleHash(gram) {
|
|
31
|
+
let h = 0x811c9dc5;
|
|
32
|
+
for (let k = 0; k < gram.length; k++) {
|
|
33
|
+
h ^= gram.charCodeAt(k);
|
|
34
|
+
h = Math.imul(h, 0x01000193);
|
|
35
|
+
}
|
|
36
|
+
return h >>> 0;
|
|
37
|
+
}
|
|
38
|
+
/** Char n-gram shingle set (deduped) of normalized text, capped at MAX_SHINGLES. */
|
|
39
|
+
export function shingles(text, size = SHINGLE_SIZE) {
|
|
40
|
+
const norm = normalize(text);
|
|
41
|
+
if (norm.length === 0)
|
|
42
|
+
return [];
|
|
43
|
+
const set = new Set();
|
|
44
|
+
if (norm.length < size) {
|
|
45
|
+
set.add(shingleHash(norm));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
for (let i = 0; i + size <= norm.length; i++) {
|
|
49
|
+
set.add(shingleHash(norm.slice(i, i + size)));
|
|
50
|
+
if (set.size >= MAX_SHINGLES)
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...set];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Compute the 256-element MinHash signature of a text. Each slot is the minimum,
|
|
58
|
+
* over all shingles, of the i-th universal hash. Empty text → all-P sentinel.
|
|
59
|
+
*/
|
|
60
|
+
export function minhashSignature(text) {
|
|
61
|
+
const grams = shingles(text);
|
|
62
|
+
const sig = new Array(NUM_HASHES).fill(P);
|
|
63
|
+
if (grams.length === 0)
|
|
64
|
+
return sig;
|
|
65
|
+
for (let i = 0; i < NUM_HASHES; i++) {
|
|
66
|
+
const a = coeffA(i);
|
|
67
|
+
const b = coeffB(i);
|
|
68
|
+
let min = P;
|
|
69
|
+
for (const x of grams) {
|
|
70
|
+
// (a*x + b) mod p — use Number math; a,x < 2^31 so a*x < 2^62, within
|
|
71
|
+
// double-precision integer range (2^53) only if reduced; reduce a*x first.
|
|
72
|
+
const ax = (a * (x % P)) % P;
|
|
73
|
+
const h = (ax + b) % P;
|
|
74
|
+
if (h < min)
|
|
75
|
+
min = h;
|
|
76
|
+
}
|
|
77
|
+
sig[i] = min;
|
|
78
|
+
}
|
|
79
|
+
return sig;
|
|
80
|
+
}
|
|
81
|
+
/** Estimated Jaccard similarity of two signatures (fraction of equal slots). */
|
|
82
|
+
export function signatureSimilarity(a, b) {
|
|
83
|
+
const n = Math.min(a.length, b.length);
|
|
84
|
+
if (n === 0)
|
|
85
|
+
return 0;
|
|
86
|
+
let equal = 0;
|
|
87
|
+
for (let i = 0; i < n; i++)
|
|
88
|
+
if (a[i] === b[i])
|
|
89
|
+
equal++;
|
|
90
|
+
return equal / n;
|
|
91
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* l1-verify.ts — trigram-similarity verification gate for L1 near-duplicates
|
|
3
|
+
* (Sprint 11).
|
|
4
|
+
*
|
|
5
|
+
* After LSH cheaply retrieves candidate chunk_ids, we apply a REAL similarity
|
|
6
|
+
* check before declaring a duplicate. This is the `pg_trgm`-equivalent final
|
|
7
|
+
* gate: we compute the trigram (character 3-gram) Jaccard / overlap similarity
|
|
8
|
+
* between the new normalized text and each candidate's normalized text.
|
|
9
|
+
*
|
|
10
|
+
* We compute it in TS (not via FTS5 MATCH, which is boolean) so we get a stable
|
|
11
|
+
* [0,1] score to threshold against (0.85 per spec). The FTS5 `trigram` table is
|
|
12
|
+
* still maintained for future query-path use, but verification is pure TS so its
|
|
13
|
+
* result is deterministic and unit-testable (PREVENT-PI-004).
|
|
14
|
+
*/
|
|
15
|
+
import { normalize } from "./normalize.js";
|
|
16
|
+
const TRIGRAM_SIZE = 3;
|
|
17
|
+
export const L1_VERIFY_THRESHOLD = 0.85;
|
|
18
|
+
/** Extract the set of character trigrams from normalized text. */
|
|
19
|
+
function trigrams(text) {
|
|
20
|
+
const norm = normalize(text);
|
|
21
|
+
const set = new Set();
|
|
22
|
+
if (norm.length === 0)
|
|
23
|
+
return set;
|
|
24
|
+
if (norm.length < TRIGRAM_SIZE) {
|
|
25
|
+
set.add(norm);
|
|
26
|
+
return set;
|
|
27
|
+
}
|
|
28
|
+
for (let i = 0; i + TRIGRAM_SIZE <= norm.length; i++) {
|
|
29
|
+
set.add(norm.slice(i, i + TRIGRAM_SIZE));
|
|
30
|
+
}
|
|
31
|
+
return set;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Trigram similarity in [0,1]. We use overlap coefficient (|A∩B| / min(|A|,|B|))
|
|
35
|
+
* which, like pg_trgm's `similarity`, is robust when one text is a substring of
|
|
36
|
+
* the other — better than Jaccard for the near-dup "one-word edit" case.
|
|
37
|
+
*/
|
|
38
|
+
export function trigramSimilarity(a, b) {
|
|
39
|
+
const ta = trigrams(a);
|
|
40
|
+
const tb = trigrams(b);
|
|
41
|
+
if (ta.size === 0 || tb.size === 0)
|
|
42
|
+
return 0;
|
|
43
|
+
let inter = 0;
|
|
44
|
+
const smaller = ta.size <= tb.size ? ta : tb;
|
|
45
|
+
const larger = smaller === ta ? tb : ta;
|
|
46
|
+
for (const g of smaller)
|
|
47
|
+
if (larger.has(g))
|
|
48
|
+
inter++;
|
|
49
|
+
return inter / smaller.size;
|
|
50
|
+
}
|
|
51
|
+
/** True when `a` and `b` are near-duplicates under the L1 threshold. */
|
|
52
|
+
export function isNearDuplicate(a, b, threshold = L1_VERIFY_THRESHOLD) {
|
|
53
|
+
return trigramSimilarity(a, b) >= threshold;
|
|
54
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mmr.ts — Maximal Marginal Relevance reranking for retrieval diversity
|
|
3
|
+
* (Sprint 12, QA #10).
|
|
4
|
+
*
|
|
5
|
+
* After a relevance-ranked candidate list, MMR reorders so we don't inject a
|
|
6
|
+
* cluster of near-identical checkpoints. Each step picks the candidate that
|
|
7
|
+
* maximizes `λ·relevance − (1−λ)·maxSimToAlreadySelected`, balancing relevance
|
|
8
|
+
* against redundancy. λ=0.5 is the default (equal weight).
|
|
9
|
+
*
|
|
10
|
+
* Pure function over cosine similarities — no deps, no network (PREVENT-PI-004).
|
|
11
|
+
*/
|
|
12
|
+
import { cosineSimilarity } from "../embedder.js";
|
|
13
|
+
export const MMR_LAMBDA = 0.5;
|
|
14
|
+
/**
|
|
15
|
+
* Rerank `items` by MMR. Returns the items in MMR order, capped at `k`.
|
|
16
|
+
* `lambda` balances relevance vs diversity (1 = pure relevance, 0 = max diversity).
|
|
17
|
+
*/
|
|
18
|
+
export function mmrRerank(items, k, lambda = MMR_LAMBDA) {
|
|
19
|
+
if (items.length === 0)
|
|
20
|
+
return [];
|
|
21
|
+
const remaining = [...items];
|
|
22
|
+
const selected = [];
|
|
23
|
+
const cap = Math.min(k, items.length);
|
|
24
|
+
while (selected.length < cap && remaining.length > 0) {
|
|
25
|
+
let bestIdx = 0;
|
|
26
|
+
let bestScore = -Infinity;
|
|
27
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
28
|
+
const cand = remaining[i];
|
|
29
|
+
// Max similarity to already-selected (redundancy penalty).
|
|
30
|
+
let maxSimToSelected = 0;
|
|
31
|
+
for (const sel of selected) {
|
|
32
|
+
const sim = cosineSimilarity(cand.vector, sel.vector);
|
|
33
|
+
if (sim > maxSimToSelected)
|
|
34
|
+
maxSimToSelected = sim;
|
|
35
|
+
}
|
|
36
|
+
const mmr = lambda * cand.relevance - (1 - lambda) * maxSimToSelected;
|
|
37
|
+
if (mmr > bestScore) {
|
|
38
|
+
bestScore = mmr;
|
|
39
|
+
bestIdx = i;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
selected.push(remaining.splice(bestIdx, 1)[0]);
|
|
43
|
+
}
|
|
44
|
+
return selected.map((s) => s.item);
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* normalize.ts — text normalization for content-addressable dedup (Sprint 9).
|
|
3
|
+
*
|
|
4
|
+
* The L0 dedup key is `sha256(normalize(text))`, so normalization decides which
|
|
5
|
+
* surface variants collapse to the same checkpoint. Pure, synchronous, no deps.
|
|
6
|
+
*
|
|
7
|
+
* Steps (order matters):
|
|
8
|
+
* 1. strip ANSI escape sequences (terminal color codes leak into tool output)
|
|
9
|
+
* 2. Unicode NFC (canonical composition — "e" + combining accent == "é")
|
|
10
|
+
* 3. case-fold (NFKC Cf + toLowerCase) so "Foo"/"foo"/"FOO" collapse (Sprint 10 L0 upgrade)
|
|
11
|
+
* 4. normalize newlines (CRLF/CR → LF)
|
|
12
|
+
* 5. collapse runs of whitespace to a single space, trim ends
|
|
13
|
+
* 6. cap at 32K chars (bounds hashing cost on pathological inputs — QA #7/#15)
|
|
14
|
+
*/
|
|
15
|
+
const MAX_CHARS = 32_768;
|
|
16
|
+
// ANSI/VT100 escape sequence: ESC (0x1B) [ ...params... [ -/]* final-byte.
|
|
17
|
+
// Built from the code point so the source file contains no literal escape byte.
|
|
18
|
+
const ESC = String.fromCharCode(0x1b);
|
|
19
|
+
const ANSI_RE = new RegExp(ESC + "\\[[0-?]*[ -/]*[@-~]", "g");
|
|
20
|
+
/** Strip ANSI/VT100 escape sequences. */
|
|
21
|
+
export function stripAnsi(text) {
|
|
22
|
+
return text.replace(ANSI_RE, "");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Normalize text to its canonical dedup form. Deterministic and idempotent:
|
|
26
|
+
* `normalize(normalize(x)) === normalize(x)`.
|
|
27
|
+
*/
|
|
28
|
+
export function normalize(text) {
|
|
29
|
+
if (!text)
|
|
30
|
+
return "";
|
|
31
|
+
let out = stripAnsi(text);
|
|
32
|
+
out = out.normalize("NFC");
|
|
33
|
+
out = out.toLowerCase(); // case-fold so "Foo"/"FOO" collapse to one key (locale-independent: byte-exact across languages)
|
|
34
|
+
out = out.replace(/\r\n?/g, "\n"); // CRLF / CR → LF
|
|
35
|
+
out = out.replace(/\s+/g, " ").trim();
|
|
36
|
+
if (out.length > MAX_CHARS)
|
|
37
|
+
out = out.slice(0, MAX_CHARS);
|
|
38
|
+
return out;
|
|
39
|
+
}
|