@gamaze/hicortex 0.21.0 → 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.
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
@@ -1548,7 +1548,7 @@ async function startServer(options = {}) {
1548
1548
  //
1549
1549
  // An enrich is EVIDENCE ABOUT IMPORTANCE: it bumps corroboration_count and
1550
1550
  // base_strength (+the calibration delta, capped at 1.0) — the same anchor
1551
- // the nightly's LLM scoring and hub-boost write. It must NOT touch
1551
+ // the nightly's LLM scoring writes. It must NOT touch
1552
1552
  // access_count (reserved for real recall use) or shown_count (index
1553
1553
  // exposure) — faking either corrupts the uses-per-showing adoption metric.
1554
1554
  // Never a stage write: stages are derived presentation (the E-reframe).
package/dist/nofit.d.ts CHANGED
@@ -38,7 +38,8 @@
38
38
  * memory from pruning. Halving does not touch last_accessed/access_count.
39
39
  * - Re-classification: a later run whose LLM tags it, or whose evolved
40
40
  * prototypes clear the floor, gives it a (weak) primary — halving stops.
41
- * Its strength is NOT restored; only access does that job.
41
+ * Its strength is restored only by the nightly promotion stage (#448),
42
+ * which converts each new access_count delta into a base_strength bump.
42
43
  *
43
44
  * PRUNE INTERACTION (verified against stageDecayPrune + effectiveStrength):
44
45
  * prune fires when effectiveStrength < 0.01 for a >90-day-old, never-accessed
package/dist/nofit.js CHANGED
@@ -39,7 +39,8 @@
39
39
  * memory from pruning. Halving does not touch last_accessed/access_count.
40
40
  * - Re-classification: a later run whose LLM tags it, or whose evolved
41
41
  * prototypes clear the floor, gives it a (weak) primary — halving stops.
42
- * Its strength is NOT restored; only access does that job.
42
+ * Its strength is restored only by the nightly promotion stage (#448),
43
+ * which converts each new access_count delta into a base_strength bump.
43
44
  *
44
45
  * PRUNE INTERACTION (verified against stageDecayPrune + effectiveStrength):
45
46
  * prune fires when effectiveStrength < 0.01 for a >90-day-old, never-accessed
@@ -10,8 +10,9 @@
10
10
  * Strengthening semantics (the recall/decay alignment):
11
11
  * - Appearing in the index = exposure: shown_count + last_accessed refresh
12
12
  * (mild, temporary strengthen — the decay clock resets) via
13
- * storage.touchMemoriesShown. NO access_count bump: hardening, the prune
14
- * shield, and the adoption metric stay driven by real use.
13
+ * storage.touchMemoriesShown. NO access_count bump: the promotion signal
14
+ * (#448), the prune shield, and the adoption metric stay driven by real
15
+ * use.
15
16
  * - hicortex_get = use: full strengthen (access_count + 1).
16
17
  *
17
18
  * Anti-bloat gates: relevance floor (measured cosine, or a real BM25 match),
@@ -11,8 +11,9 @@
11
11
  * Strengthening semantics (the recall/decay alignment):
12
12
  * - Appearing in the index = exposure: shown_count + last_accessed refresh
13
13
  * (mild, temporary strengthen — the decay clock resets) via
14
- * storage.touchMemoriesShown. NO access_count bump: hardening, the prune
15
- * shield, and the adoption metric stay driven by real use.
14
+ * storage.touchMemoriesShown. NO access_count bump: the promotion signal
15
+ * (#448), the prune shield, and the adoption metric stay driven by real
16
+ * use.
16
17
  * - hicortex_get = use: full strengthen (access_count + 1).
17
18
  *
18
19
  * Anti-bloat gates: relevance floor (measured cosine, or a real BM25 match),
@@ -208,7 +208,7 @@ export declare function l2ToCosine(distance: number): number;
208
208
  */
209
209
  export declare function cosineBetweenVectors(a: Float32Array, b: Float32Array): number;
210
210
  /**
211
- * Compute decayed strength with adaptive decay (B+E+D model).
211
+ * Compute decayed strength with adaptive decay (B+D model).
212
212
  * Exported for use by consolidation decay/prune stage.
213
213
  *
214
214
  * #425 read-side law: the decay-relevant importance is CLAMPED at the
@@ -216,11 +216,17 @@ export declare function cosineBetweenVectors(a: Float32Array, b: Float32Array):
216
216
  * exactly 1.0 the decay rate is exactly 1.0 and the row never decays, so
217
217
  * legacy base-1.0 rows (and any write site that predates the cap) decay
218
218
  * again. The clamp applies to explicit importance passes too.
219
+ *
220
+ * #448: the access/connectivity HARDENING terms are REMOVED — the fade rate
221
+ * no longer depends on access or link history (a briefly-used memory was
222
+ * near-immortal: 10 accesses cut the decay rate to 3% of normal). Real use
223
+ * now raises the STORED score via the nightly promotion stage
224
+ * (consolidate.ts stagePromotion) instead of slowing future decay, so a
225
+ * promoted-then-abandoned memory fades on the same 365-day clock as any
226
+ * other, from a higher anchor.
219
227
  */
220
228
  export declare function effectiveStrength(baseStrength: number, lastAccessed: string | null, now: Date, options?: {
221
229
  importance?: number;
222
- accessCount?: number;
223
- linkCount?: number;
224
230
  }): number;
225
231
  /**
226
232
  * Return a composite relevance score in [0, 1] for a candidate memory.