@gamaze/hicortex 0.20.10 → 0.22.0

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.
@@ -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,7 +66,7 @@ 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).
@@ -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,16 +93,15 @@ 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).
135
100
  */
136
101
  function runStructuralStrengthStats(db) {
137
102
  const rows = db
138
- .prepare("SELECT id, base_strength, last_accessed, access_count FROM memories")
103
+ .prepare("SELECT id, base_strength, last_accessed FROM memories")
139
104
  .all();
140
- const linkCounts = storage.getAllLinkCounts(db);
141
105
  const now = new Date();
142
106
  const nowVals = [];
143
107
  const at180 = [];
@@ -148,11 +112,9 @@ function runStructuralStrengthStats(db) {
148
112
  const base = r.base_strength ?? 0.5;
149
113
  if (base < exports.EVER_PRUNABLE_BASE_STRENGTH_CEILING)
150
114
  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));
115
+ nowVals.push((0, retrieval_js_1.effectiveStrength)(base, r.last_accessed, now));
116
+ at180.push((0, retrieval_js_1.effectiveStrength)(base, r.last_accessed, new Date(now.getTime() + 180 * 86_400_000)));
117
+ at365.push((0, retrieval_js_1.effectiveStrength)(base, r.last_accessed, new Date(now.getTime() + 365 * 86_400_000)));
156
118
  neverVals.push(base * base * 0.1);
157
119
  }
158
120
  return {
@@ -86,7 +86,7 @@ const ranking_battery_js_1 = require("./ranking-battery.js");
86
86
  const DEFAULT_PHOTO_PATH = (0, node_path_1.join)(process.cwd(), "data", "ranking-eval-photo.json");
87
87
  /** Battery gate thresholds (sweep protocol, issue #425 commit-3 gates). */
88
88
  const BATTERY_TOP1_STABILITY_MIN = 0.9;
89
- /** Plant one fixture corpus into a writable DB with its hardened metadata. */
89
+ /** Plant one fixture corpus into a writable DB with its seeded strength metadata. */
90
90
  async function plantRankingFixture(db, fixture) {
91
91
  const idByKey = new Map();
92
92
  const now = Date.now();
@@ -5,7 +5,7 @@
5
5
  * issue (the owner's live console test, 2026-09-13): a user searches a
6
6
  * DISTINCTIVE PROPER NOUN they know exists; the one memory carrying that
7
7
  * token has the best similarity of all candidates AND the literal FTS hit,
8
- * yet ranks behind hardened but less-relevant memories that win on
8
+ * yet ranks behind high-strength but less-relevant memories that win on
9
9
  * effective strength + graph connections.
10
10
  *
11
11
  * (a) sirnas_exact_match — the AC1 fixture: a low-strength, unlinked,
@@ -25,9 +25,9 @@
25
25
  * - The texts are synthetic and generic (this package publishes to npm —
26
26
  * no real infrastructure, people, or fleet detail), but SHAPED like the
27
27
  * field failure: the proper noun is invented (it must appear nowhere
28
- * else — verified by an invariant test), the rivals are hardened with
29
- * metadata the production pipeline itself writes (base strength, access
30
- * hardening, link counts).
28
+ * else — verified by an invariant test), the rivals carry the metadata
29
+ * the production pipeline itself writes (base strength, access history,
30
+ * link counts).
31
31
  * - Similarities are MEASURED at run time with the real bge-small-en-v1.5
32
32
  * embedder (ranking-eval.ts) — never asserted into existence. The
33
33
  * declared expectation is about ORDER under the shipped ranking, and
@@ -46,7 +46,7 @@ export interface RankingRow {
46
46
  memoryType: string;
47
47
  /** Birth base_strength — the value stageImportance would have written. */
48
48
  baseStrength: number;
49
- /** Access history (hardening + the decay clock). */
49
+ /** Access history (the decay clock + the #448 promotion baseline). */
50
50
  accessCount: number;
51
51
  /** Days before the run's `now` of the last access (0 = touched today). */
52
52
  lastAccessedDaysAgo: number;
@@ -6,7 +6,7 @@
6
6
  * issue (the owner's live console test, 2026-09-13): a user searches a
7
7
  * DISTINCTIVE PROPER NOUN they know exists; the one memory carrying that
8
8
  * token has the best similarity of all candidates AND the literal FTS hit,
9
- * yet ranks behind hardened but less-relevant memories that win on
9
+ * yet ranks behind high-strength but less-relevant memories that win on
10
10
  * effective strength + graph connections.
11
11
  *
12
12
  * (a) sirnas_exact_match — the AC1 fixture: a low-strength, unlinked,
@@ -26,9 +26,9 @@
26
26
  * - The texts are synthetic and generic (this package publishes to npm —
27
27
  * no real infrastructure, people, or fleet detail), but SHAPED like the
28
28
  * field failure: the proper noun is invented (it must appear nowhere
29
- * else — verified by an invariant test), the rivals are hardened with
30
- * metadata the production pipeline itself writes (base strength, access
31
- * hardening, link counts).
29
+ * else — verified by an invariant test), the rivals carry the metadata
30
+ * the production pipeline itself writes (base strength, access history,
31
+ * link counts).
32
32
  * - Similarities are MEASURED at run time with the real bge-small-en-v1.5
33
33
  * embedder (ranking-eval.ts) — never asserted into existence. The
34
34
  * declared expectation is about ORDER under the shipped ranking, and
@@ -50,13 +50,13 @@ exports.RANKING_SOURCE_AGENT = "ranking-eval";
50
50
  // The target mirrors the field case's SHAPE with invented names: a home
51
51
  // harbour memory carrying one distinctive proper noun, born weak (base 0.35,
52
52
  // the "routine-but-curated" band), never accessed, unlinked, a few months
53
- // old. The rivals are what the decay model produces for early-hardened
53
+ // old. The rivals are what the decay model produces for early high-strength
54
54
  // technical rows: base 0.90-0.95, mutual links, access history, created
55
55
  // BEFORE the target. They share the harbour/boating domain vocabulary — so
56
56
  // a bare proper-noun query lands in their neighborhood — but none carries
57
57
  // the proper noun.
58
58
  const SIRNAS_ROWS = [
59
- // --- rivals: hardened, connected, older, domain-adjacent ---
59
+ // --- rivals: high-strength, connected, older, domain-adjacent ---
60
60
  {
61
61
  key: "rival_fees",
62
62
  content: "Harbour fees and guest dock rules: pay at the machine by the fuel quay, electricity on the guest dock is metered, " +
@@ -158,7 +158,7 @@ const SIRNAS_ROWS = [
158
158
  exports.SIRNAS_FIXTURE = {
159
159
  key: "sirnas_exact_match",
160
160
  description: "AC1: a distinctive proper-noun query must rank the exact-match (both-channel) memory first, " +
161
- "ahead of hardened domain-adjacent rivals that win on strength + connections.",
161
+ "ahead of high-strength domain-adjacent rivals that win on strength + connections.",
162
162
  rows: SIRNAS_ROWS,
163
163
  query: "Vindarsvik",
164
164
  distinctiveToken: "vindarsvik",
@@ -93,7 +93,7 @@ function renderDecay(backlog, prune, structural, adoption) {
93
93
  lines.push("### Structural strength — who can EVER prune\n");
94
94
  lines.push(`A memory can only ever cross the prune floor (effective strength < 0.01) if its asymptote is below that floor: ` +
95
95
  `\`base_strength < sqrt(0.1) ≈ ${round(structural.everPrunableCeiling, 4)}\` (floor = base_strength × importance × 0.1, ` +
96
- `importance defaults to base_strength — independent of access/link hardening, which only slows the approach).\n`);
96
+ `importance defaults to base_strength).\n`);
97
97
  lines.push(`**Ever-prunable: ${structural.everPrunableCount} / ${structural.totalMemories} (${pct(structural.totalMemories > 0 ? structural.everPrunableCount / structural.totalMemories : 0)})** — everyone else has a strength floor that never drops low enough to prune, no matter how long it goes unaccessed.\n`);
98
98
  for (const [label, hist] of [
99
99
  ["Effective strength now", structural.effectiveStrengthNowHistogram],
package/dist/graph.d.ts CHANGED
@@ -103,8 +103,7 @@ export interface ExportGraphOptions {
103
103
  * Nodes are ranked by effective (decayed) strength and capped at `limit`.
104
104
  * Edges include only links where BOTH endpoints made the cut.
105
105
  * Link counts come from one aggregate query over memory_links (no per-row
106
- * queries) and feed both the effectiveStrength hardening term and the
107
- * per-node linkCount field.
106
+ * queries) and feed the per-node linkCount field.
108
107
  */
109
108
  export declare function exportGraph(db: Database.Database, options?: ExportGraphOptions): VizGraph;
110
109
  export declare function shortestPath(db: Database.Database, fromId: string, toId: string, maxDepth?: number): string[] | null;
package/dist/graph.js CHANGED
@@ -232,8 +232,7 @@ function makeLabel(content) {
232
232
  * Nodes are ranked by effective (decayed) strength and capped at `limit`.
233
233
  * Edges include only links where BOTH endpoints made the cut.
234
234
  * Link counts come from one aggregate query over memory_links (no per-row
235
- * queries) and feed both the effectiveStrength hardening term and the
236
- * per-node linkCount field.
235
+ * queries) and feed the per-node linkCount field.
237
236
  */
238
237
  function exportGraph(db, options = {}) {
239
238
  const limit = Math.min(Math.max(Math.floor(options.limit ?? exports.EXPORT_DEFAULT_LIMIT), 1), exports.EXPORT_MAX_LIMIT);
@@ -249,7 +248,7 @@ function exportGraph(db, options = {}) {
249
248
  const linkCounts = new Map(linkCountRows.map((r) => [r.id, r.cnt]));
250
249
  // Candidate memories — domain/type filters pushed into SQL
251
250
  let sql = `SELECT id, content, memory_type, domain, project, base_strength,
252
- last_accessed, access_count, created_at
251
+ last_accessed, created_at
253
252
  FROM memories`;
254
253
  const where = [];
255
254
  const params = [];
@@ -275,7 +274,7 @@ function exportGraph(db, options = {}) {
275
274
  // Score, filter on effective strength, rank, cap
276
275
  const scored = rows.map((row) => {
277
276
  const linkCount = linkCounts.get(row.id) ?? 0;
278
- const strength = (0, retrieval_js_1.effectiveStrength)(row.base_strength ?? 0.5, row.last_accessed, now, { accessCount: row.access_count ?? 0, linkCount });
277
+ const strength = (0, retrieval_js_1.effectiveStrength)(row.base_strength ?? 0.5, row.last_accessed, now);
279
278
  return { row, strength, linkCount };
280
279
  });
281
280
  const filtered = options.minStrength !== undefined