@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
@@ -15,17 +15,22 @@
15
15
  PORTED FROM mockups/409-r6-flight.html (the design of record — its banner
16
16
  documents every behavior; the engine below is that page with the seeded
17
17
  data layer replaced by fetches). Live-data substitutions:
18
- /dashboard/data?range=90d — band numbers, snapshot series, digest
18
+ /dashboard/data?range=Rd — band numbers, snapshot series, digest
19
19
  /dashboard/field — the whole field (memories + real links)
20
- /dashboard/events?days=90 — the replay ledger (real nights)
20
+ /dashboard/events?days=R — the replay ledger (real nights)
21
21
  /health + /health/detail — live dot (30 s poll) + model label
22
22
  /account + /account/token — the account menu (harvested from 0.20.4)
23
23
  /search /memory /update /delete /identity — the browser + agent rail
24
+ (R = the range selector's value: 30 by default, 7/90/180 opt-in — #452)
24
25
 
25
26
  PHASE 2 (#422 — operator surfaces) LANDED:
26
27
  - Capture health: real /distill accounting rows (posts · sessions · MB ·
27
28
  held per machine × harness) via /dashboard/data capture_health; the
28
29
  phase-1 counts-only rows remain as the degrade.
30
+ Fix round 7 (#409, owner ruling 2026-09-14 — "the normal 30 day as the
31
+ other cards"): the card's face is the ROLLING window (window_rows,
32
+ window_days), the newest night rides as a per-row secondary suffix;
33
+ retention == window (CAPTURE_HEALTH_WINDOW_DAYS).
29
34
  - Activity bars: five segments (added + merged + the three graduation
30
35
  deltas from snapshot stage_counts; two-segment rendering when buckets
31
36
  are absent).
@@ -132,6 +137,30 @@
132
137
  stays marked; the visible word failed to communicate). Vocabulary
133
138
  amendment: Low / HEALTHY / Overfetching. Marker + trend stay.
134
139
 
140
+ REPLAY + RANGE (#452, owner directives 2026-09-16):
141
+ - 30-DAY DEFAULT FOR ALL VIEWS: the replay ledger, the band and the
142
+ snapshot series all default to 30 days (server EVENTS_DEFAULT_DAYS
143
+ 90 → 30; the client's every fetch is selector-driven). Longer
144
+ ranges are OPT-IN via a compact 7 / 30 / 90 / 180 segmented control
145
+ in the replay foot — explicit click only, never auto-loaded
146
+ (SUPERSEDES the 2026-09-12 no-dropdowns ruling). No persistence:
147
+ every load starts at 30 (loads fast).
148
+ - PLAY ONCE, THEN REST: the replay auto-starts as before (~3.6 s after
149
+ boot), walks the ledger ONCE to today, and STOPS there — no loop
150
+ (SUPERSEDES the 2026-09-13 auto-loop/no-buttons ruling). Controls in
151
+ the replay foot, styled like "return to now": pause/resume (holds
152
+ position mid-cruise; hides once the playback completed — nothing is
153
+ playing) and "replay again" (appears once completed; restarts from
154
+ the range start). Drag/scrub behavior is UNCHANGED: a manual take
155
+ pauses the cruise and re-engages ~12 s after idle, continuing
156
+ toward today — where it now stops instead of looping.
157
+ - DATE LABEL ON THE SCRUBBER: the cursor dot carries a small mono
158
+ label above it (the night being viewed, "now" at the end).
159
+ - TRUTH-MANAGEMENT CAPTIONS: Superseded / Retracted / Rewritten ride
160
+ the events ledger alongside learned/enriched/merged (server-side
161
+ sources in dashboard.ts; superseded/retracted bloom in the fading
162
+ color — demoted — rewritten in the truth color).
163
+
135
164
  REMAINING DEGRADATIONS (planned, per the #409 phase table — not gaps):
136
165
  - Recall card: no raw shown/used counts (the per-memory adoption sums
137
166
  are not in any payload yet).
@@ -561,11 +590,25 @@
561
590
  pointer-events:none; transition:opacity 1.4s; z-index:5;
562
591
  }
563
592
  .hint em{ font-style:normal; color:var(--dim); }
564
- /* replay — inside the brain panel; drag-only (no play button anywhere) */
593
+ /* replay — inside the brain panel; drag to scrub, or let the cruise walk
594
+ the ledger ONCE to today (#452: range pills + pause / replay again) */
565
595
  .replay-foot{ border-top:1px solid var(--line); padding:7px 12px 8px; flex:none; background:color-mix(in srgb, var(--panel) 88%, transparent); }
566
596
  #replay{ display:flex; align-items:center; gap:12px; }
567
597
  #replay.off{ opacity:.45; pointer-events:none; }
598
+ /* the range selector stays LIVE even when the instrument is off — a wider
599
+ window can reveal ≥2 nights and revive the replay (#452) */
600
+ #replay.off #rp-range{ opacity:1; pointer-events:auto; }
568
601
  #rp-label{ font-size:10px; color:var(--faint); text-transform:uppercase; letter-spacing:.14em; white-space:nowrap; flex:none; }
602
+ /* #452 range selector — the .idpill.seg idiom (mono pills, active = green) */
603
+ #rp-range{ display:inline-flex; gap:3px; flex:none; }
604
+ .rp-seg{
605
+ font-size:9px; font-family:var(--mono); letter-spacing:.05em; color:var(--faint);
606
+ border:1px solid var(--line2); border-radius:999px; padding:3px 8px; white-space:nowrap;
607
+ transition:color .15s, border-color .15s;
608
+ }
609
+ .rp-seg:hover{ color:var(--dim); }
610
+ .rp-seg[aria-pressed="true"]{ color:var(--green-mid); border-color:color-mix(in srgb, var(--green) 40%, transparent); }
611
+ html:not([data-theme="light"]) .rp-seg[aria-pressed="true"]{ box-shadow:0 0 10px rgba(61,255,122,.12); }
569
612
  #rp-track{ flex:1; position:relative; height:26px; cursor:pointer; touch-action:none; min-width:60px; }
570
613
  #rp-rail{ position:absolute; left:0; right:0; top:11px; height:3px; border-radius:2px; background:var(--tbar-bg); }
571
614
  #rp-fill{ position:absolute; left:0; top:11px; height:3px; border-radius:2px; width:100%;
@@ -577,14 +620,23 @@
577
620
  }
578
621
  html:not([data-theme="light"]) #rp-cursor{ background:#0d1f14; box-shadow:0 0 12px rgba(61,255,122,.7); }
579
622
  html[data-theme="light"] #rp-cursor{ box-shadow:0 1px 4px rgba(56,54,42,.3); }
623
+ /* #452: the night being viewed, floating above the scrub dot (the
624
+ #rp-dates span styling; overflow is visible — it may overlap the canvas
625
+ bottom edge like .hint does) */
626
+ #rp-date{
627
+ position:absolute; top:-13px; transform:translateX(-50%); white-space:nowrap;
628
+ font-size:9px; color:var(--faint); font-family:var(--mono); pointer-events:none; z-index:3;
629
+ display:none;
630
+ }
631
+ #rp-date.show{ display:block; }
580
632
  .rp-dot{ position:absolute; top:9px; width:5px; height:5px; border-radius:50%; margin-left:-2.5px; background:var(--line2); }
581
633
  .rp-dot.g{ background:var(--green-mid); }
582
634
  .rp-dot.w{ background:var(--dim); }
583
635
  #rp-dates{ position:absolute; left:0; right:0; top:21px; height:12px; }
584
636
  #rp-dates span{ position:absolute; transform:translateX(-50%); font-size:9px; color:var(--faint); font-family:var(--mono); }
585
- #rp-now{ display:none; font-size:10.5px; color:var(--green-mid); border:1px solid color-mix(in srgb, var(--green) 40%, transparent); border-radius:999px; padding:5px 11px; white-space:nowrap; }
586
- #rp-now.show{ display:inline-block; }
587
- html:not([data-theme="light"]) #rp-now{ box-shadow:0 0 12px rgba(61,255,122,.15); }
637
+ #rp-now,#rp-pause,#rp-again{ display:none; font-size:10.5px; color:var(--green-mid); border:1px solid color-mix(in srgb, var(--green) 40%, transparent); border-radius:999px; padding:5px 11px; white-space:nowrap; }
638
+ #rp-now.show,#rp-pause.show,#rp-again.show{ display:inline-block; }
639
+ html:not([data-theme="light"]) #rp-now,html:not([data-theme="light"]) #rp-pause,html:not([data-theme="light"]) #rp-again{ box-shadow:0 0 12px rgba(61,255,122,.15); }
588
640
 
589
641
  /* ---- agents rail — the ONLY thing beside the brain, rightmost ---- */
590
642
  #agentscol{
@@ -768,7 +820,7 @@
768
820
  /* capture health — phase 2: per machine × harness rows from /distill
769
821
  accounting (posts/sessions/MB/held); the phase-1 counts-only rows remain
770
822
  as the degrade for old servers / nothing recorded yet */
771
- .cap-row2{ display:flex; align-items:center; gap:9px; padding:2.5px 0; border-bottom:1px solid color-mix(in srgb, var(--line) 60%, transparent); }
823
+ .cap-row2{ display:flex; flex-wrap:wrap; align-items:center; gap:1px 9px; padding:2.5px 0; border-bottom:1px solid color-mix(in srgb, var(--line) 60%, transparent); }
772
824
  .cap-row2:last-of-type{ border-bottom:none; }
773
825
  .cap-dot{ width:6px; height:6px; border-radius:50%; flex:none; background:var(--green-mid); }
774
826
  .cap-row2 .hn{ font:11px var(--mono); color:var(--text); flex:0 0 148px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
@@ -779,6 +831,12 @@
779
831
  .cap-bar .fill{ height:100%; border-radius:2.5px; background:var(--green-mid); }
780
832
  .cap-row2 .st{ font-size:10px; color:var(--faint); font-family:var(--mono); white-space:nowrap; }
781
833
  .cap-row2 .st b{ color:var(--dim); font-weight:500; }
834
+ /* fix round 7: the newest-night sub-line on the rolling-window rows — a
835
+ full-basis child wraps under the row's primary line (the suffix does NOT
836
+ fit beside "N sess · X MB" without pushing past the card edge); same
837
+ faint mono tone, theme token only, both themes free. Rows without a
838
+ newest night stay single-line. */
839
+ .cap-row2 .cap-ton{ flex-basis:100%; text-align:right; font:10px var(--mono); color:var(--faint); line-height:1.1; }
782
840
  .cap-held{ color:var(--amber); margin-left:5px; }
783
841
  /* #423 phase 3: the capture-pause badge — the same amber family as the
784
842
  held badge (an operator state, not an error) */
@@ -963,6 +1021,7 @@
963
1021
  .brow{ grid-template-columns:1fr; }
964
1022
  .mrow .sn{ font-size:10px; }
965
1023
  #rp-label{ display:none; }
1024
+ #replay{ flex-wrap:wrap; } /* #452: range pills + controls wrap rather than crush the track */
966
1025
  #rp-dates span:nth-child(n+3){ display:none; }
967
1026
  .acct-trigger .who{ display:none; }
968
1027
  .field-head{ padding:8px 12px 7px; }
@@ -1180,13 +1239,17 @@
1180
1239
  </div>
1181
1240
  <div class="replay-foot">
1182
1241
  <div id="replay">
1242
+ <div id="rp-range" role="group" aria-label="range"></div>
1183
1243
  <span id="rp-label">nights</span>
1184
1244
  <div id="rp-track">
1185
1245
  <div id="rp-rail"></div>
1186
1246
  <div id="rp-fill"></div>
1187
1247
  <div id="rp-dates"></div>
1188
1248
  <div id="rp-cursor"></div>
1249
+ <span id="rp-date"></span>
1189
1250
  </div>
1251
+ <button id="rp-pause">pause</button>
1252
+ <button id="rp-again">replay again</button>
1190
1253
  <button id="rp-now">return to now</button>
1191
1254
  </div>
1192
1255
  </div>
@@ -1307,7 +1370,7 @@ if(STILL){ document.body.classList.remove('booting'); document.body.classList.ad
1307
1370
  /* =====================================================================
1308
1371
  The approved #409 r6-flight engine, ported to live data. One canvas,
1309
1372
  three verbs (look / travel / time) plus the quiet fourth: WATCH — ambient
1310
- signals loop slowly along REAL stored links, never behind a play button.
1373
+ signals loop slowly along REAL stored links, always on.
1311
1374
  Stages are DERIVED presentation (server-side via /dashboard/field) over
1312
1375
  the one numeric strength signal — never stored state.
1313
1376
  ===================================================================== */
@@ -1328,9 +1391,15 @@ function fmtTokens(n){
1328
1391
  return String(n);
1329
1392
  }
1330
1393
  /* deterministic per-id randomness — the same memory always lands in the
1331
- same place across reloads (positions are content of the field, not noise) */
1394
+ same place across reloads (positions are content of the field, not noise).
1395
+ #465: the salt hashes at the FRONT (hash32(salt+':'+s)) — FNV-1a does not
1396
+ diffuse a trailing-byte difference, and salts 1/2/3 differ only in their
1397
+ last character, so the old trailing form coupled angle to radius at
1398
+ Pearson r=0.953 (four spiral arms per domain). Salt-first measures
1399
+ r=0.019 (noise). Positions shift once (hash-derived content), then
1400
+ re-stabilize. */
1332
1401
  function hash32(s){ let h=2166136261; for(let i=0;i<s.length;i++){ h^=s.charCodeAt(i); h=Math.imul(h,16777619);} return h>>>0; }
1333
- function rand01(s,salt){ return hash32(s+':'+salt)/4294967296; }
1402
+ function rand01(s,salt){ return hash32(salt+':'+s)/4294967296; }
1334
1403
 
1335
1404
  /* ---------- token handling (harvested verbatim from the 0.20.4 console:
1336
1405
  ?token= handoff stripped from the URL, localStorage persistence, 401 →
@@ -1424,9 +1493,12 @@ function bubbleRadius(s){ return 6 + Math.sqrt(clamp(s??0,0,1))*3; }
1424
1493
  function depthSizeDivisor(dz){ return Math.pow(Math.max(.14,dz),0.35); }
1425
1494
 
1426
1495
  /* ---------- live-data state ---------- */
1427
- let D=null; // /dashboard/data?range=90d
1496
+ let RANGE=30; // days — the range selector's value (#452; 30-day default for
1497
+ // ALL views, owner directive 2026-09-16). NO persistence: every
1498
+ // load starts at 30 — it loads fast; longer is opt-in per click.
1499
+ let D=null; // /dashboard/data?range=RANGE d — band, series, digest
1428
1500
  let F=null; // /dashboard/field
1429
- let EV=null; // /dashboard/events?days=90
1501
+ let EV=null; // /dashboard/events?days=RANGE — the replay ledger
1430
1502
  let M=null; // /dashboard/model (#422) — null on failure: label-only degrade, no configure pill
1431
1503
  let TOTAL=0, removedNow=0;
1432
1504
  /* the store's static LIVE count (headline.live_memories ?? total): the
@@ -1637,13 +1709,19 @@ function buildWorld(){
1637
1709
 
1638
1710
  // the flattened replay ledger (events within a night fire in cruise order);
1639
1711
  // added/merged reuse the staggered born/merge times so blooms and births
1640
- // coincide; enriched/linked carry their own salted staggers.
1712
+ // coincide; enriched/linked carry their own salted staggers. #452 adds the
1713
+ // truth-management captions — stagger order enriched .55 < retracted .65 <
1714
+ // superseded .70 < rewritten .75 < learned .80 keeps the cruise sane (each
1715
+ // new type gets its own salt, so per-id positions never collide).
1641
1716
  EVENTS = [];
1642
1717
  NIGHTS.forEach((n,i)=>{
1643
1718
  for(const id of n.added) EVENTS.push({t:bornOf.get(id), type:'learned', id, cap:'Learned — “'+trunc(titleOf(id),52)+'”'});
1644
1719
  for(const id of n.enriched) EVENTS.push({t:i+.55+stag(id,19)*.08, type:'enriched', id, cap:'Enriched — “'+trunc(titleOf(id),52)+'”'});
1645
1720
  for(const mg of n.merged) EVENTS.push({t:mergeOf.get(mg.loser).t, type:'merged', loser:mg.loser, id:mg.loser, cap:'Merged — “'+trunc(titleOf(mg.loser),40)+'”'});
1646
1721
  for(const lk of n.linked) EVENTS.push({t:i+.90+stag(lk.a+lk.b,23)*.08, type:'linked', a:lk.a, b:lk.b, cap:null});
1722
+ for(const sp of n.superseded) EVENTS.push({t:i+.70+stag(sp.old,29)*.08, type:'superseded', id:sp.old, cap:'Superseded — “'+trunc(titleOf(sp.old),52)+'”'});
1723
+ for(const rt of n.retracted) EVENTS.push({t:i+.65+stag(rt.old,31)*.08, type:'retracted', id:rt.old, cap:'Retracted — “'+trunc(titleOf(rt.old),52)+'”'});
1724
+ for(const id of n.rewritten) EVENTS.push({t:i+.75+stag(id,37)*.08, type:'rewritten', id, cap:'Rewritten — “'+trunc(titleOf(id),52)+'”'});
1647
1725
  });
1648
1726
  EVENTS.sort((x,y)=>x.t-y.t);
1649
1727
 
@@ -2487,14 +2565,21 @@ function toast(msg, ms){
2487
2565
  function wake(){ lastActivity=performance.now(); }
2488
2566
 
2489
2567
  /* ---------- replay — time travel: drag to take the helm, else the G1
2490
- auto-cruise walks the REAL event ledger (one memory action at a time,
2491
- looping; still NO play button — cruising is the ambient default).
2568
+ auto-cruise walks the REAL event ledger (one memory action at a time)
2569
+ ONCE to today, then rests there (#452, owner ruling 2026-09-16 —
2570
+ supersedes the 2026-09-13 auto-loop). Pause/resume + "replay again" sit
2571
+ in the foot beside "return to now"; a manual take still pauses the
2572
+ cruise and re-engages ~12 s after idle, continuing toward today — where
2573
+ it now stops instead of looping.
2492
2574
  With fewer than 2 nights the instrument renders its "now" state and
2493
2575
  disables itself (degrade quietly). ---------- */
2494
2576
  const track=el('rp-track');
2495
2577
  function trackFrac(e){ const r=track.getBoundingClientRect(); return clamp((e.clientX-r.left)/r.width,0,1); }
2496
2578
  let autoPlay=false, autoEvIdx=0, autoNextAt=0, manualUntil=3600, pendingApply=null;
2579
+ let paused=false; /* #452: pause holds the cruise position; resume continues */
2580
+ let played=false; /* #452: a full playback completed → rest at today, never loop */
2497
2581
  function manualTake(){ autoPlay=false; pendingApply=null; manualUntil=performance.now()+12000; wake(); }
2582
+ function setPaused(v){ paused=v; el('rp-pause').textContent=paused?'resume':'pause'; }
2498
2583
  /* HERALD — before each cruise step's change, a bright pulse flies in along
2499
2584
  a REAL link touching the target (else from its nearest same-domain
2500
2585
  neighbor) and LANDS: the update we see is the arrival */
@@ -2518,18 +2603,25 @@ function heraldTo(id){
2518
2603
  signals.push({ path:p, i:0, s:0, dur:.45, tail:[], showcase:true, herald:true }); /* dur is seconds */
2519
2604
  }
2520
2605
  function autoTick(now){
2521
- if(REDUCED||booting||!REPLAY_OK) return;
2606
+ if(REDUCED||booting||!REPLAY_OK||paused) return;
2522
2607
  if(!autoPlay){ if(now>manualUntil && !scrubbing) autoPlay=true; else return; }
2523
2608
  if(pendingApply){
2524
2609
  if(now>=pendingApply.at){ seek(pendingApply.t); pendingApply=null; autoNextAt=now+820; }
2525
2610
  return; /* the herald is in flight */
2526
2611
  }
2527
2612
  if(now<autoNextAt) return;
2528
- if(T>=NOW_T-.001){ /* a beat at "now", then loop from the start */
2613
+ if(T>=NOW_T-.001){
2614
+ /* a beat at "now": re-kick to the range start ONLY before the first
2615
+ full playback; after it, REST here — plays once, never loops (#452) */
2616
+ if(played){ autoPlay=false; autoNextAt=now+3200; return; }
2529
2617
  autoEvIdx=0; autoNextAt=now+3200; seek(.001); return;
2530
2618
  }
2531
2619
  while(autoEvIdx<EVENTS.length && EVENTS[autoEvIdx].t<=T+.0001) autoEvIdx++;
2532
- if(autoEvIdx>=EVENTS.length){ autoNextAt=now+3200; seek(WINDOW); return; }
2620
+ if(autoEvIdx>=EVENTS.length){
2621
+ /* ledger exhausted: arrive at today and complete the playback (the
2622
+ flags go BEFORE the seek so onTChanged's control swap sees them) */
2623
+ autoNextAt=now+3200; played=true; autoPlay=false; seek(WINDOW); return;
2624
+ }
2533
2625
  const ev=EVENTS[autoEvIdx++];
2534
2626
  if(ev.id && signals.length<8){ heraldTo(ev.id); pendingApply={t:ev.t+.001, at:now+430}; }
2535
2627
  else { seek(ev.t+.001); autoNextAt=now+820; }
@@ -2550,7 +2642,10 @@ function onTChanged(fromSeek){
2550
2642
  const b=mems.get(ev.id);
2551
2643
  if(!b||!aliveAt(b,ev.t)||b._sx===undefined) continue;
2552
2644
  if(blooms.length>=6) break;
2553
- const rgb= ev.type==='merged'? PAL.stages.fading.rgb : (ev.type==='learned'? PAL.stages.forming.rgb : PAL.stages.truth.rgb);
2645
+ /* #452: superseded/retracted bloom in the FADING color (demoted
2646
+ claims); rewritten falls to the default truth color (a rewrite is
2647
+ the claim surviving, corrected) */
2648
+ const rgb= ev.type==='merged'||ev.type==='superseded'||ev.type==='retracted'? PAL.stages.fading.rgb : (ev.type==='learned'? PAL.stages.forming.rgb : PAL.stages.truth.rgb);
2554
2649
  blooms.push({x:b._sx,y:b._sy,r0:2,r1:24+b._sr,t0:performance.now(),dur:1400,cap:ev.cap,rgb});
2555
2650
  }
2556
2651
  }
@@ -2559,6 +2654,18 @@ function onTChanged(fromSeek){
2559
2654
  el('rp-cursor').style.left=(T/WINDOW*100)+'%';
2560
2655
  const isNow=T>=NOW_T-.001;
2561
2656
  el('rp-now').classList.toggle('show',!isNow&&REPLAY_OK);
2657
+ /* #452 date label above the scrub dot — same left % as the cursor, the
2658
+ night being viewed ("now" at the end); hidden when there is no ledger */
2659
+ const dlab=el('rp-date');
2660
+ if(REPLAY_OK&&NIGHTS.length){
2661
+ dlab.style.left=(T/WINDOW*100)+'%';
2662
+ dlab.textContent=isNow?'now':fmtDay(NIGHTS[Math.min(Math.floor(T),NIGHTS.length-1)].date);
2663
+ dlab.classList.add('show');
2664
+ }else dlab.classList.remove('show');
2665
+ /* #452 lifecycle pills: pause while a playback is live, "replay again"
2666
+ once it completed — at most ONE of them beside "return to now" */
2667
+ el('rp-pause').classList.toggle('show',REPLAY_OK&&!played);
2668
+ el('rp-again').classList.toggle('show',REPLAY_OK&&played);
2562
2669
  updateMeasures();
2563
2670
  /* #422 owner ruling: the overview counter is STATIC (the store's live
2564
2671
  count, set once at boot + after writes) — scrubbing the replay no longer
@@ -2566,10 +2673,13 @@ function onTChanged(fromSeek){
2566
2673
  dirty=true;
2567
2674
  }
2568
2675
  /* warp-to-now — accelerates through every remaining event, blooms firing,
2569
- and arrives ~2.4 s later (ease in-out) */
2676
+ and arrives ~2.4 s later (ease in-out). #452: arriving at now by hand
2677
+ completes the playback too — the cruise rests instead of looping after */
2570
2678
  let warpUntil=0, warpFrom=0;
2571
2679
  function warpToNow(){
2572
2680
  manualTake();
2681
+ setPaused(false);
2682
+ played=true;
2573
2683
  if(REDUCED||T>=NOW_T-.001||!REPLAY_OK){ seek(WINDOW); return; }
2574
2684
  warpFrom=T; warpUntil=performance.now()+2400;
2575
2685
  }
@@ -2582,6 +2692,72 @@ function warpTick(now){
2582
2692
  }
2583
2693
  el('rp-now').onclick=warpToNow;
2584
2694
 
2695
+ /* #452 — pause/resume + replay again. Pause holds the cruise position
2696
+ (autoTick returns early, pending heralds included); resume continues
2697
+ toward today. Replay again restarts from the range start and plays once.
2698
+ Visibility is onTChanged's job: pause renders only while a playback is
2699
+ live, "replay again" only once one completed — least clutter. */
2700
+ el('rp-pause').onclick=()=>{
2701
+ setPaused(!paused);
2702
+ if(!paused) autoPlay=true; /* resume continues the cruise immediately */
2703
+ wake();
2704
+ };
2705
+ el('rp-again').onclick=()=>{
2706
+ manualTake(); setPaused(false);
2707
+ played=false; autoEvIdx=0; pendingApply=null; autoPlay=true;
2708
+ seek(.001); wake();
2709
+ };
2710
+
2711
+ /* #452 — the range selector: 7/30/90/180 days (the owner-fixed option
2712
+ set), default 30, NO persistence — every load starts at 30 (loads fast).
2713
+ A click refetches BOTH range-scoped sources and rebuilds the world
2714
+ exactly like boot — the agents rail included (its "last night" line
2715
+ reads NIGHTS, so it must follow the new window); the replay then
2716
+ auto-starts on the new range via the idle re-engage (mirrors boot).
2717
+ Fail-soft: a failed fetch toasts, keeps the OLD data and the OLD pills
2718
+ — never a half-switched view. One switch at a time (refreshAfterWrite's
2719
+ refreshInFlight pattern): a click while a switch is in flight is
2720
+ IGNORED — a racing second fetch could resolve out of order and leave
2721
+ pills and data disagreeing. */
2722
+ const RANGE_OPTIONS=[7,30,90,180];
2723
+ function renderRangePills(){
2724
+ const host=el('rp-range'); host.innerHTML='';
2725
+ for(const n of RANGE_OPTIONS){
2726
+ const b=document.createElement('button');
2727
+ b.type='button'; b.className='rp-seg'; b.textContent=n+'d';
2728
+ b.title='last '+n+' days';
2729
+ b.setAttribute('aria-pressed', String(n===RANGE));
2730
+ b.onclick=()=>{ if(n!==RANGE) switchRange(n); };
2731
+ host.appendChild(b);
2732
+ }
2733
+ }
2734
+ let rangeSwitchInFlight=false;
2735
+ async function switchRange(n){
2736
+ if(rangeSwitchInFlight) return; rangeSwitchInFlight=true;
2737
+ const prev=RANGE; RANGE=n; renderRangePills();
2738
+ try{
2739
+ const [dd,ee]=await Promise.all([
2740
+ api('/dashboard/data?range='+RANGE+'d'),
2741
+ api('/dashboard/events?days='+RANGE),
2742
+ ]);
2743
+ D=dd;
2744
+ if(ee&&Array.isArray(ee.nights)){ EV=ee; NIGHTS=EV.nights; } else { NIGHTS=[]; }
2745
+ }catch(e){
2746
+ RANGE=prev; renderRangePills();
2747
+ toast('Range switch failed: '+e.message);
2748
+ return;
2749
+ }finally{
2750
+ rangeSwitchInFlight=false;
2751
+ }
2752
+ buildWorld(); buildPaths(); buildTimeline(); renderBand(); buildRoll(); buildCapture(); buildAgents();
2753
+ /* reset the replay over the new window (boot's pattern): rest at "now"
2754
+ first, the idle re-engage then walks the new range once from the start */
2755
+ T=WINDOW; lastT=WINDOW; played=false; autoEvIdx=0; pendingApply=null; autoPlay=false;
2756
+ setPaused(false);
2757
+ onTChanged(true);
2758
+ }
2759
+ renderRangePills();
2760
+
2585
2761
  /* the timeline: dots per night (green when busy adding, grey when the
2586
2762
  night merged ≥3 — the prototype's thresholds), date labels at the ends */
2587
2763
  function buildTimeline(){
@@ -2594,6 +2770,7 @@ function buildTimeline(){
2594
2770
  dots.style.cssText='position:absolute;inset:0;pointer-events:none;';
2595
2771
  if(REPLAY_OK){
2596
2772
  label.textContent=NIGHTS.length+' nights';
2773
+ el('replay').classList.remove('off'); /* a range switch can revive the instrument (#452) */
2597
2774
  const s1=document.createElement('span'); s1.style.left='0%'; s1.style.transform='none';
2598
2775
  s1.textContent=fmtDay(NIGHTS[0].date); dates.appendChild(s1);
2599
2776
  const s2=document.createElement('span'); s2.style.left='100%'; s2.style.transform='translateX(-100%)';
@@ -2603,7 +2780,10 @@ function buildTimeline(){
2603
2780
  d.style.left=((i+.5)/WINDOW*100)+'%';
2604
2781
  if(mergedL[i]>=3) d.classList.add('w');
2605
2782
  else if(learnedL[i]>=8) d.classList.add('g');
2606
- d.title=fmtDay(n.date)+' — learned '+(learnedL[i]||0)+(enrichedL[i]?' · enriched '+enrichedL[i]:'')+(mergedL[i]?' · merged '+mergedL[i]:'');
2783
+ d.title=fmtDay(n.date)+' — learned '+(learnedL[i]||0)+(enrichedL[i]?' · enriched '+enrichedL[i]:'')+(mergedL[i]?' · merged '+mergedL[i]:'')
2784
+ +(n.superseded.length?' · superseded '+n.superseded.length:'')
2785
+ +(n.retracted.length?' · retracted '+n.retracted.length:'')
2786
+ +(n.rewritten.length?' · rewritten '+n.rewritten.length:'');
2607
2787
  frag.appendChild(d);
2608
2788
  });
2609
2789
  }else{
@@ -3231,7 +3411,14 @@ function buildRoll(){
3231
3411
 
3232
3412
  /* ---------- capture health — phase 2: per machine × harness rows from the
3233
3413
  /dashboard/data capture_health accounting; the counts-only rows remain
3234
- the degrade for old servers / nothing recorded yet ---------- */
3414
+ the degrade for old servers / nothing recorded yet.
3415
+ Fix round 7 (owner ruling 2026-09-14 — "the normal 30 day as the other
3416
+ cards"): the card's face is the ROLLING window (capture_health
3417
+ window_rows, summed over the last window_days calendar days); the newest
3418
+ night rides as a per-row secondary suffix ("tonight: N sess" / the
3419
+ night's date when it is not today). Degradation ladder: pre-round-7
3420
+ server (no window_rows) → the newest-night view below; no activity rows
3421
+ at all → the counts-only rows. ---------- */
3235
3422
  /* ---------- machine × harness grouping (#424 review fixes 4+5 + the owner's
3236
3423
  model-B direction): source_agent is "harness/profile" and each context may
3237
3424
  carry a source_machine stamp (migration v15+). The rows a stats card and
@@ -3343,6 +3530,72 @@ function buildCapture(){
3343
3530
  host.innerHTML='';
3344
3531
  const sub=el('cap-sub');
3345
3532
  const ch=D&&D.capture_health?D.capture_health:null;
3533
+ /* fix round 7: the rolling-window view — the card shows the normal 30 day
3534
+ like the band's other cards. window_rows is ABSENT on a pre-round-7
3535
+ server (and empty when nothing is recorded) → fall through to the
3536
+ newest-night view / counts degrade below. */
3537
+ if(ch&&Array.isArray(ch.window_rows)&&ch.window_rows.length){
3538
+ const wd=ch.window_days||30;
3539
+ if(sub) sub.textContent='last '+wd+' days';
3540
+ /* group the window's machine × agent rows into machine × harness
3541
+ bundles — same grouping as every other card surface */
3542
+ const groups=new Map();
3543
+ for(const r of ch.window_rows){
3544
+ const agent=String(r.agent||'unknown');
3545
+ const harness=agent.split('/')[0]||agent;
3546
+ const key=(r.machine||'')+'|'+harness;
3547
+ let g=groups.get(key);
3548
+ if(!g){ g={machine:r.machine||null, harness, posts:0, sessions:0, bytes:0, held:0, retried:0, agents:[]}; groups.set(key,g); }
3549
+ g.posts+=r.posts||0; g.sessions+=r.sessions||0; g.bytes+=r.bytes||0;
3550
+ g.held+=r.held||0; g.retried+=r.retried||0;
3551
+ g.agents.push(agent+' — '+r.posts+' post'+(r.posts===1?'':'s')+' · '+r.sessions+' sess · '+mbFmt(r.bytes||0)
3552
+ +(r.held?' · '+r.held+' held':'')+(r.retried?' · '+r.retried+' retried':''));
3553
+ }
3554
+ /* newest-night secondary: per-bundle sessions + held off capture_health
3555
+ rows (the newest day with rows). held keys the badge — a held post is
3556
+ actionable NOW (the client retries it next run); the window's own
3557
+ held/retried live in the row tooltip. */
3558
+ const night=new Map();
3559
+ if(ch.day&&Array.isArray(ch.rows)) for(const r of ch.rows){
3560
+ const agent=String(r.agent||'unknown');
3561
+ const harness=agent.split('/')[0]||agent;
3562
+ const key=(r.machine||'')+'|'+harness;
3563
+ const prev=night.get(key)||{sessions:0,held:0};
3564
+ prev.sessions+=r.sessions||0; prev.held+=r.held||0;
3565
+ night.set(key,prev);
3566
+ }
3567
+ const today=new Date().toISOString().slice(0,10);
3568
+ const nightLabel=ch.day===today?'tonight':fmtDay(ch.day);
3569
+ const gs2=[...groups.values()].sort((a,b)=>b.bytes-a.bytes);
3570
+ const maxB=gs2[0].bytes;
3571
+ const pausedKeys=fleetPausesByKey();
3572
+ for(const g of gs2){
3573
+ const n=night.get((g.machine||'')+'|'+g.harness);
3574
+ const row=document.createElement('div');
3575
+ row.className='cap-row2';
3576
+ const label=g.machine?g.machine+' · '+g.harness:g.harness;
3577
+ const paused=pausedKeys.has((g.machine||'')+'|'+g.harness);
3578
+ row.title=label+' — last '+wd+' days: '+fmt(g.sessions)+' sessions · '+mbFmt(g.bytes)+' posted'
3579
+ +(g.held>0?' · '+g.held+' held in window':'')
3580
+ +(g.retried>0?' · '+g.retried+' retried in window':'')
3581
+ +(n?' · '+nightLabel+': '+n.sessions+' sess'+(n.held>0?' ('+n.held+' held)':''):'')
3582
+ +(paused?' · CAPTURE PAUSED (posts are skipped — sessions are not backfilled)':'')
3583
+ +':\n'+g.agents.join('\n');
3584
+ row.innerHTML='<span class="cap-dot"></span>'+
3585
+ '<span class="hn" title="'+esc(label)+'">'+esc(label)+'</span>'+
3586
+ '<span class="cap-bar"><span class="fill" style="width:'+(g.bytes/Math.max(maxB,1)*100)+'%"></span></span>'+
3587
+ '<span class="st"><b>'+fmt(g.sessions)+'</b> sess · '+mbFmt(g.bytes)
3588
+ +(paused?'<span class="cap-paused" title="capture paused — posts are skipped (200) and sessions are not backfilled">PAUSED</span>':'')
3589
+ +(n&&n.held>0?'<span class="cap-held" title="'+nightLabel+' — failed posts the client will retry — cursor held">&#9888; '+n.held+' held</span>':'')
3590
+ +'</span>'
3591
+ /* the newest-night sub-line — a full-basis flex child, so it wraps
3592
+ UNDER the primary line (the suffix never fits beside it; see the
3593
+ .cap-ton rule) and only exists for bundles with newest-night rows */
3594
+ +(n?'<span class="cap-ton" title="newest capture night ('+nightLabel+') — '+n.sessions+' session'+(n.sessions===1?'':'s')+' posted that night">'+nightLabel+': '+n.sessions+' sess</span>':'');
3595
+ host.appendChild(row);
3596
+ }
3597
+ return;
3598
+ }
3346
3599
  if(ch&&Array.isArray(ch.rows)&&ch.rows.length){
3347
3600
  /* phase-2 live view: the /distill accounting rows, grouped machine ×
3348
3601
  harness — width encodes capture volume (each bundle's byte share of
@@ -3421,7 +3674,9 @@ function buildCapture(){
3421
3674
  function lastNightByAgent(){
3422
3675
  for(let i=NIGHTS.length-1;i>=0;i--){
3423
3676
  const n=NIGHTS[i];
3424
- if(n.added.length+n.enriched.length+n.merged.length+n.linked.length>0) return {date:n.date,by:n.by_agent};
3677
+ /* #452: the truth-management events count too — a superseded-only night
3678
+ is a real night */
3679
+ if(n.added.length+n.enriched.length+n.merged.length+n.linked.length+n.superseded.length+n.retracted.length+n.rewritten.length>0) return {date:n.date,by:n.by_agent};
3425
3680
  }
3426
3681
  return null;
3427
3682
  }
@@ -3659,7 +3914,7 @@ function buildAgents(){
3659
3914
  * counts — the paused state and dots re-derive from the fresh payload. */
3660
3915
  async function refreshFleet(){
3661
3916
  try{
3662
- const dd=await api('/dashboard/data?range=90d');
3917
+ const dd=await api('/dashboard/data?range='+RANGE+'d');
3663
3918
  D=dd;
3664
3919
  buildAgents();
3665
3920
  buildCapture();
@@ -4040,7 +4295,7 @@ let refreshInFlight=false;
4040
4295
  async function refreshAfterWrite(){
4041
4296
  if(refreshInFlight) return; refreshInFlight=true;
4042
4297
  try{
4043
- const [ff,dd]=await Promise.all([api('/dashboard/field'),api('/dashboard/data?range=90d').catch(()=>null)]);
4298
+ const [ff,dd]=await Promise.all([api('/dashboard/field'),api('/dashboard/data?range='+RANGE+'d').catch(()=>null)]);
4044
4299
  if(ff){ F=ff; buildWorld(); }
4045
4300
  if(dd){ D=dd; }
4046
4301
  renderBand(); buildRoll(); buildCapture(); renderLegend(); renderFeel();
@@ -4351,9 +4606,9 @@ async function pollHealth(){
4351
4606
  ===================================================================== */
4352
4607
  async function boot(){
4353
4608
  const [dd,ff,ee,hh,hd,mm]=await Promise.allSettled([
4354
- api('/dashboard/data?range=90d'),
4609
+ api('/dashboard/data?range='+RANGE+'d'),
4355
4610
  api('/dashboard/field'),
4356
- api('/dashboard/events?days=90'),
4611
+ api('/dashboard/events?days='+RANGE),
4357
4612
  fetch('/health'),
4358
4613
  api('/health/detail'),
4359
4614
  api('/dashboard/model'),
@@ -28,6 +28,36 @@
28
28
  * ranking. Long-term remembering is the product; time preference stays mild.
29
29
  */
30
30
  export declare const DECAY_HALF_LIFE_DAYS = 365;
31
+ /**
32
+ * Per-use strength promotion rate (#448, the strength model's upward path):
33
+ * one application of `promotion = PROMOTION_RATE × (1 − S / IMPORTANCE_CEILING)
34
+ * × S^(−0.3)` per new use (S = stored base_strength), iterated once per
35
+ * access-count delta by the nightly stagePromotion stage and clamped at the
36
+ * ceiling. Calibrated so the first access on a median-0.35 memory gives ~+0.03
37
+ * (0.35 → 0.3786). Shape grounded in LTP saturation curves (Cao & Harris
38
+ * 2014), the power law of practice (α ≈ 0.3, Newell & Rosenbloom 1981), and
39
+ * spaced-repetition stability multipliers (S^−0.53, SuperMemo SM-17,
40
+ * n > 60,000 real repetitions): bigger boosts for weaker memories,
41
+ * saturating near the ceiling, never fully stopping (asymptotic). The decay
42
+ * side is untouched — a memory's fade RATE no longer depends on its use or
43
+ * link history (the #448 hardening removal); use raises the STORED score
44
+ * instead, and abandonment fades it on the same 365-day clock as everything
45
+ * else. RELEASE-MANAGED per this module's evolution contract — no config key.
46
+ */
47
+ export declare const PROMOTION_RATE = 0.033;
48
+ /**
49
+ * Lower bound on the promotion formula's S input (#453 review Minor finding,
50
+ * owner decision 2026-09-16). base_strength 0.0 is a writable value (legacy
51
+ * rows, operator edits), and S^(−0.3) has a zero-singularity: unfloored, one
52
+ * access on a 0.0 row computed delta = 0.033 × (1 − 0/0.95) × 0^−0.3 =
53
+ * Infinity, and the ceiling clamp turned that into a leap straight to 0.95.
54
+ * Floored at 0.01, the same access lands at 0.033 × (1 − 0.01/0.95) ×
55
+ * 0.01^(−0.3) ≈ +0.13 (0.0 → ≈0.14) — the biggest first boost the formula
56
+ * can give, bounded and far from the ceiling. The floor clamps the FORMULA
57
+ * INPUT only: storage keeps a 0.0 row at 0.0 until it is used, and the
58
+ * promoted result is nonzero from then on. RELEASE-MANAGED like PROMOTION_RATE.
59
+ */
60
+ export declare const PROMOTION_STRENGTH_FLOOR = 0.01;
31
61
  /** Default k for retrieve() (/search without an explicit limit). #192. */
32
62
  export declare const SEARCH_LIMIT = 8;
33
63
  /** Default k for searchRecent() (/recent without an explicit limit). #192. */
@@ -85,23 +115,58 @@ export declare const SCORE_SIMILARITY_WEIGHT = 0.5;
85
115
  export declare const SCORE_STRENGTH_WEIGHT = 0.2;
86
116
  /** Graph-centrality share of the composite score (was 0.20; see above). */
87
117
  export declare const SCORE_CONNECTIONS_WEIGHT = 0.15;
88
- /** Slow recency curve share of the composite score (was 0.10; see above). */
118
+ /**
119
+ * Log-saturation degree K for the connections term (#449 PR E, items 1+3):
120
+ * the credit is min(1, log1p(k) / log1p(K)) × SCORE_CONNECTIONS_WEIGHT on
121
+ * the ABSOLUTE undirected-degree scale k — full at k = K, exactly +0 at
122
+ * k = 0 (unlinked rows score bit-identically to the linear term they
123
+ * replace). Provenance: p99 of the REAL undirected degree distribution
124
+ * (read-only audit 2026-09-17 of the canonical wave snapshot, 18,798
125
+ * memories / 14,600 linked: p50 = 4, p90 = 8, p95 = 9, p99 = 15, p99.9 =
126
+ * 28.4, max = 40; 135 memories ≥ 16 links = 0.92% of linked) — so the top
127
+ * ~1% of hubs tie at full credit and everything below differentiates
128
+ * modestly. Shape grounded in the #449 research base: ACT-R fan saturation
129
+ * (Anderson & Reder 1999 — activation falls with the LOG of fan), SAM's
130
+ * saturating returns, cue overload (Watkins & Watkins 1975). Replaces the
131
+ * candidate-set-relative normalization #449 deleted (k / the per-query max
132
+ * degree) — a row's score no longer depends on which other rows happened
133
+ * to match. Alternatives K = 8 (p90 — collapses all p90+ rows to
134
+ * full credit) and K = 32 (≈p99.9 — leaves the p99 hub at 0.12 of the
135
+ * term) were tabled in the #449 spec. RELEASE-MANAGED per this module's
136
+ * evolution contract — overridable only through the configureScoring seam.
137
+ */
138
+ export declare const CONNECTIONS_SATURATION_DEGREE = 16;
139
+ /** Time-curve share of the composite score — the merged curve's blend weight
140
+ * AND slow-region amplitude (was 0.10; see above). The four blend weights
141
+ * still sum to 1.0; the head amplitude below is a documented overshoot, NOT
142
+ * a fifth blend weight (#430). Zero (via the seam) disables the whole term. */
89
143
  export declare const SCORE_RECENCY_WEIGHT = 0.15;
90
- /** Fresh-memory window: the additive bonus fades linearly to 0 over this
91
- * many days. 7 — nightly capture means 1 day is the floor of "fresh"
92
- * (#191 Phase B). */
93
- export declare const FRESHNESS_BOOST_DAYS = 7;
94
- /** Fresh-memory bonus size at age 0 (#191 Phase B; 0 = disabled via seam). */
95
- export declare const FRESHNESS_BOOST_WEIGHT = 0.15;
144
+ /** Hourly decay of the merged curve's slow region (#430) — promoted from the
145
+ * inline literal computeScore carried; half-life ≈ 57.7 days. At and beyond
146
+ * RECENCY_HEAD_DAYS the curve is bit-identical to the pre-#430 slow term
147
+ * (SCORE_RECENCY_WEIGHT × RECENCY_HOURLY_DECAY^hours). */
148
+ export declare const RECENCY_HOURLY_DECAY = 0.9995;
149
+ /** Merged time-curve head amplitude at age 0 (#430) — the pre-#430 slow
150
+ * weight 0.15 + fresh bonus 0.15: the freshness job is now the early steep
151
+ * part of ONE curve. The head joins value-continuously onto the slow region
152
+ * at RECENCY_HEAD_DAYS; its rate is DERIVED from that continuity, never a
153
+ * free constant. */
154
+ export declare const RECENCY_HEAD_WEIGHT = 0.3;
155
+ /** Join age in days of the merged time curve (#430) — the head window edge,
156
+ * carried over from the pre-#430 freshness window. Nightly capture means
157
+ * 1 day is the floor of "fresh" (#191 Phase B). */
158
+ export declare const RECENCY_HEAD_DAYS = 7;
96
159
  /** Score multiplier for a memory a later decision superseded (0.15.2; the
97
160
  * belief walk (#393 D) is the primary mechanism — this is the safety net
98
161
  * for rows the walk does not reach). */
99
162
  export declare const SUPERSEDED_DEMOTION = 0.5;
100
- /** #203 soft boost on exact project match. ADDITIVE, zero-boost neutral,
101
- * never a penalty — a foreign memory ranks equal, not lower. */
102
- export declare const PROJECT_AFFINITY_WEIGHT = 0.15;
103
- /** #203 soft boost multiplier on max overlapping domain-tag weight. */
104
- export declare const DOMAIN_AFFINITY_WEIGHT = 0.15;
163
+ /** #430 merged scope affinity — ONE term replacing #203's two boosts (the
164
+ * project-affinity and domain-affinity constants) at their shared value.
165
+ * Boost = max(project-match indicator (1 on exact match), max overlapping
166
+ * domain-tag weight) × this weight: the strongest single scope signal counts
167
+ * once, never stacked. ADDITIVE, zero-boost neutral, never a penalty — an
168
+ * absent scope adds exactly 0; a foreign memory ranks equal, not lower. */
169
+ export declare const SCOPE_AFFINITY_WEIGHT = 0.15;
105
170
  /** #205 RRF k parameter (1/(k+rank+1)) — matches the pre-#205 hardcoded 60
106
171
  * so the no-config path was byte-identical to 0.15.3. */
107
172
  export declare const RRF_K = 60;