@gamaze/hicortex 0.22.0 → 0.22.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.
@@ -11,7 +11,8 @@
11
11
  * nouns (df 2-8), plus the fixed "Sirnäs" query (the owner's live case).
12
12
  *
13
13
  * Also carries the photo-comparison gates the sweep protocol defines:
14
- * - case 2 byte-stability (no-match queries must not drift),
14
+ * - case 2 unlinked-row identity (#449: unlinked rows' scores + mutual
15
+ * order must not drift; linked-row order drifts by design),
15
16
  * - battery top-1 stability >= 90%,
16
17
  * - no query losing a both-channel exact match from its top-3.
17
18
  */
@@ -47,8 +48,54 @@ export declare function deriveBatteryQueries(rows: Array<{
47
48
  id: string;
48
49
  content: string;
49
50
  }>): BatteryQuery[];
50
- /** Case 2 gate: the FULL returned list must be byte-identical (ids + order). */
51
+ /**
52
+ * Case 2 gate (pre-#449): the FULL returned list must be byte-identical
53
+ * (ids + order). Superseded as the --compare gate by `compareCase2Unlinked`
54
+ * in #449 PR E — linked rows' connections credit RISES by design under the
55
+ * reshape, so linked-row order drifts; kept for reference/re-recording old
56
+ * photos.
57
+ */
51
58
  export declare function compareCase2(baseline: string[], current: string[]): boolean;
59
+ /** One recorded case-2 result row (the #449 per-row photo extension). */
60
+ export interface Case2IdentityRow {
61
+ key: string;
62
+ score: number;
63
+ connections: number;
64
+ }
65
+ /** The photo sections the identity gate reads (baseline or current). */
66
+ export interface Case2IdentityPhoto {
67
+ ftsHitRows?: number;
68
+ ids?: string[];
69
+ results?: Case2IdentityRow[];
70
+ }
71
+ export interface Case2IdentityResult {
72
+ pass: boolean;
73
+ failures: string[];
74
+ }
75
+ /**
76
+ * #449 (spec AC-7): the unlinked-row identity gate REPLACING the ids+order
77
+ * byte-gate for this PR — linked-row order drifts BY DESIGN (their
78
+ * connections credit rises), while everything UNLINKED must be untouched:
79
+ * the log term contributes exactly +0 at k = 0, so an unlinked row's score
80
+ * is bit-identical pre/post change. On the extended photo (per-row results
81
+ * with scores + measured connections) it checks that
82
+ * (i) FTS hits stay 0 on both sides (no both-channel candidate can exist
83
+ * — the case's structural inertness),
84
+ * (ii) every connections===0 row's recorded score is identical to the
85
+ * baseline's within CASE2_SCORE_TOLERANCE (matched by key — the
86
+ * log term contributes exactly +0 at k = 0; the tolerance absorbs
87
+ * the wall-clock drift of the time terms between recordings),
88
+ * (iii) the subsequence of unlinked keys in the returned list is
89
+ * identical (bit-identical scores + stable sort ⇒ their mutual
90
+ * order cannot flip), and
91
+ * (iv) every position change involves at least one LINKED row.
92
+ *
93
+ * A baseline photo without per-row results (pre-#449 harness) fails with a
94
+ * re-record instruction — the gate refuses to compare what it cannot see.
95
+ */
96
+ export declare function compareCase2Unlinked(baseline: Case2IdentityPhoto, current: Case2IdentityPhoto, opts?: {
97
+ ftsHitMax?: number;
98
+ }): Case2IdentityResult;
52
99
  export interface BatteryComparison {
53
100
  top1Stability: number;
54
101
  /** Stability with intended D3 promotions excluded: old top-1 was NOT a
@@ -12,7 +12,8 @@
12
12
  * nouns (df 2-8), plus the fixed "Sirnäs" query (the owner's live case).
13
13
  *
14
14
  * Also carries the photo-comparison gates the sweep protocol defines:
15
- * - case 2 byte-stability (no-match queries must not drift),
15
+ * - case 2 unlinked-row identity (#449: unlinked rows' scores + mutual
16
+ * order must not drift; linked-row order drifts by design),
16
17
  * - battery top-1 stability >= 90%,
17
18
  * - no query losing a both-channel exact match from its top-3.
18
19
  */
@@ -20,6 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
21
  exports.STOPWORDS = exports.SIRNAS_QUERY = void 0;
21
22
  exports.deriveBatteryQueries = deriveBatteryQueries;
22
23
  exports.compareCase2 = compareCase2;
24
+ exports.compareCase2Unlinked = compareCase2Unlinked;
23
25
  exports.batteryComparison = batteryComparison;
24
26
  exports.SIRNAS_QUERY = "Sirnäs";
25
27
  /** Compact English stopword block (battery derivation only, not product). */
@@ -88,11 +90,117 @@ function deriveBatteryQueries(rows) {
88
90
  queries.push({ q: t, band: "proper-noun" });
89
91
  return queries;
90
92
  }
91
- /** Case 2 gate: the FULL returned list must be byte-identical (ids + order). */
93
+ /**
94
+ * Case 2 gate (pre-#449): the FULL returned list must be byte-identical
95
+ * (ids + order). Superseded as the --compare gate by `compareCase2Unlinked`
96
+ * in #449 PR E — linked rows' connections credit RISES by design under the
97
+ * reshape, so linked-row order drifts; kept for reference/re-recording old
98
+ * photos.
99
+ */
92
100
  function compareCase2(baseline, current) {
93
101
  return (baseline.length === current.length &&
94
102
  baseline.every((id, i) => id === current[i]));
95
103
  }
104
+ /**
105
+ * Score-comparison tolerance for gate (ii) — the WALL-CLOCK drift of the
106
+ * recorded scores, not slack. retrieve() scores with the live clock
107
+ * (computeScore's `now`), and the time-curve + decay terms move every
108
+ * row's score ≈1.5e-5/hour of wall time between photo recordings (measured
109
+ * 2026-09-17: two SAME-BUILD runs minutes apart differ by ~1e-6; the
110
+ * embedder itself is bit-deterministic across processes). The tolerance
111
+ * covers ~7 hours of drift between recordings while staying 290x below the
112
+ * smallest real movement this gate exists to catch — the k=1 linked-row
113
+ * credit change (0.0367 − 0.0075 = 0.0292). TRUE bit-identity of the k=0
114
+ * term is pinned at the unit level instead (tests/ranking-flip.test.ts,
115
+ * fixed NOW, synthetic distances).
116
+ */
117
+ const CASE2_SCORE_TOLERANCE = 1e-4;
118
+ /**
119
+ * #449 (spec AC-7): the unlinked-row identity gate REPLACING the ids+order
120
+ * byte-gate for this PR — linked-row order drifts BY DESIGN (their
121
+ * connections credit rises), while everything UNLINKED must be untouched:
122
+ * the log term contributes exactly +0 at k = 0, so an unlinked row's score
123
+ * is bit-identical pre/post change. On the extended photo (per-row results
124
+ * with scores + measured connections) it checks that
125
+ * (i) FTS hits stay 0 on both sides (no both-channel candidate can exist
126
+ * — the case's structural inertness),
127
+ * (ii) every connections===0 row's recorded score is identical to the
128
+ * baseline's within CASE2_SCORE_TOLERANCE (matched by key — the
129
+ * log term contributes exactly +0 at k = 0; the tolerance absorbs
130
+ * the wall-clock drift of the time terms between recordings),
131
+ * (iii) the subsequence of unlinked keys in the returned list is
132
+ * identical (bit-identical scores + stable sort ⇒ their mutual
133
+ * order cannot flip), and
134
+ * (iv) every position change involves at least one LINKED row.
135
+ *
136
+ * A baseline photo without per-row results (pre-#449 harness) fails with a
137
+ * re-record instruction — the gate refuses to compare what it cannot see.
138
+ */
139
+ function compareCase2Unlinked(baseline, current, opts) {
140
+ const failures = [];
141
+ const ftsMax = opts?.ftsHitMax ?? 0;
142
+ if ((baseline.ftsHitRows ?? ftsMax) > ftsMax) {
143
+ failures.push(`baseline FTS hits ${baseline.ftsHitRows} (expected <= ${ftsMax})`);
144
+ }
145
+ if ((current.ftsHitRows ?? ftsMax) > ftsMax) {
146
+ failures.push(`current FTS hits ${current.ftsHitRows} (expected <= ${ftsMax})`);
147
+ }
148
+ if (!baseline.results || baseline.results.length === 0 || !current.results) {
149
+ failures.push("baseline photo lacks case-2 per-row results — re-record it with the #449 harness " +
150
+ "(the identity gate reads recorded scores + connections)");
151
+ return { pass: false, failures };
152
+ }
153
+ // (ii) every unlinked row's score identical to the baseline's within the
154
+ // wall-clock tolerance (both directions: no unlinked row may appear,
155
+ // disappear, or rescore beyond clock drift).
156
+ const baseByKey = new Map(baseline.results.map((r) => [r.key, r]));
157
+ for (const r of current.results) {
158
+ if (r.connections !== 0)
159
+ continue;
160
+ const b = baseByKey.get(r.key);
161
+ if (!b) {
162
+ failures.push(`unlinked row ${r.key} missing from baseline results`);
163
+ continue;
164
+ }
165
+ if (Math.abs(b.score - r.score) > CASE2_SCORE_TOLERANCE) {
166
+ failures.push(`unlinked row ${r.key} score ${r.score} vs baseline ${b.score} (|Δ| ${Math.abs(b.score - r.score).toExponential(2)} > ${CASE2_SCORE_TOLERANCE})`);
167
+ }
168
+ }
169
+ const curKeys = new Set(current.results.map((r) => r.key));
170
+ for (const b of baseline.results) {
171
+ if (b.connections === 0 && !curKeys.has(b.key)) {
172
+ failures.push(`baseline unlinked row ${b.key} missing from current results`);
173
+ }
174
+ }
175
+ // (iii) the unlinked-key subsequence (order included) is identical.
176
+ const unlinkedKeys = (rows) => rows.filter((r) => r.connections === 0).map((r) => r.key);
177
+ const baseSub = unlinkedKeys(baseline.results);
178
+ const curSub = unlinkedKeys(current.results);
179
+ if (baseSub.join("") !== curSub.join("")) {
180
+ failures.push(`unlinked-key subsequence changed: [${baseSub.join(", ")}] -> [${curSub.join(", ")}]`);
181
+ }
182
+ // (iv) every position change involves at least one linked row.
183
+ const connByKey = (photo) => new Map(photo.map((r) => [r.key, r.connections]));
184
+ const baseConn = connByKey(baseline.results);
185
+ const curConn = connByKey(current.results);
186
+ if (baseline.results.length !== current.results.length) {
187
+ failures.push(`returned-list length changed (${baseline.results.length} -> ${current.results.length})`);
188
+ }
189
+ else {
190
+ for (let i = 0; i < current.results.length; i++) {
191
+ const oldKey = baseline.results[i].key;
192
+ const newKey = current.results[i].key;
193
+ if (oldKey === newKey)
194
+ continue;
195
+ const oldLinked = (baseConn.get(oldKey) ?? 0) > 0;
196
+ const newLinked = (curConn.get(newKey) ?? 0) > 0;
197
+ if (!oldLinked && !newLinked) {
198
+ failures.push(`position ${i + 1} changed ${oldKey} -> ${newKey} with BOTH rows unlinked`);
199
+ }
200
+ }
201
+ }
202
+ return { pass: failures.length === 0, failures };
203
+ }
96
204
  /** A both-channel row carrying the query term — the genuine-match signature. */
97
205
  function isBothChannelExact(m, id, term, contentById) {
98
206
  if (m)
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Search-trust ranking gate (#425) — the AC1/AC2/AC3 before/after photo.
4
+ * Extended by #449 (items 1+3, PR E): the case-2 photo gains per-row
5
+ * results (scores + measured connections) and its gate becomes the
6
+ * unlinked-row identity comparator; a new planted case 3 (linked_fallback)
7
+ * pins the connections-reshape expectations.
4
8
  *
5
9
  * npm run eval:ranking [--snapshot <db>] [--photo <out.json>]
6
10
  * [--compare <baseline.json>] [--scoring '<json>']
7
11
  *
8
- * --snapshot <db> enables case 3: the deterministic real-query battery
12
+ * --snapshot <db> enables case 4: the deterministic real-query battery
9
13
  * against a READONLY snapshot (openSnapshot — never
10
14
  * initDb; the snapshot is never touched). Also reports
11
15
  * where the real "Sirnäs" memory ranks today (the
@@ -14,12 +18,19 @@
14
18
  * data/ranking-eval-photo.json). ALWAYS written, even
15
19
  * on a red run — the photo IS the measurement.
16
20
  * --compare <json> gate against a previously recorded photo: case 2's
17
- * returned list must be byte-identical, and the battery
18
- * must hold top-1 stability >= 90% with no query losing
19
- * a both-channel exact match from its top-3.
21
+ * UNLINKED rows must keep their scores (within the
22
+ * wall-clock tolerance) and mutual order — linked-row
23
+ * order drifts BY DESIGN under #449 — and the battery
24
+ * (when both sides have one) must hold top-1
25
+ * stability >= 90% with no query losing a both-channel
26
+ * exact match from its top-3.
20
27
  * --scoring '<json>' a JSON object of ScoringWeights overrides passed to
21
28
  * configureScoring (the eval/test seam, #408) — the
22
29
  * sweep knob. Production runs omit it.
30
+ * --now <ISO> #458: pin the eval clock — planted lastAccessed offsets
31
+ * AND every retrieve() score against the SAME instant,
32
+ * so before/after photos are wall-clock-independent.
33
+ * Default: the live clock. The photo records the clock.
23
34
  *
24
35
  * Cases (fixtures: ranking-fixtures.ts, real bge-small-en-v1.5 embedder):
25
36
  * 1. sirnas_exact_match — planted corpus in a THROWAWAY writable DB;
@@ -27,8 +38,17 @@
27
38
  * Exit code reflects the verdict (a pre-fix run is EXPECTED to exit
28
39
  * non-zero — that red photo is the before picture).
29
40
  * 2. no_match_control — same corpus, a "mamma"-class query with zero FTS
30
- * hits; records the full returned list for byte-stability comparison.
31
- * 3. real-query battery (needs --snapshot) — ~30 queries derived
41
+ * hits; records the full returned list (ids + per-row results) for the
42
+ * unlinked-identity comparison.
43
+ * 3. linked_fallback (#449) — planted corpus in its OWN throwaway DB: a
44
+ * 16-link vs 20-link identical-content hub pair (saturation tie,
45
+ * declared byte-equal post-change) + an unlinked higher-similarity row
46
+ * vs a 4-link lower-similarity row (direction pin). Measured under the
47
+ * eval seam rrfCompositeWeight=1 (finalScore = composite exactly —
48
+ * identical-content hubs differ by an RRF epsilon at the default
49
+ * blend). RED under the pre-#449 linear term: the recorded
50
+ * before-picture.
51
+ * 4. real-query battery (needs --snapshot) — ~30 queries derived
32
52
  * deterministically from the snapshot corpus (top/mid/rare-frequency
33
53
  * distinctive terms + proper nouns) + the fixed "Sirnäs" query; top-8
34
54
  * ids per query recorded to the photo.