@gamaze/hicortex 0.21.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.
Files changed (50) hide show
  1. package/assets/dashboard.html +282 -27
  2. package/dist/calibration.d.ts +77 -12
  3. package/dist/calibration.js +82 -15
  4. package/dist/capture-health.d.ts +42 -5
  5. package/dist/capture-health.js +57 -7
  6. package/dist/capture-pause.d.ts +7 -4
  7. package/dist/capture-pause.js +7 -4
  8. package/dist/consolidate.d.ts +19 -0
  9. package/dist/consolidate.js +125 -51
  10. package/dist/dashboard.d.ts +41 -16
  11. package/dist/dashboard.js +110 -41
  12. package/dist/db.js +16 -0
  13. package/dist/dedup.js +4 -1
  14. package/dist/eval/decay-eval.d.ts +6 -5
  15. package/dist/eval/decay-eval.js +10 -48
  16. package/dist/eval/eval-clock.d.ts +32 -0
  17. package/dist/eval/eval-clock.js +47 -0
  18. package/dist/eval/graph-eval.d.ts +15 -2
  19. package/dist/eval/graph-eval.js +51 -5
  20. package/dist/eval/planted-eval.d.ts +4 -0
  21. package/dist/eval/planted-eval.js +27 -2
  22. package/dist/eval/planted-harness.d.ts +7 -0
  23. package/dist/eval/planted-harness.js +2 -0
  24. package/dist/eval/ranking-battery.d.ts +49 -2
  25. package/dist/eval/ranking-battery.js +110 -2
  26. package/dist/eval/ranking-eval.d.ts +26 -6
  27. package/dist/eval/ranking-eval.js +197 -34
  28. package/dist/eval/ranking-fixtures.d.ts +46 -6
  29. package/dist/eval/ranking-fixtures.js +268 -9
  30. package/dist/eval/recall-sweep.d.ts +7 -2
  31. package/dist/eval/recall-sweep.js +42 -13
  32. package/dist/eval/relevance-eval.d.ts +115 -1
  33. package/dist/eval/relevance-eval.js +318 -32
  34. package/dist/eval/run-eval.d.ts +7 -4
  35. package/dist/eval/run-eval.js +37 -10
  36. package/dist/graph.d.ts +1 -2
  37. package/dist/graph.js +3 -4
  38. package/dist/mcp-server.js +3 -2
  39. package/dist/nofit.d.ts +2 -1
  40. package/dist/nofit.js +2 -1
  41. package/dist/recall-index.d.ts +3 -2
  42. package/dist/recall-index.js +3 -2
  43. package/dist/retrieval.d.ts +43 -18
  44. package/dist/retrieval.js +148 -86
  45. package/dist/status.js +18 -0
  46. package/dist/storage.d.ts +2 -1
  47. package/dist/storage.js +6 -1
  48. package/dist/types.d.ts +25 -4
  49. package/package.json +1 -1
  50. package/server.json +4 -4
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import type express from "express";
24
24
  import type Database from "better-sqlite3";
25
+ import { type CaptureHealthRow } from "./capture-health.js";
25
26
  import { type Stage } from "./stages.js";
26
27
  /** Corpus-shape snapshot. `adoption` is null in backfilled rows (point-in-time,
27
28
  * can't be reconstructed from created_at). */
@@ -202,7 +203,7 @@ export interface DashboardData {
202
203
  period_start: string | null;
203
204
  };
204
205
  };
205
- range: "7d" | "30d" | "90d" | "all";
206
+ range: "7d" | "30d" | "90d" | "180d" | "all";
206
207
  series: DashboardSnapshot[];
207
208
  composition: {
208
209
  by_type: Record<string, number>;
@@ -213,22 +214,28 @@ export interface DashboardData {
213
214
  };
214
215
  /**
215
216
  * #422 Phase 2 — capture health: per machine × agent /distill outcome
216
- * accounting for the most recent day with rows (posts / sessions / bytes /
217
- * held / retried). ALWAYS present; {day: null, rows: []} when nothing is
217
+ * accounting. ALWAYS present; {day: null, rows: []} when nothing is
218
218
  * recorded (fresh install / all rows pruned — the page degrades to the
219
219
  * phase-1 counts-only rows).
220
+ *
221
+ * #409 fix round 7 (owner ruling 2026-09-14: "the normal 30 day as the
222
+ * other cards"): `day`/`rows` keep the NEWEST night with rows (secondary
223
+ * info — the card's "tonight" suffix), while `window_days`/`window_rows`
224
+ * carry the ROLLING capture window's per machine × agent sums — the card's
225
+ * face. Both derive from the same distill_activity rows and the same
226
+ * CAPTURE_HEALTH_WINDOW_DAYS constant (retention == window). window_rows
227
+ * is [] exactly when rows is (same table) — the counts fallback then still
228
+ * applies. A pre-round-7 server sends neither window key; the page guards.
220
229
  */
221
230
  capture_health: {
222
231
  day: string | null;
223
- rows: Array<{
224
- machine: string;
225
- agent: string;
226
- posts: number;
227
- sessions: number;
228
- bytes: number;
229
- held: number;
230
- retried: number;
231
- }>;
232
+ rows: CaptureHealthRow[];
233
+ /** Echoed window length (the page renders "last N days" from it — never
234
+ * hardcoded client-side). */
235
+ window_days: number;
236
+ /** Per machine × agent sums over the rolling window (posts / sessions /
237
+ * bytes / held / retried), bytes DESC. */
238
+ window_rows: CaptureHealthRow[];
232
239
  };
233
240
  /**
234
241
  * #423 phase 3 — fleet presence: the operator's capture pauses + per-bundle
@@ -565,7 +572,10 @@ export declare function handleDashboardField(db: Database.Database): {
565
572
  export declare function dashboardFieldHandler(getDb: () => Database.Database): express.RequestHandler;
566
573
  /** One night of the replay ledger. `by_agent` keys are source_agent strings
567
574
  * ("(unknown)" when the memory row is gone); learned counts added, enriched
568
- * counts distinct enriched memories, merged counts dedup losers. */
575
+ * counts distinct enriched memories, merged counts dedup losers, and the
576
+ * #452 truth-management keys count superseded/retracted sources and
577
+ * reconsolidation rewrites (agent = the OLD memory's source — the event
578
+ * belongs to the memory that was demoted). */
569
579
  export interface DashboardEventsNight {
570
580
  date: string;
571
581
  added: string[];
@@ -578,10 +588,25 @@ export interface DashboardEventsNight {
578
588
  a: string;
579
589
  b: string;
580
590
  }>;
591
+ /** #452 — superseded_by links born that night ({old, by} ids). */
592
+ superseded: Array<{
593
+ old: string;
594
+ by: string;
595
+ }>;
596
+ /** #452 — corrected_by links whose source is currently status='retracted'. */
597
+ retracted: Array<{
598
+ old: string;
599
+ by: string;
600
+ }>;
601
+ /** #452 — distinct memories reconsolidation rewrote that night. */
602
+ rewritten: string[];
581
603
  by_agent: Record<string, {
582
604
  learned: number;
583
605
  enriched: number;
584
606
  merged: number;
607
+ superseded: number;
608
+ retracted: number;
609
+ rewritten: number;
585
610
  }>;
586
611
  }
587
612
  /** The /dashboard/events response. */
@@ -589,9 +614,9 @@ export interface DashboardEvents {
589
614
  nights: DashboardEventsNight[];
590
615
  }
591
616
  /**
592
- * The pure data handler for GET /dashboard/events?days=N. Buckets the four
593
- * event sources into UTC nights (only nights with events, ascending) within
594
- * the last `days` calendar days (today inclusive).
617
+ * The pure data handler for GET /dashboard/events?days=N. Buckets the event
618
+ * sources into UTC nights (only nights with events, ascending) within the
619
+ * last `days` calendar days (today inclusive).
595
620
  */
596
621
  export declare function handleDashboardEvents(db: Database.Database, query: {
597
622
  days?: unknown;
package/dist/dashboard.js CHANGED
@@ -113,38 +113,17 @@ function computeDashboardMetrics(db) {
113
113
  },
114
114
  };
115
115
  }
116
- /**
117
- * One grouped pass over memory_links for per-memory link counts (batch —
118
- * never per-row queries); shared by the field payload and stage_counts.
119
- * Counts BOTH directions (a memory's connectivity is symmetric).
120
- */
121
- function linkCountsByMemory(db) {
122
- const rows = db
123
- .prepare(`SELECT id, SUM(c) AS c FROM (
124
- SELECT source_id AS id, COUNT(*) AS c FROM memory_links GROUP BY source_id
125
- UNION ALL
126
- SELECT target_id AS id, COUNT(*) AS c FROM memory_links GROUP BY target_id
127
- ) GROUP BY id`)
128
- .all();
129
- const linkCounts = new Map();
130
- for (const r of rows)
131
- linkCounts.set(r.id, r.c);
132
- return linkCounts;
133
- }
134
116
  /**
135
117
  * Derive one memory's {strength, stage} — the exact math of the field
136
- * payload: effectiveStrength (importance = base, access + link hardening)
137
- * rounded to the 1e-6 wire format, then the recency gate (days since
118
+ * payload: effectiveStrength (importance = base) rounded to the 1e-6 wire
119
+ * format, then the recency gate (days since
138
120
  * last_accessed, falling back to created_at; unparseable → null, the gate is
139
121
  * skipped rather than guessing "very old") and the calibrated strength bands.
140
122
  */
141
- function deriveStageForRow(row, linkCount, now, nowDay) {
123
+ function deriveStageForRow(row, now, nowDay) {
142
124
  const base = row.base_strength ?? 0.5;
143
- const accessCount = row.access_count ?? 0;
144
125
  const effStr = (0, retrieval_js_1.effectiveStrength)(base, row.last_accessed, now, {
145
126
  importance: base,
146
- accessCount,
147
- linkCount,
148
127
  });
149
128
  const strength = Math.round(effStr * 1e6) / 1e6;
150
129
  const refDay = utcDayNumber(row.last_accessed) ?? utcDayNumber(row.created_at);
@@ -160,15 +139,14 @@ function deriveStageForRow(row, linkCount, now, nowDay) {
160
139
  function computeStageCounts(db) {
161
140
  const now = new Date();
162
141
  const nowDay = Math.floor(now.getTime() / 86_400_000);
163
- const linkCounts = linkCountsByMemory(db);
164
142
  const rows = db
165
- .prepare(`SELECT id, base_strength, last_accessed, access_count, created_at
143
+ .prepare(`SELECT id, base_strength, last_accessed, created_at
166
144
  FROM memories
167
145
  WHERE COALESCE(status, '') != 'absorbed'`)
168
146
  .all();
169
147
  const counts = { forming: 0, belief: 0, truth: 0, fading: 0 };
170
148
  for (const r of rows) {
171
- counts[deriveStageForRow(r, linkCounts.get(r.id) ?? 0, now, nowDay).stage]++;
149
+ counts[deriveStageForRow(r, now, nowDay).stage]++;
172
150
  }
173
151
  return counts;
174
152
  }
@@ -397,7 +375,7 @@ function backfillSnapshots(db) {
397
375
  // LIVE composition (so day-one, before any snapshot is written, still shows
398
376
  // the current corpus shape), and builds the digest for the selected day.
399
377
  // ---------------------------------------------------------------------------
400
- const VALID_RANGES = new Set(["7d", "30d", "90d", "all"]);
378
+ const VALID_RANGES = new Set(["7d", "30d", "90d", "180d", "all"]);
401
379
  const DEFAULT_RANGE = "30d";
402
380
  const DEFAULT_DIGEST_LIMIT = 10;
403
381
  function rangeToCutoff(range) {
@@ -597,7 +575,7 @@ function handleDashboardData(db, query, config) {
597
575
  by_source_agent: live.by_source_agent,
598
576
  by_source_machine: live.by_source_machine,
599
577
  },
600
- capture_health: (0, capture_health_js_1.readCaptureHealth)(db),
578
+ capture_health: { ...(0, capture_health_js_1.readCaptureHealth)(db), ...(0, capture_health_js_1.readCaptureHealthWindow)(db) },
601
579
  // #423 phase 3: pauses + presence in one block — one source of truth
602
580
  // for the rail's dots, toggles and the capture card's PAUSED badges.
603
581
  fleet: {
@@ -690,8 +668,6 @@ function utcDayNumber(iso) {
690
668
  function handleDashboardField(db) {
691
669
  const now = new Date();
692
670
  const nowDay = Math.floor(now.getTime() / 86_400_000);
693
- // Shared derivation inputs (one batched pass — never per-row queries).
694
- const linkCounts = linkCountsByMemory(db);
695
671
  const rows = db
696
672
  .prepare(`SELECT id, content, base_strength, last_accessed, access_count,
697
673
  created_at, domain, source_agent, source_machine
@@ -702,7 +678,7 @@ function handleDashboardField(db) {
702
678
  const memories = rows.map((r) => {
703
679
  // Shared per-row derivation — the SAME math the snapshot stage_counts
704
680
  // uses (deriveStageForRow); the field only adds its wire fields.
705
- const { strength, stage } = deriveStageForRow(r, linkCounts.get(r.id) ?? 0, now, nowDay);
681
+ const { strength, stage } = deriveStageForRow(r, now, nowDay);
706
682
  return {
707
683
  id: r.id,
708
684
  title: (0, recall_index_js_1.memoryTitle)(r.content, calibration_js_1.RECALL_TITLE_CHARS),
@@ -787,9 +763,12 @@ function dashboardFieldHandler(getDb) {
787
763
  }
788
764
  };
789
765
  }
790
- const EVENTS_DEFAULT_DAYS = 90;
766
+ // #452 (owner directive 2026-09-16): the default window is 30 days for ALL
767
+ // views — longer ranges are opt-in via the page's selector. MAX stays 365:
768
+ // it is the generic API cap and an explicit ?days= is honored up to it.
769
+ const EVENTS_DEFAULT_DAYS = 30;
791
770
  const EVENTS_MAX_DAYS = 365;
792
- /** Parse the ?days= param: integer 1..365, default 90. Returns null when the
771
+ /** Parse the ?days= param: integer 1..365, default 30. Returns null when the
793
772
  * value is present but invalid (→ 400); undefined when absent (→ default). */
794
773
  function parseEventsDays(raw) {
795
774
  if (raw === undefined)
@@ -801,9 +780,9 @@ function parseEventsDays(raw) {
801
780
  return n;
802
781
  }
803
782
  /**
804
- * The pure data handler for GET /dashboard/events?days=N. Buckets the four
805
- * event sources into UTC nights (only nights with events, ascending) within
806
- * the last `days` calendar days (today inclusive).
783
+ * The pure data handler for GET /dashboard/events?days=N. Buckets the event
784
+ * sources into UTC nights (only nights with events, ascending) within the
785
+ * last `days` calendar days (today inclusive).
807
786
  */
808
787
  function handleDashboardEvents(db, query) {
809
788
  const parsed = parseEventsDays(query.days);
@@ -828,14 +807,31 @@ function handleDashboardEvents(db, query) {
828
807
  const nightFor = (date) => {
829
808
  let n = nights.get(date);
830
809
  if (!n) {
831
- n = { date, added: [], enriched: [], merged: [], linked: [], by_agent: {} };
810
+ n = {
811
+ date,
812
+ added: [],
813
+ enriched: [],
814
+ merged: [],
815
+ linked: [],
816
+ superseded: [],
817
+ retracted: [],
818
+ rewritten: [],
819
+ by_agent: {},
820
+ };
832
821
  nights.set(date, n);
833
822
  }
834
823
  return n;
835
824
  };
836
825
  const bumpAgent = (n, agent, key) => {
837
826
  const k = agent ?? "(unknown)";
838
- const rec = n.by_agent[k] ?? { learned: 0, enriched: 0, merged: 0 };
827
+ const rec = n.by_agent[k] ?? {
828
+ learned: 0,
829
+ enriched: 0,
830
+ merged: 0,
831
+ superseded: 0,
832
+ retracted: 0,
833
+ rewritten: 0,
834
+ };
839
835
  rec[key] += 1;
840
836
  n.by_agent[k] = rec;
841
837
  };
@@ -855,10 +851,12 @@ function handleDashboardEvents(db, query) {
855
851
  // enriched — memory_history rows, DISTINCT memory per night (one rewrite of
856
852
  // the same memory on a night is one enrichment). LEFT JOIN: the memory row
857
853
  // can be gone (hard-deleted legacy losers); those count under "(unknown)".
854
+ // #452: reconsolidation rewrites are EXCLUDED — they render as `rewritten`
855
+ // below, so one rewrite is one caption, never "Enriched"+"Rewritten".
858
856
  const enrichRows = db
859
857
  .prepare(`SELECT h.memory_id, h.created_at, m.source_agent AS agent
860
858
  FROM memory_history h LEFT JOIN memories m ON m.id = h.memory_id
861
- WHERE h.created_at >= ?`)
859
+ WHERE h.created_at >= ? AND h.cause <> 'reconsolidation'`)
862
860
  .all(cutoffIso);
863
861
  const enrichedSeen = new Set(); // `${date}|${memory_id}` dedup
864
862
  for (const r of enrichRows) {
@@ -898,8 +896,79 @@ function handleDashboardEvents(db, query) {
898
896
  continue;
899
897
  nightFor(date).linked.push({ a: r.source_id, b: r.target_id });
900
898
  }
899
+ // superseded (#452) — superseded_by links born in the window; agent = the
900
+ // OLD memory's source (the event belongs to the demoted claim). The link
901
+ // ALSO stays in `linked` above — supersession is still a connection, the
902
+ // ring is unchanged.
903
+ const supersededRows = db
904
+ .prepare(`SELECT l.source_id, l.target_id, l.created_at, m.source_agent AS agent
905
+ FROM memory_links l LEFT JOIN memories m ON m.id = l.source_id
906
+ WHERE l.relationship = 'superseded_by' AND l.created_at >= ?`)
907
+ .all(cutoffIso);
908
+ for (const r of supersededRows) {
909
+ const date = inWindow(r.created_at);
910
+ if (date === null)
911
+ continue;
912
+ const n = nightFor(date);
913
+ n.superseded.push({ old: r.source_id, by: r.target_id });
914
+ bumpAgent(n, r.agent, "superseded");
915
+ }
916
+ // retracted (#452) — corrected_by links whose SOURCE's CURRENT status is
917
+ // 'retracted'. INNER JOIN so the status filter applies: rewrite-path
918
+ // corrected_by links leave status 'corrected' (or NULL on legacy rows) and
919
+ // are NOT retractions. Marks write no memory_history row (grounding: the
920
+ // only two INSERT sites are the reconsolidation rewrite + rollback), so
921
+ // the link + current status IS the retraction record. Dedup per
922
+ // (date, source_id) — the enriched pattern — in case a pair's link row was
923
+ // re-created on the same night.
924
+ const retractedRows = db
925
+ .prepare(`SELECT l.source_id, l.target_id, l.created_at, m.source_agent AS agent
926
+ FROM memory_links l JOIN memories m ON m.id = l.source_id
927
+ WHERE l.relationship = 'corrected_by' AND m.status = 'retracted' AND l.created_at >= ?`)
928
+ .all(cutoffIso);
929
+ const retractedSeen = new Set(); // `${date}|${source_id}` dedup
930
+ for (const r of retractedRows) {
931
+ const date = inWindow(r.created_at);
932
+ if (date === null)
933
+ continue;
934
+ const key = `${date}|${r.source_id}`;
935
+ if (retractedSeen.has(key))
936
+ continue;
937
+ retractedSeen.add(key);
938
+ const n = nightFor(date);
939
+ n.retracted.push({ old: r.source_id, by: r.target_id });
940
+ bumpAgent(n, r.agent, "retracted");
941
+ }
942
+ // rewritten (#452) — memory_history rows from the reconsolidation rewrite
943
+ // (cause='reconsolidation'; rollback rows keep their own cause and stay
944
+ // enriched-shaped). DISTINCT memory per night, the enriched pattern.
945
+ const rewrittenRows = db
946
+ .prepare(`SELECT h.memory_id, h.created_at, m.source_agent AS agent
947
+ FROM memory_history h LEFT JOIN memories m ON m.id = h.memory_id
948
+ WHERE h.cause = 'reconsolidation' AND h.created_at >= ?`)
949
+ .all(cutoffIso);
950
+ const rewrittenSeen = new Set(); // `${date}|${memory_id}` dedup
951
+ for (const r of rewrittenRows) {
952
+ const date = inWindow(r.created_at);
953
+ if (date === null)
954
+ continue;
955
+ const key = `${date}|${r.memory_id}`;
956
+ if (rewrittenSeen.has(key))
957
+ continue;
958
+ rewrittenSeen.add(key);
959
+ const n = nightFor(date);
960
+ n.rewritten.push(r.memory_id);
961
+ bumpAgent(n, r.agent, "rewritten");
962
+ }
901
963
  const sorted = Array.from(nights.values())
902
- .filter((n) => n.added.length + n.enriched.length + n.merged.length + n.linked.length > 0)
964
+ .filter((n) => n.added.length +
965
+ n.enriched.length +
966
+ n.merged.length +
967
+ n.linked.length +
968
+ n.superseded.length +
969
+ n.retracted.length +
970
+ n.rewritten.length >
971
+ 0)
903
972
  .sort((x, y) => (x.date < y.date ? -1 : x.date > y.date ? 1 : 0));
904
973
  return { status: 200, body: { nights: sorted } };
905
974
  }
package/dist/db.js CHANGED
@@ -652,6 +652,22 @@ const MIGRATIONS = [
652
652
  WHERE importance_scored_at IS NULL AND base_strength != 0.5`);
653
653
  },
654
654
  },
655
+ {
656
+ version: 20,
657
+ name: "add_promotion_last_count",
658
+ up: (db) => {
659
+ // #448 — promotion baseline: the access_count the nightly promotion
660
+ // stage (stagePromotion) last consumed. Backfilled to access_count so
661
+ // the first post-upgrade nightly does not replay lifetime counts as
662
+ // promotions (v19 backfill precedent). Written by the stage, this
663
+ // backfill, and dedup merges (the baseline moves with the summed
664
+ // counter). Idempotent via hasColumn (the v8 pattern).
665
+ if (!hasColumn(db, "memories", "promotion_last_count")) {
666
+ db.exec("ALTER TABLE memories ADD COLUMN promotion_last_count INTEGER");
667
+ }
668
+ db.exec("UPDATE memories SET promotion_last_count = access_count WHERE promotion_last_count IS NULL");
669
+ },
670
+ },
655
671
  ];
656
672
  /**
657
673
  * Run all pending migrations against the database.
package/dist/dedup.js CHANGED
@@ -347,7 +347,9 @@ function mergeCluster(db, canonical, losers, injectFailure) {
347
347
  clearLoserTags.run(loser.id);
348
348
  storage.updateMemory(db, loser.id, { domain: null });
349
349
  }
350
- // 5. Merge counters onto the canonical.
350
+ // 5. Merge counters onto the canonical. promotion_last_count moves WITH the
351
+ // summed access_count (#448): a held baseline would replay the losers'
352
+ // lifetime accesses as fresh promotions on the next nightly run.
351
353
  const accessCount = canonical.access_count + losers.reduce((s, l) => s + l.access_count, 0);
352
354
  const shownCount = (canonical.shown_count ?? 0) + losers.reduce((s, l) => s + (l.shown_count ?? 0), 0);
353
355
  const lastAccessed = [canonical, ...losers]
@@ -361,6 +363,7 @@ function mergeCluster(db, canonical, losers, injectFailure) {
361
363
  shown_count: shownCount,
362
364
  ...(lastAccessed ? { last_accessed: lastAccessed } : {}),
363
365
  base_strength: baseStrength,
366
+ promotion_last_count: accessCount,
364
367
  });
365
368
  injectFailure?.(canonical.id);
366
369
  // 6. Audit trail (dedup_log is the merge record — and the only surviving
@@ -50,8 +50,7 @@ export declare function runPruneDryRun(db: Database.Database, decayHalfLifeDays?
50
50
  * with `importance` defaulting to `base_strength` (retrieval.ts). A memory
51
51
  * can EVER cross the prune floor (0.01, stageDecayPrune) only if that
52
52
  * asymptote is itself below it: base_strength² * 0.1 < 0.01, i.e.
53
- * base_strength < sqrt(0.1). Independent of access/link hardening — those
54
- * only slow the approach, they never raise the floor.
53
+ * base_strength < sqrt(0.1).
55
54
  */
56
55
  export declare const EVER_PRUNABLE_BASE_STRENGTH_CEILING: number;
57
56
  export declare function histogram(values: number[], edges?: number[]): Record<string, number>;
@@ -67,12 +66,14 @@ export interface StructuralStrengthStats {
67
66
  }
68
67
  /**
69
68
  * Compute effective-strength distributions now, and simulated further out
70
- * (same access/link counts, decay continuing with no further access — the
69
+ * (decay continuing with no further access or promotion — the
71
70
  * "if nothing changes" projection), plus the asymptotic floor per memory.
72
71
  * Requires `configureDecay` to already reflect the desired half-life (call
73
72
  * `runPruneDryRun` first, or `configureDecay` directly, in the same process).
73
+ * `now` (#458): the instant "now" means — pinned by run-eval's --now for
74
+ * wall-clock-independent before/after audits; default the live clock.
74
75
  */
75
- export declare function runStructuralStrengthStats(db: Database.Database): StructuralStrengthStats;
76
+ export declare function runStructuralStrengthStats(db: Database.Database, now?: Date): StructuralStrengthStats;
76
77
  export interface AdoptionStats {
77
78
  totalMemories: number;
78
79
  shownCountHistogram: Record<string, number>;
@@ -107,4 +108,4 @@ export interface AdoptionStats {
107
108
  * feature's youth as much as its effectiveness. Report callers should state
108
109
  * that caveat alongside the numbers, not just the numbers.
109
110
  */
110
- export declare function runAdoptionStats(db: Database.Database): AdoptionStats;
111
+ export declare function runAdoptionStats(db: Database.Database, now?: Date): AdoptionStats;
@@ -11,39 +11,6 @@
11
11
  * floor, now vs simulated further out
12
12
  * (d) adoption — #192 shown_count/access_count signal quality
13
13
  */
14
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
- if (k2 === undefined) k2 = k;
16
- var desc = Object.getOwnPropertyDescriptor(m, k);
17
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
- desc = { enumerable: true, get: function() { return m[k]; } };
19
- }
20
- Object.defineProperty(o, k2, desc);
21
- }) : (function(o, m, k, k2) {
22
- if (k2 === undefined) k2 = k;
23
- o[k2] = m[k];
24
- }));
25
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
- Object.defineProperty(o, "default", { enumerable: true, value: v });
27
- }) : function(o, v) {
28
- o["default"] = v;
29
- });
30
- var __importStar = (this && this.__importStar) || (function () {
31
- var ownKeys = function(o) {
32
- ownKeys = Object.getOwnPropertyNames || function (o) {
33
- var ar = [];
34
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
- return ar;
36
- };
37
- return ownKeys(o);
38
- };
39
- return function (mod) {
40
- if (mod && mod.__esModule) return mod;
41
- var result = {};
42
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
- __setModuleDefault(result, mod);
44
- return result;
45
- };
46
- })();
47
14
  Object.defineProperty(exports, "__esModule", { value: true });
48
15
  exports.EVER_PRUNABLE_BASE_STRENGTH_CEILING = void 0;
49
16
  exports.runDomainBacklogAudit = runDomainBacklogAudit;
@@ -53,7 +20,6 @@ exports.runStructuralStrengthStats = runStructuralStrengthStats;
53
20
  exports.runAdoptionStats = runAdoptionStats;
54
21
  const retrieval_js_1 = require("../retrieval.js");
55
22
  const consolidate_js_1 = require("../consolidate.js");
56
- const storage = __importStar(require("../storage.js"));
57
23
  const state_js_1 = require("../state.js");
58
24
  /**
59
25
  * Partition NULL-domain memories by the `domainCursor` rowid watermark.
@@ -106,8 +72,7 @@ function runPruneDryRun(db, decayHalfLifeDays = retrieval_js_1.DEFAULT_DECAY_HAL
106
72
  * with `importance` defaulting to `base_strength` (retrieval.ts). A memory
107
73
  * can EVER cross the prune floor (0.01, stageDecayPrune) only if that
108
74
  * asymptote is itself below it: base_strength² * 0.1 < 0.01, i.e.
109
- * base_strength < sqrt(0.1). Independent of access/link hardening — those
110
- * only slow the approach, they never raise the floor.
75
+ * base_strength < sqrt(0.1).
111
76
  */
112
77
  exports.EVER_PRUNABLE_BASE_STRENGTH_CEILING = Math.sqrt(0.1);
113
78
  const STRENGTH_BUCKET_EDGES = [0, 0.01, 0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0];
@@ -128,17 +93,17 @@ function histogram(values, edges = STRENGTH_BUCKET_EDGES) {
128
93
  }
129
94
  /**
130
95
  * Compute effective-strength distributions now, and simulated further out
131
- * (same access/link counts, decay continuing with no further access — the
96
+ * (decay continuing with no further access or promotion — the
132
97
  * "if nothing changes" projection), plus the asymptotic floor per memory.
133
98
  * Requires `configureDecay` to already reflect the desired half-life (call
134
99
  * `runPruneDryRun` first, or `configureDecay` directly, in the same process).
100
+ * `now` (#458): the instant "now" means — pinned by run-eval's --now for
101
+ * wall-clock-independent before/after audits; default the live clock.
135
102
  */
136
- function runStructuralStrengthStats(db) {
103
+ function runStructuralStrengthStats(db, now = new Date()) {
137
104
  const rows = db
138
- .prepare("SELECT id, base_strength, last_accessed, access_count FROM memories")
105
+ .prepare("SELECT id, base_strength, last_accessed FROM memories")
139
106
  .all();
140
- const linkCounts = storage.getAllLinkCounts(db);
141
- const now = new Date();
142
107
  const nowVals = [];
143
108
  const at180 = [];
144
109
  const at365 = [];
@@ -148,11 +113,9 @@ function runStructuralStrengthStats(db) {
148
113
  const base = r.base_strength ?? 0.5;
149
114
  if (base < exports.EVER_PRUNABLE_BASE_STRENGTH_CEILING)
150
115
  everPrunable++;
151
- const linkCount = linkCounts.get(r.id) ?? 0;
152
- const opts = { accessCount: r.access_count ?? 0, linkCount };
153
- nowVals.push((0, retrieval_js_1.effectiveStrength)(base, r.last_accessed, now, opts));
154
- at180.push((0, retrieval_js_1.effectiveStrength)(base, r.last_accessed, new Date(now.getTime() + 180 * 86_400_000), opts));
155
- at365.push((0, retrieval_js_1.effectiveStrength)(base, r.last_accessed, new Date(now.getTime() + 365 * 86_400_000), opts));
116
+ nowVals.push((0, retrieval_js_1.effectiveStrength)(base, r.last_accessed, now));
117
+ at180.push((0, retrieval_js_1.effectiveStrength)(base, r.last_accessed, new Date(now.getTime() + 180 * 86_400_000)));
118
+ at365.push((0, retrieval_js_1.effectiveStrength)(base, r.last_accessed, new Date(now.getTime() + 365 * 86_400_000)));
156
119
  neverVals.push(base * base * 0.1);
157
120
  }
158
121
  return {
@@ -185,11 +148,10 @@ function ageBucketLabel(ageDays) {
185
148
  * feature's youth as much as its effectiveness. Report callers should state
186
149
  * that caveat alongside the numbers, not just the numbers.
187
150
  */
188
- function runAdoptionStats(db) {
151
+ function runAdoptionStats(db, now = new Date()) {
189
152
  const rows = db
190
153
  .prepare("SELECT id, shown_count, access_count, source_agent, created_at FROM memories")
191
154
  .all();
192
- const now = new Date();
193
155
  const shownVals = [];
194
156
  const accessVals = [];
195
157
  let totalShown = 0;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * #458 — the shared pinned-clock helper for eval harnesses.
3
+ *
4
+ * The recall evals score against wall-clock time (recency + decay terms in
5
+ * computeScore/effectiveStrength), so a pre-photo and a post-run hours apart
6
+ * drift even on identical code — PR D evidenced mode-OFF units moving with no
7
+ * changed code in the path. The fix is a harness-visible pin: every eval that
8
+ * calls retrieve() (or the decay/adoption stats) accepts a `--now <ISO>` flag
9
+ * and threads the SAME instant into the retrieve() `now` seam (retrieval.ts —
10
+ * the injectable-clock idiom of run-deadline.ts), making before/after runs
11
+ * wall-clock-independent. Default stays the live clock — pinning is opt-in,
12
+ * for comparability.
13
+ *
14
+ * Fail-explicit by design (the repo convention): an invalid pin THROWS with a
15
+ * clear message rather than silently falling back to the live clock — a run
16
+ * that believed it was pinned but wasn't would silently reintroduce the drift
17
+ * class this helper exists to remove.
18
+ */
19
+ /**
20
+ * Parse a `--now` flag value into the pinned instant.
21
+ *
22
+ * @param raw the raw flag value. undefined or empty/whitespace → null (the
23
+ * live clock — the flag was not passed). Anything else must parse as a
24
+ * Date; garbage or an invalid instant (e.g. month 13) throws.
25
+ * @returns the pinned Date, or null for the live clock.
26
+ */
27
+ export declare function parsePinnedNow(raw: string | undefined): Date | null;
28
+ /**
29
+ * Human-readable clock mode for report headers / photo JSON: "pinned <ISO>"
30
+ * or "live" — so artifacts are self-describing about which clock produced them.
31
+ */
32
+ export declare function clockLabel(now: Date | null): string;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ /**
3
+ * #458 — the shared pinned-clock helper for eval harnesses.
4
+ *
5
+ * The recall evals score against wall-clock time (recency + decay terms in
6
+ * computeScore/effectiveStrength), so a pre-photo and a post-run hours apart
7
+ * drift even on identical code — PR D evidenced mode-OFF units moving with no
8
+ * changed code in the path. The fix is a harness-visible pin: every eval that
9
+ * calls retrieve() (or the decay/adoption stats) accepts a `--now <ISO>` flag
10
+ * and threads the SAME instant into the retrieve() `now` seam (retrieval.ts —
11
+ * the injectable-clock idiom of run-deadline.ts), making before/after runs
12
+ * wall-clock-independent. Default stays the live clock — pinning is opt-in,
13
+ * for comparability.
14
+ *
15
+ * Fail-explicit by design (the repo convention): an invalid pin THROWS with a
16
+ * clear message rather than silently falling back to the live clock — a run
17
+ * that believed it was pinned but wasn't would silently reintroduce the drift
18
+ * class this helper exists to remove.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.parsePinnedNow = parsePinnedNow;
22
+ exports.clockLabel = clockLabel;
23
+ /**
24
+ * Parse a `--now` flag value into the pinned instant.
25
+ *
26
+ * @param raw the raw flag value. undefined or empty/whitespace → null (the
27
+ * live clock — the flag was not passed). Anything else must parse as a
28
+ * Date; garbage or an invalid instant (e.g. month 13) throws.
29
+ * @returns the pinned Date, or null for the live clock.
30
+ */
31
+ function parsePinnedNow(raw) {
32
+ if (raw === undefined || raw.trim() === "")
33
+ return null;
34
+ const parsed = new Date(raw);
35
+ if (Number.isNaN(parsed.getTime())) {
36
+ throw new Error(`eval clock: invalid --now value "${raw}" — pass a full ISO 8601 instant ` +
37
+ `(e.g. 2026-09-17T09:00:00.000Z) or omit the flag to run on the live clock`);
38
+ }
39
+ return parsed;
40
+ }
41
+ /**
42
+ * Human-readable clock mode for report headers / photo JSON: "pinned <ISO>"
43
+ * or "live" — so artifacts are self-describing about which clock produced them.
44
+ */
45
+ function clockLabel(now) {
46
+ return now === null ? "live" : `pinned ${now.toISOString()}`;
47
+ }
@@ -8,6 +8,13 @@
8
8
  * corrected cosine formula, #145).
9
9
  */
10
10
  import type Database from "better-sqlite3";
11
+ /**
12
+ * Deterministic sample of `rows` (#460): a seeded partial Fisher–Yates
13
+ * shuffles the LAST `size` slots from the back, and that shuffled tail is
14
+ * the sample. Rows must already be in a stable order (the caller sorts by
15
+ * PK) so the draw does not depend on SQLite's scan order.
16
+ */
17
+ export declare function seededSample<T>(rows: T[], size: number, seed: number): T[];
11
18
  export interface LinkRow {
12
19
  source_id: string;
13
20
  target_id: string;
@@ -50,8 +57,14 @@ export interface DriftReport {
50
57
  maxAbsDrift: number;
51
58
  skippedMissingEmbedding: number;
52
59
  }
53
- /** Recompute cosine for a random link sample and compare to stored `strength` (post migration-5 rescale, should track closely). */
54
- export declare function runDriftSample(db: Database.Database, embeddings: Map<string, Float32Array>, sampleSize?: number): DriftReport;
60
+ /**
61
+ * Recompute cosine for a link sample and compare to stored `strength` (post
62
+ * migration-5 rescale, should track closely). The sample is deterministic
63
+ * (#460): all link rows in stable PK order, drawn by a seeded Fisher–Yates —
64
+ * two runs on the same build + snapshot produce an identical report, so
65
+ * before/after comparisons can require full-output identity.
66
+ */
67
+ export declare function runDriftSample(db: Database.Database, embeddings: Map<string, Float32Array>, sampleSize?: number, seed?: number): DriftReport;
55
68
  export interface PartitionStats {
56
69
  linkCount: number;
57
70
  byRelationship: Record<string, number>;