@gamaze/hicortex 0.22.1 → 0.22.2

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.
@@ -427,6 +427,24 @@
427
427
  .rec-band{ position:absolute; inset:0; border-radius:4px; }
428
428
  .rec-dot{ position:absolute; top:50%; transform:translate(-50%,-50%); width:11px; height:11px; border-radius:50%; background:var(--text); border:2px solid var(--panel); box-shadow:0 0 0 1px color-mix(in srgb, var(--text) 40%, transparent), 0 0 8px color-mix(in srgb, var(--text) 30%, transparent); }
429
429
  .rec-foot{ display:flex; align-items:center; gap:7px; }
430
+ /* Memory Precision Level 1 (#476) — the pushed-index row: a compact
431
+ second line in the .csub scale (the sibling-rhythm ruling 2026-09-14:
432
+ the card keeps the shared .bseg gap rhythm, header → big → band →
433
+ index row → foot). Direction-only coloring: green at/above the echoed
434
+ recall floor, muted below (the index bar reads conservative); the
435
+ floor tick is amber like the pace marker. Theme tokens only — both
436
+ themes render from their own table, no JS colors here. */
437
+ .rec-l1{ display:block; font-size:10.5px; color:var(--dim); min-width:0; line-height:1.6; }
438
+ .rec-l1 .l1row{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
439
+ .rec-l1 .lbl{ display:none; } /* #476 rename: the three-bucket text is self-labeled */
440
+ .rec-l1 .l1bar{ position:relative; flex:1 1 46px; height:5px; border-radius:3px; background:var(--tbar-bg); min-width:36px; }
441
+ .rec-l1 .l1bar .l1-fill{ position:absolute; left:0; top:0; bottom:0; border-radius:3px; background:color-mix(in srgb, var(--green) 65%, transparent); }
442
+ .rec-l1.below .l1bar .l1-fill{ background:color-mix(in srgb, var(--dim) 55%, transparent); }
443
+ .rec-l1 .l1bar .l1-floor{ position:absolute; top:-2px; bottom:-2px; width:1px; background:var(--amber); }
444
+ .rec-l1 .l1txt{ color:var(--dim); overflow:hidden; text-overflow:ellipsis; font-size:10.5px; letter-spacing:-.01em; }
445
+ .rec-l1 b{ color:var(--green-mid); font-weight:600; }
446
+ .rec-l1.below b{ color:var(--dim); }
447
+ #rec-div{ color:var(--amber); overflow:hidden; text-overflow:ellipsis; }
430
448
  .b-health{ display:flex; align-items:center; gap:7px; font-size:11px; color:var(--dim); white-space:nowrap; }
431
449
  .dot{ width:6px; height:6px; border-radius:50%; background:var(--green); box-shadow:0 0 10px color-mix(in srgb, var(--green) 70%, transparent); animation:breathe 3.2s ease-in-out infinite; flex:none; }
432
450
  .dot.blue{ background:var(--blue); box-shadow:0 0 10px color-mix(in srgb, var(--blue) 70%, transparent); }
@@ -1159,21 +1177,32 @@
1159
1177
  <div class="csub" id="tok-sub">&nbsp;</div>
1160
1178
  <div id="tok-nights" aria-label="LLM tokens per nightly run this billing period"></div>
1161
1179
  </div>
1162
- <!-- Recall level — the designed U-band (#426 final ruling 2026-09-13).
1163
- Title/subtitle hierarchy is the mock of record; the raw ratio is
1164
- tooltip-only (relative value, not user-facing); zone edges arrive
1165
- from the /dashboard/field thresholds echo (never hardcoded here).
1166
- #409 fix round 3 (owner, verbatim intent): the zone word is the
1167
- card's BIG element (the .big .n slot, colored by sampling the band
1168
- gradient at the marker); the band sits in the siblings' chart slot
1169
- (header → big → band → foot — same child order as the limitbar
1170
- cards); "provisional" is hover-only (band tooltip); the marker
1171
- keeps the raw-value tooltip; the trend glyph stays (mock family). -->
1180
+ <!-- Memory Precision (#476, owner retitle) — ONE card, two context
1181
+ levels. Level 2 (recall depth) keeps the #426 U-band + zone word +
1182
+ provisional-hover semantics; the marker/tooltip read the WINDOW
1183
+ ratio off D.memory_precision.level2 (a pre-#476 server has no block
1184
+ → the headline ratio — today's rendering exactly). Level 1 (pushed
1185
+ index) is a compact second row: mean similarity of pushed lines to
1186
+ their prompt, with a floor marker at the echoed
1187
+ recall_min_similarity and muted below it (the index's bar reads
1188
+ conservative — its noise costs compound per prompt) + the
1189
+ redundancy share; direction-only coloring, NO verdict words (the
1190
+ #426 semantics ruling extended: similarity is
1191
+ conversation-dependent). Divergence count on the FACE, the ≤3
1192
+ titles hover-only (no memory lists on the main page — owner ruling
1193
+ 2026-09-12; the header search panel is the list surface). The
1194
+ window rides the #452 selector via the echoed window_days — never
1195
+ hardcoded, never a second range control. -->
1172
1196
  <div class="bseg" id="lim-rec">
1173
- <div class="gh"><span class="t">Recall level</span><span class="sub">uses per showing</span></div>
1197
+ <div class="gh"><span class="t">Memory Precision</span><span class="sub" id="rec-sub">uses per showing</span></div>
1174
1198
  <div class="big"><b class="n rec-word" id="rec-zone">—</b></div>
1175
1199
  <div class="rec-bandrow" id="rec-bandrow"><div class="rec-band" id="rec-band"></div><i class="rec-dot" id="rec-dot" hidden></i></div>
1176
- <div class="csub rec-foot"><span class="trend flat" id="rec-trend" title="last 7 nights vs the prior 7">&#9644;</span></div>
1200
+ <div class="rec-l1" id="rec-l1" hidden style="display:block">
1201
+ <div class="l1row" id="l1-rel">&nbsp;</div>
1202
+ <div class="l1row" id="l1-rep">&nbsp;</div>
1203
+ <div class="l1row" id="l1-off">&nbsp;</div>
1204
+ </div>
1205
+ <div class="csub rec-foot"><span id="rec-div" hidden></span></div>
1177
1206
  </div>
1178
1207
  </div>
1179
1208
  <!-- row 2 · ops & maturity — capture health · 30 nights · maturity split · model -->
@@ -1488,8 +1517,11 @@ const DEPTH_BANDS = { truth:[.9,1.05], belief:[1.05,1.3], forming:[1.3,1.55], fa
1488
1517
  post-backfill store still shows a visible gradient and the inflated
1489
1518
  pre-#425 store shows no monsters. Robust by construction: the mapping is
1490
1519
  absolute (no min/max over the live distribution), so it cannot explode on
1491
- a skewed store. Pure functions — the ratio is behaviorally tested. */
1492
- function bubbleRadius(s){ return 6 + Math.sqrt(clamp(s??0,0,1))*3; }
1520
+ a skewed store. Pure functions — the ratio is behaviorally tested.
1521
+ #474 graph final (owner 2026-09-19): the balls at 30% — 1.8→2.7, exactly
1522
+ 0.30× of the 6→9 law; the depth divisor and the ruled ratio band are
1523
+ scale-invariant and unchanged. */
1524
+ function bubbleRadius(s){ return 1.8 + Math.sqrt(clamp(s??0,0,1))*0.9; }
1493
1525
  function depthSizeDivisor(dz){ return Math.pow(Math.max(.14,dz),0.35); }
1494
1526
 
1495
1527
  /* ---------- live-data state ---------- */
@@ -1506,7 +1538,7 @@ let TOTAL=0, removedNow=0;
1506
1538
  clock (owner ruling 2026-09-13; only the boot reveal counts up) */
1507
1539
 
1508
1540
  /* =====================================================================
1509
- WORLD — domains, memories (every one), the interactive sample, real
1541
+ WORLD — domains, memories, the dateline window (the visible set), real
1510
1542
  links, the replay ledger. All positions derive from id hashes → stable
1511
1543
  across reloads and across the refreshes after Update/Remove.
1512
1544
  ===================================================================== */
@@ -1515,8 +1547,14 @@ const PORT = [ [.50,.20,.15],[.28,.38,.12],[.74,.40,.12],[.50,.62,.13],[.25,.68,
1515
1547
  let DOMAINS = []; // {name,count,land,port,cx,cy,cr}
1516
1548
  const mems = new Map(); // id -> memory object (live + replay ghosts)
1517
1549
  const links = []; // real link edges, engine-decorated
1518
- const bubbles = []; // the ~120-mem interactive importance sample
1519
1550
  let crowdByStage = {}; // stage -> [mem refs] (EVERY memory paints)
1551
+ const bubbles = []; // the latest-N-by-dateline visible set (#474
1552
+ // return; owned by refreshWindow, rebuilt on T)
1553
+ const FIELD_WINDOW_N = 1000; // #474 graph final (owner 2026-09-19): the field shows the latest N memories by dateline
1554
+ let WIN_ORDER = []; // every mem (live + ghosts), effCreated DESC, id ASC
1555
+ let winSet = new Set(); // ids in the window at the current T
1556
+ let winCount = []; // per-domain window counts (labels + tip)
1557
+ let winT = -1;
1520
1558
  let byAgentCounts = {}; // source_agent -> count
1521
1559
  let CONTEXTS = []; // [{id,name,machine,count}] — #421 machine × harness grouping
1522
1560
  let adj = new Map(); // id -> [neighbor ids] (real links)
@@ -1525,6 +1563,13 @@ let learnedL = [], mergedL = [], enrichedL = [];
1525
1563
  let NIGHTS = []; // the events payload's night list
1526
1564
  let WINDOW = 1, NOW_T = .999, REPLAY_OK = false;
1527
1565
 
1566
+ /* Replay pacing (owner 2026-09-19: 2x faster default). Event-driven cruise:
1567
+ each ledger event lands every REPLAY_TICK_MS; a heralded (birth/merge)
1568
+ event holds the clock REPLAY_HERALD_MS then applies; boundary beats rest
1569
+ REPLAY_REST_MS. Halve all three to run 2x; the constants are the only
1570
+ pacing surface. */
1571
+ const REPLAY_TICK_MS=205, REPLAY_HERALD_MS=108, REPLAY_REST_MS=800;
1572
+
1528
1573
  function domIndex(domain){ const i = DOMAINS.findIndex(d=>d.name===domain); return i<0?0:i; }
1529
1574
  function titleOf(id){ const m=mems.get(id); return m?m.title:'(absorbed)'; }
1530
1575
 
@@ -1589,6 +1634,10 @@ function buildWorld(){
1589
1634
  if(!m.ghost && !keep.has(id)) mems.delete(id);
1590
1635
  }
1591
1636
 
1637
+ // the crowd: EVERY memory, bucketed by stage for one fillStyle per pass
1638
+ crowdByStage = { forming:[], belief:[], truth:[], fading:[] };
1639
+ for(const m of mems.values()) (crowdByStage[m.stage]||crowdByStage.forming).push(m);
1640
+
1592
1641
  // replay metadata from the events ledger: in-window births + merge-outs
1593
1642
  // (absorbed losers get ghost entries so their merge animation has a
1594
1643
  // position and their birth a bloom — they are not part of the live field).
@@ -1657,34 +1706,12 @@ function buildWorld(){
1657
1706
  }
1658
1707
  }
1659
1708
 
1660
- // the crowd: EVERY memory, bucketed by stage for one fillStyle per pass
1661
- crowdByStage = { forming:[], belief:[], truth:[], fading:[] };
1662
- for(const m of mems.values()) (crowdByStage[m.stage]||crowdByStage.forming).push(m);
1663
-
1664
- // the interactive sample (#422 live review, "all dots green"): STRATIFIED
1665
- // by stage — quota proportional to each stage's share of live memories
1666
- // (largest-remainder to the fixed total, minimum 1 each), each quota filled
1667
- // by that stage's OWN strength→access ranking with an id-ASC tie-break —
1668
- // fully deterministic, no Math.random. The old global top-120-by-strength
1669
- // sample was 100% truth BY CONSTRUCTION (truth IS the top strength band),
1670
- // so every touchable dot read green and the 1–2px crowd was visually
1671
- // negligible; on an even store this yields ~30/30/30/30.
1672
- bubbles.length = 0;
1673
- {
1674
- const BUBBLE_N=120;
1675
- const stageArrs=STAGE_KEYS.map(k=>crowdByStage[k].filter(m=>!m.ghost)
1676
- .sort((a,b)=> b.strength-a.strength || b.access-a.access || (a.id<b.id?-1:a.id>b.id?1:0)));
1677
- const liveCount=stageArrs.reduce((n,a)=>n+a.length,0);
1678
- if(liveCount>0){
1679
- const exact=stageArrs.map(a=>a.length/liveCount*BUBBLE_N);
1680
- const quota=exact.map(Math.floor);
1681
- for(let i=0;i<quota.length;i++) if(quota[i]===0&&stageArrs[i].length>0) quota[i]=1;
1682
- let left=BUBBLE_N-quota.reduce((n,q)=>n+q,0);
1683
- const order=exact.map((_,i)=>i).sort((a,b)=>(exact[b]%1)-(exact[a]%1)); // remainder DESC, stage order tie
1684
- for(let oi=0; left>0; oi++, left--) quota[order[oi%order.length]]++;
1685
- stageArrs.forEach((a,i)=>{ bubbles.push(...a.slice(0,Math.min(quota[i],a.length))); });
1686
- }
1687
- }
1709
+ // #474 return: the visible set is the dateline WINDOW, not a sample —
1710
+ // WIN_ORDER walks every mem (live + ghosts) newest-first (effCreated DESC,
1711
+ // id ASC tie-break; ghosts sort via their birth night's date), and
1712
+ // refreshWindow (draw) slices the latest FIELD_WINDOW_N alive-at-T rows
1713
+ // out of it each time T moves.
1714
+ WIN_ORDER = Array.from(mems.values()).sort(winCmp);
1688
1715
 
1689
1716
  // contexts (source_agent values) with their machine stamps (#421 machine ×
1690
1717
  // harness): the rail + capture card group on (machine ?? legacy) × harness.
@@ -1746,6 +1773,31 @@ function aliveAt(b,t){
1746
1773
  return true;
1747
1774
  }
1748
1775
  function dateAt(t){ const i=clamp(Math.floor(t),0,NIGHTS.length-1); return NIGHTS[i]?NIGHTS[i].date:''; }
1776
+ /* #474 return: the visible set is a DATELINE WINDOW — at replay clock T the
1777
+ field draws the latest FIELD_WINDOW_N memories by effective created date
1778
+ (created_at; ghosts via their birth night), not a sample and not the whole
1779
+ store. Pure helpers (behaviorally tested). */
1780
+ function effCreated(m){ return m.created || (m.born>=0 ? dateAt(m.born) : ''); }
1781
+ function windowPick(order, t, n, dl, isNow){
1782
+ const out=[];
1783
+ for(const m of order){
1784
+ if(out.length>=n) break;
1785
+ if(!aliveAt(m,t)) continue;
1786
+ if(!isNow && m.born<0 && effCreated(m)>dl) continue; // untracked + newer than the dateline
1787
+ out.push(m);
1788
+ }
1789
+ return out;
1790
+ }
1791
+ function winCmp(a,b){ return effCreated(b)<effCreated(a)?-1: effCreated(b)>effCreated(a)?1: a.id<b.id?-1:1; }
1792
+ function refreshWindow(t){
1793
+ if(t===winT) return;
1794
+ winT=t;
1795
+ const picked = windowPick(WIN_ORDER, t, FIELD_WINDOW_N, dateAt(t), t>=NOW_T);
1796
+ winSet = new Set(picked.map(m=>m.id));
1797
+ winCount = new Array(DOMAINS.length).fill(0);
1798
+ for(const m of picked) winCount[m.dom]++;
1799
+ bubbles.length=0; bubbles.push(...picked);
1800
+ }
1749
1801
 
1750
1802
  /* =====================================================================
1751
1803
  THEME — one engine, two palettes. Dark (the brand night: glow) is the
@@ -1903,22 +1955,16 @@ function sprite(stage, r){
1903
1955
  const st = PAL.stages[stage] || PAL.stages.forming;
1904
1956
  const rgb = st.rgb;
1905
1957
  if(PAL.name==='dark'){
1958
+ /* #474 final polish (owner 2026-09-20): plain dot — no glow halo, no
1959
+ white core gradient. A filled circle with a slightly brighter edge;
1960
+ the maturity color carries the reading, the size stays small. */
1906
1961
  const S = Math.ceil(rad*PAL.sprK), c2 = document.createElement('canvas');
1907
1962
  c2.width=S; c2.height=S;
1908
1963
  const g = c2.getContext('2d'), cxp=S/2;
1909
- let gr = g.createRadialGradient(cxp,cxp,rad*.2, cxp,cxp,rad*2.3);
1910
- gr.addColorStop(0, rgba(rgb,.5*st.glow));
1911
- gr.addColorStop(.45, rgba(rgb,.16*st.glow));
1912
- gr.addColorStop(1, rgba(rgb,0));
1913
- g.fillStyle=gr; g.fillRect(0,0,S,S);
1914
- gr = g.createRadialGradient(cxp-rad*.28,cxp-rad*.32,rad*.1, cxp,cxp,rad);
1915
- gr.addColorStop(0,'rgba(255,255,255,'+(.5*st.alpha)+')');
1916
- gr.addColorStop(.35, rgba(rgb,.55*st.alpha));
1917
- gr.addColorStop(.82, rgba(rgb,.22*st.alpha));
1918
- gr.addColorStop(1, rgba(rgb,.04));
1919
- g.fillStyle=gr; g.beginPath(); g.arc(cxp,cxp,rad,0,7); g.fill();
1920
- g.strokeStyle=rgba(rgb,.34*st.alpha); g.lineWidth=1;
1921
- g.beginPath(); g.arc(cxp,cxp,rad-.5,0,7); g.stroke();
1964
+ g.fillStyle=rgba(rgb,.88*st.alpha);
1965
+ g.beginPath(); g.arc(cxp,cxp,rad*.82,0,7); g.fill();
1966
+ g.strokeStyle=rgba(rgb,.45*st.alpha); g.lineWidth=1;
1967
+ g.beginPath(); g.arc(cxp,cxp,rad*.82,0,7); g.stroke();
1922
1968
  /* #422 round 2: fading's DISTINCT outer ring — the rim stage's fill runs
1923
1969
  at low alpha by design; without its own edge treatment the dots were
1924
1970
  illegible against the night ground */
@@ -2001,7 +2047,7 @@ function buildPaths(){
2001
2047
  }
2002
2048
  }
2003
2049
  function pathAlive(p,t){
2004
- for(const id of p.nodes){ const b=mems.get(id); if(!b||!aliveAt(b,t)) return false; }
2050
+ for(const id of p.nodes){ const b=mems.get(id); if(!b||!aliveAt(b,t)||!winSet.has(id)) return false; }
2005
2051
  return t>=p.born;
2006
2052
  }
2007
2053
  function spawnSignal(showcase){
@@ -2060,8 +2106,10 @@ function advanceSignals(now, dt){
2060
2106
  }
2061
2107
 
2062
2108
  /* =====================================================================
2063
- THE FRAME — theme-aware; crowd first (micro-stars), then synapses,
2064
- signals, bubbles (painter-sorted far→near), waymarkers, furniture.
2109
+ THE FRAME — theme-aware; synapses, signals, bubbles (painter-sorted
2110
+ far→near), waymarkers, furniture. #474 return: the ONLY dots drawn are
2111
+ the dateline window (latest FIELD_WINDOW_N by effective created date at
2112
+ the replay clock — see refreshWindow); no crowd pass, no sample.
2065
2113
  ===================================================================== */
2066
2114
  let labelRects=[];
2067
2115
  /* emergence queue (#422 live review): a night's births/links become VISIBLE
@@ -2105,6 +2153,7 @@ function processEmergence(t){
2105
2153
  }
2106
2154
  function draw(now, dt){
2107
2155
  const t=T;
2156
+ refreshWindow(t); // #474: the visible set = latest-N by dateline at this T (bubbles, winSet, winCount)
2108
2157
  const dark = PAL.name==='dark';
2109
2158
  const ADD = dark ? 'lighter' : 'source-over';
2110
2159
  ctx.clearRect(0,0,view.w,view.h);
@@ -2170,6 +2219,12 @@ function draw(now, dt){
2170
2219
  }
2171
2220
  ctx.globalAlpha=1;
2172
2221
 
2222
+ /* emergence flags resolve FIRST (once per frame) — the visible set and
2223
+ the link pass below read them; dots still queued (_emerged===false)
2224
+ stay invisible until their frame budget comes up. (#474: the old
2225
+ every-memory crowd pass is gone — the window IS the field.) */
2226
+ processEmergence(t);
2227
+
2173
2228
  /* THE CROWD — every memory in the store as maturity-colored micro-stars
2174
2229
  behind the touchable sample (no sampling: the dot count IS the store).
2175
2230
  One fillStyle per stage pass. The emergence queue is resolved FIRST
@@ -2187,7 +2242,7 @@ function draw(now, dt){
2187
2242
  if(fog<.15) continue;
2188
2243
  const [sx,sy]=proj3(m.nx||.5,m.ny||.5,dz);
2189
2244
  if(sx<-2||sy<-2||sx>view.w+2||sy>view.h+2) continue;
2190
- ctx.globalAlpha=fog*.5;
2245
+ ctx.globalAlpha=fog*.5*(focusDom!=null&&m.dom!==focusDom?.15:1);
2191
2246
  const px=Math.max(1,Math.round((rand01(m.id,9)<.85?1:1.8)/dz*1.4));
2192
2247
  ctx.fillRect(sx,sy,px,px);
2193
2248
  }
@@ -2241,6 +2296,7 @@ function draw(now, dt){
2241
2296
  if(L.rk>=lodK) continue; // zoom LOD (lines only)
2242
2297
  const a=mems.get(L.a), b=mems.get(L.b);
2243
2298
  if(!a||!b||!aliveAt(a,t)||!aliveAt(b,t)) continue;
2299
+ /* #474 final polish: the both-endpoints-in-window gate killed the mesh — links draw between ANY placed memories (bedrock parity: the full LOD-tamed mesh), endpoints carry positions regardless of window membership */
2244
2300
  if(a._emerged===false||b._emerged===false) continue; // thread waits for both endpoints
2245
2301
  const [ax,ay]=bpos(a,now,t), [bx,by]=bpos(b,now,t);
2246
2302
  const ctrl=segCtrlN(ax,ay,bx,by,(L.use>18?1:-1));
@@ -2255,6 +2311,9 @@ function draw(now, dt){
2255
2311
  const prog = L.forming ? clamp((t-(L._emergeT!==undefined?L._emergeT:L.born))/1.4,0,1) : 1;
2256
2312
  const drawP=Math.min(prog,linkBoot);
2257
2313
  if(drawP<=0) continue;
2314
+ /* #474 final polish: domain focus — a thread stays lit only if an
2315
+ endpoint belongs to the focused domain (owner 2026-09-20) */
2316
+ const focusDim = (focusDom!=null && a.dom!==focusDom && b.dom!==focusDom) ? .15 : 1;
2258
2317
  const len=Math.hypot(x2-x1,y2-y1)*1.15;
2259
2318
  const superFam = L.rel==='superseded_by'||L.rel==='corrected_by';
2260
2319
  /* fix-round-3 weights: settled alphas roughly halved (both themes) so the
@@ -2262,7 +2321,7 @@ function draw(now, dt){
2262
2321
  const bright=(a.stage==='truth'||b.stage==='truth') ? (dark?.07:.11) : (dark?.035:.06);
2263
2322
  const baseW=.34+useF*.55+heat*.5;
2264
2323
  /* the SETTLED stroke is always the base */
2265
- ctx.strokeStyle=rgba(PAL.thread,(bright*(superFam?.45:1)+useF*(dark?.05:.07)+heat*.28)*linkBoot*tfog);
2324
+ ctx.strokeStyle=rgba(PAL.thread,(bright*(superFam?.45:1)+useF*(dark?.05:.07)+heat*.28)*linkBoot*tfog*focusDim);
2266
2325
  ctx.lineWidth=baseW;
2267
2326
  ctx.setLineDash(superFam?[4,4]:[len*drawP,1e5]);
2268
2327
  ctx.beginPath(); ctx.moveTo(x1,y1); ctx.quadraticCurveTo(cx1,cy1,x2,y2); ctx.stroke();
@@ -2271,7 +2330,7 @@ function draw(now, dt){
2271
2330
  in the same proportion as the settled stroke) */
2272
2331
  const settle=1-prog;
2273
2332
  const baseA=(dark? .07 : .09)+((dark?.15:.17)-(dark?.07:.09))*drawP;
2274
- ctx.strokeStyle=rgba(PAL.threadHot,(baseA+.14*heat)*tfog*(superFam?.5:1)*settle);
2333
+ ctx.strokeStyle=rgba(PAL.threadHot,(baseA+.14*heat)*tfog*(superFam?.5:1)*settle*focusDim);
2275
2334
  ctx.lineWidth=baseW+(.18+drawP*.3)*settle;
2276
2335
  ctx.setLineDash(superFam?[4,4]:[len*drawP,1e5]);
2277
2336
  ctx.beginPath(); ctx.moveTo(x1,y1); ctx.quadraticCurveTo(cx1,cy1,x2,y2); ctx.stroke();
@@ -2346,8 +2405,9 @@ function draw(now, dt){
2346
2405
  dark-blue-on-near-black at ~4px read invisible to a vision model);
2347
2406
  the projection factor is known here, and far-stage bubbles (rim depth
2348
2407
  / small viewports) were collapsing to ~1-2px. Every stage stays a
2349
- legible glimmer; ordering untouched. */
2350
- r=Math.max(r,5);
2408
+ legible glimmer; ordering untouched. #474 graph final: floor 1.5 —
2409
+ 0.30× of 5, proportional with the 30% balls. */
2410
+ r=Math.max(r,1.5);
2351
2411
  let remA=1;
2352
2412
  if(b.removing){ const p=clamp((now-b.removing)/700,0,1); remA=1-p; if(p>=1){ b.removed=true; b.removing=undefined; } }
2353
2413
  const ex=b._ex||0;
@@ -2447,8 +2507,11 @@ function draw(now, dt){
2447
2507
  ctx.font=(9.5*k).toFixed(1)+'px '+FF;
2448
2508
  try{ ctx.letterSpacing=(1*k).toFixed(1)+'px'; }catch(e){}
2449
2509
  ctx.fillStyle= dark? 'rgba(90,90,90,'+(.85*dimming*lp)+')' : hexA(PAL.labelCount,dimming*lp);
2450
- ctx.fillText(fmt(d.count), sx+w+9*k, sy);
2451
- const bw=w+9*k+ctx.measureText(fmt(d.count)).width;
2510
+ /* #474: the label count is the WINDOW count (what is drawn at this T),
2511
+ not the store total — honest with the dateline view. */
2512
+ const cnt=fmt(winCount[i]||0);
2513
+ ctx.fillText(cnt, sx+w+9*k, sy);
2514
+ const bw=w+9*k+ctx.measureText(cnt).width;
2452
2515
  ctx.strokeStyle= active? rgba(PAL.stages.truth.rgb,.55*lp) : (dark? 'rgba(120,130,140,'+(.4*dimming*lp)+')' : 'rgba(96,92,78,'+(.5*dimming*lp)+')');
2453
2516
  ctx.lineWidth=1;
2454
2517
  const bx=sx-9*k, by=sy-11*k, bxx=bx+bw+9*k, byy=by+15*k, tk=4*k;
@@ -2613,18 +2676,18 @@ function autoTick(now){
2613
2676
  if(T>=NOW_T-.001){
2614
2677
  /* a beat at "now": re-kick to the range start ONLY before the first
2615
2678
  full playback; after it, REST here — plays once, never loops (#452) */
2616
- if(played){ autoPlay=false; autoNextAt=now+3200; return; }
2617
- autoEvIdx=0; autoNextAt=now+3200; seek(.001); return;
2679
+ if(played){ autoPlay=false; autoNextAt=now+REPLAY_REST_MS; return; }
2680
+ autoEvIdx=0; autoNextAt=now+REPLAY_REST_MS; seek(.001); return;
2618
2681
  }
2619
2682
  while(autoEvIdx<EVENTS.length && EVENTS[autoEvIdx].t<=T+.0001) autoEvIdx++;
2620
2683
  if(autoEvIdx>=EVENTS.length){
2621
2684
  /* ledger exhausted: arrive at today and complete the playback (the
2622
2685
  flags go BEFORE the seek so onTChanged's control swap sees them) */
2623
- autoNextAt=now+3200; played=true; autoPlay=false; seek(WINDOW); return;
2686
+ autoNextAt=now+REPLAY_REST_MS; played=true; autoPlay=false; seek(WINDOW); return;
2624
2687
  }
2625
2688
  const ev=EVENTS[autoEvIdx++];
2626
- if(ev.id && signals.length<8){ heraldTo(ev.id); pendingApply={t:ev.t+.001, at:now+430}; }
2627
- else { seek(ev.t+.001); autoNextAt=now+820; }
2689
+ if(ev.id && signals.length<8){ heraldTo(ev.id); pendingApply={t:ev.t+.001, at:now+REPLAY_HERALD_MS}; }
2690
+ else { seek(ev.t+.001); autoNextAt=now+REPLAY_TICK_MS; }
2628
2691
  }
2629
2692
  track.addEventListener('pointerdown',e=>{ if(!REPLAY_OK) return; scrubbing=true; manualTake(); track.setPointerCapture(e.pointerId); seek(trackFrac(e)*WINDOW); });
2630
2693
  track.addEventListener('pointermove',e=>{ if(scrubbing) seek(trackFrac(e)*WINDOW); });
@@ -2841,7 +2904,8 @@ cv.addEventListener('pointermove',e=>{
2841
2904
  tip.style.display='block';
2842
2905
  tip.style.left=Math.min(e.clientX+14,window.innerWidth-300)+'px';
2843
2906
  tip.style.top=(e.clientY+16)+'px';
2844
- tip.innerHTML='<b>'+esc(d.name)+'</b><span style="color:var(--dim)">'+fmt(d.count)+' memories</span>';
2907
+ /* #474: the window count at this T — honest with what is drawn */
2908
+ tip.innerHTML='<b>'+esc(d.name)+'</b><span style="color:var(--dim)">'+fmt(winCount[lb]||0)+' in view</span>';
2845
2909
  }else tip.style.display='none';
2846
2910
  dirty=true;
2847
2911
  });
@@ -3180,7 +3244,14 @@ function recallGradAt(frac,z,theme){
3180
3244
  }
3181
3245
  let lastRecUses=null; /* setTheme's re-render carries the last value */
3182
3246
  function renderRecBand(ups){
3247
+ /* #476: the marker/tooltip read the WINDOW ratio when the memory_precision
3248
+ block is present; a pre-#476 server (no block) degrades to the headline
3249
+ ratio — today's rendering exactly (the capture card's block-absence
3250
+ guard pattern). A null window value honestly renders "—" below. */
3251
+ const mp=D&&D.memory_precision?D.memory_precision:null;
3252
+ if(mp&&mp.level2) ups=mp.level2.uses_per_showing;
3183
3253
  lastRecUses=ups;
3254
+ renderRecL1(mp);
3184
3255
  const z=F&&F.thresholds?F.thresholds.recall_uses:null;
3185
3256
  const axis=z&&typeof z.axis_max==='number'&&z.axis_max>0?z.axis_max:null;
3186
3257
  const row=el('rec-bandrow'), band=el('rec-band'), dot=el('rec-dot'), word=el('rec-zone');
@@ -3204,7 +3275,7 @@ function renderRecBand(ups){
3204
3275
  if(zk){
3205
3276
  dot.hidden=false;
3206
3277
  dot.style.left=pct(ups)+'%';
3207
- dot.title=ups.toFixed(3)+' uses per showing — last 30 nights';
3278
+ dot.title=ups.toFixed(3)+' uses per showing — last '+(mp&&mp.window_days?mp.window_days:30)+' days';
3208
3279
  word.textContent=RECALL_ZONE_WORD[zk];
3209
3280
  word.className='n rec-word';
3210
3281
  /* the big word's color = the gradient sampled AT the marker — the word
@@ -3232,6 +3303,62 @@ function renderRecBand(ups){
3232
3303
  }
3233
3304
  }
3234
3305
 
3306
+ /* ---------- Memory Precision — Level 1, the pushed index (#476). A compact
3307
+ second row beside Level 2's U-band: mean similarity of pushed lines to
3308
+ their prompt (a 0..1 bar with a floor tick at the /dashboard/field echo of
3309
+ recall_min_similarity — the server's own recall floor, never a page
3310
+ literal), muted below the floor (the index row reads conservative: its
3311
+ noise lands on every prompt), plus the redundancy share (lines restating
3312
+ the session's standing context at/above the echoed cosine). Direction-only
3313
+ coloring, NO verdict words — the #426 semantics ruling extended.
3314
+ Divergence: the COUNT on the card face, the ≤3 titles hover-only (no
3315
+ memory lists on the main page, owner 2026-09-12 — the search panel is the
3316
+ list surface). Absent block (pre-#476 server) → the whole row hides and
3317
+ the sub keeps the old label — today's rendering exactly. */
3318
+ function renderRecL1(mp){
3319
+ const row=el('rec-l1'), sub=el('rec-sub'), dv=el('rec-div');
3320
+ if(!mp||!mp.level1||!mp.level2){
3321
+ row.hidden=true; dv.hidden=true; dv.title='';
3322
+ if(sub) sub.textContent='uses per showing';
3323
+ return;
3324
+ }
3325
+ if(sub) sub.textContent='last '+(mp.window_days||90)+' days';
3326
+ const l1=mp.level1;
3327
+ const floor=(F&&F.thresholds&&typeof F.thresholds.recall_min_similarity==='number')?F.thresholds.recall_min_similarity:null;
3328
+ const ra=(l1.thresholds&&typeof l1.thresholds.redundant_above==='number')?l1.thresholds.redundant_above:null;
3329
+ const mins=(l1.thresholds&&typeof l1.thresholds.divergence_min_shown==='number')?l1.thresholds.divergence_min_shown:5;
3330
+ const sim=(typeof l1.mean_similarity==='number'&&isFinite(l1.mean_similarity))?l1.mean_similarity:null;
3331
+ const red=(typeof l1.redundant_share==='number'&&isFinite(l1.redundant_share))?l1.redundant_share:null;
3332
+ if(sim===null){
3333
+ row.hidden=true; /* nothing measured in the window — the row degrades away */
3334
+ }else{
3335
+ row.hidden=false;
3336
+ row.classList.toggle('below',floor!==null&&sim<floor);
3337
+ /* numbers only on the face — direction carries the meaning, no verdict words */
3338
+ /* #476 owner rename (2026-09-20): the three buckets as named — Relevant /
3339
+ Repeated known / Off-target. Relevant = the mean relevance; Repeated =
3340
+ the redundant share; Off-target = the remainder (pushed lines that were
3341
+ neither relevant nor new). Numbers, no verdict words (#426). */
3342
+ const relPct=Math.round(clamp(sim,0,1)*100);
3343
+ const repPct=red!==null?Math.round(clamp(red,0,1)*100):0;
3344
+ const offPct=Math.max(0,100-relPct-repPct);
3345
+ const dv=mp.divergence, nUsed=(dv&&typeof dv.count==='number')?dv.count:0;
3346
+ el('l1-rel').innerHTML='<b>Relevant: '+relPct+'%</b>';
3347
+ el('l1-rep').textContent='Repeated known: '+repPct+'%';
3348
+ el('l1-off').textContent='Off-target: '+offPct+'%'+(nUsed>0?' ('+nUsed+' never used)':'');
3349
+ row.title='pushed index, last '+(mp.window_days||90)+' days — mean similarity of pushed lines to their prompt (tick at the recall floor '+
3350
+ (floor!==null?floor.toFixed(2):'—')+'; muted below it)'+
3351
+ (ra!==null?'; redundant = share of lines at ≥'+ra.toFixed(2)+' cosine vs the session\'s standing context (lessons + identity)':'')+
3352
+ ' · '+fmt(l1.pushes)+' pushes, '+fmt(l1.lines)+' lines';
3353
+ }
3354
+ /* divergence — the face count is amber; the ≤3 lines ride the native
3355
+ tooltip ONLY (text via .title, never face content) */
3356
+ /* #476 owner bundling (2026-09-20): the never-used count rides inside
3357
+ Off-target; the separate face element stays hidden (titles on hover) */
3358
+ const dEl=el('rec-div');
3359
+ if(dEl){ dEl.hidden=true; }
3360
+ }
3361
+
3235
3362
  let MODEL_LABEL='';
3236
3363
  /* #409 fix round 5 — owner ruling 2026-09-14 (verbatim): "I do NOT want to
3237
3364
  know what it is compatible with, I want to know the fucking model." The
@@ -105,6 +105,39 @@ export declare const RECALL_USES_NORMAL_MAX = 0.25;
105
105
  * PROVISIONAL (owner anchor 2026-09-13, #426) — chosen so Overfetching
106
106
  * keeps a visible span, not a measured bound. */
107
107
  export declare const RECALL_USES_AXIS_MAX = 0.3;
108
+ /**
109
+ * Retention horizon for the recall_pushes/recall_events sidecar tables AND the
110
+ * longest window the Memory Precision card shows — ONE constant feeds both
111
+ * (the CAPTURE_HEALTH_WINDOW_DAYS single-constant law: the card can never
112
+ * claim a window the store no longer has rows for). 90 = the longest #452
113
+ * range option that the retention can honestly serve; 180d and all clamp to
114
+ * it server-side and the payload echoes the effective window_days.
115
+ */
116
+ export declare const MEMORY_PRECISION_WINDOW_DAYS = 90;
117
+ /**
118
+ * Cosine at/above which a pushed line reads "redundant" (restating the
119
+ * session's standing context — the top lessons + identity every session-start
120
+ * hook injects). Direction-only display guidance, never a gate. 0.80 mirrors
121
+ * SUPERSESSION_MIN_SIMILARITY: at/above it a pair reads as the same
122
+ * statement, below it as topical overlap — a reasonable first anchor until
123
+ * the judged-calibration follow-up measures the real distribution (the
124
+ * #426 provisional-anchor posture).
125
+ */
126
+ export declare const MEMORY_PRECISION_REDUNDANT_ABOVE = 0.8;
127
+ /**
128
+ * Window showings a live memory needs (with zero window fetches) to count as
129
+ * DIVERGING — the index keeps pushing it, nothing ever reads it. 5 is the
130
+ * spec's anchor (#476): enough showings that the silence is a pattern, not
131
+ * one prompt's turn-suppression.
132
+ */
133
+ export declare const MEMORY_PRECISION_DIVERGENCE_MIN_SHOWN = 5;
134
+ /**
135
+ * Cap on the prompt excerpt persisted per recall_pushes row (Option A,
136
+ * owner-blessed 2026-09-19: the first-ever server-side persistence of raw
137
+ * prompt text, bounded to 256 chars and pruned at the retention horizon —
138
+ * the same trust boundary as /distill's denoised conversation text).
139
+ */
140
+ export declare const MEMORY_PRECISION_PROMPT_EXCERPT_CHARS = 256;
108
141
  /** Semantic-similarity share of the composite score. 0.50 (raised from 0.40
109
142
  * in the 0.15.2 rebalance, #191 Phase B): on the production corpus effective
110
143
  * strength (0.30) outweighed what similarity could recover — hardened old
@@ -223,6 +256,18 @@ export declare const BM25_WEIGHT_DOMAIN = 2;
223
256
  * base-1.0 rows decay again.
224
257
  */
225
258
  export declare const IMPORTANCE_CEILING = 0.95;
259
+ /**
260
+ * How many days a memory stays in the nightly importance-settle pool after
261
+ * ingest (#478): a row's importance re-settles nightly while young — keyed
262
+ * on PER-ROW age (ingested_at), never the global lastConsolidated
263
+ * watermark, which sticks whenever a run defers and used to drag a growing
264
+ * cohort back through re-scoring (each re-settle overwrites base_strength,
265
+ * erasing promotion gains). Rows leave the pool after the window; promoted
266
+ * or enriched rows leave earlier via the paid-gain guard. Release-managed
267
+ * (#408 discipline): moving it is a release decision with soak evidence,
268
+ * not a config knob.
269
+ */
270
+ export declare const IMPORTANCE_SETTLE_WINDOW_DAYS = 3;
226
271
  /** Deterministic merge ceiling of the unified resolution pass (#392): pairs
227
272
  * at/above this cosine merge LLM-free; [CORRECTION_MIN_SIMILARITY, this)
228
273
  * get the one verdict call. 0.92 — measured on the #191 mechanical audit
@@ -24,8 +24,8 @@
24
24
  * the removal is never silent.
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.OLLAMA_FLUSH_WAIT_MS = exports.OLLAMA_FLUSH_EVERY = exports.NUM_CTX = exports.DEFAULT_FIRST_RUN_LOOKBACK_DAYS = exports.ENRICH_STRENGTH_DELTA = exports.STAGE_TRUTH_STRENGTH = exports.STAGE_BELIEF_STRENGTH = exports.STAGE_FADING_STRENGTH = exports.STAGE_FADING_DAYS = exports.WEAK_PRIMARY_FLOOR = exports.CORRECTION_REWRITE_MIN_CONFIDENCE = exports.CORRECTION_MIN_SIMILARITY = exports.SUPERSESSION_MIN_SIMILARITY = exports.DEDUP_AUTO_MERGE_THRESHOLD = exports.IMPORTANCE_CEILING = exports.BM25_WEIGHT_DOMAIN = exports.BM25_WEIGHT_PROJECT = exports.BM25_WEIGHT_BODY = exports.BOTH_CHANNEL_BOOST = exports.RRF_VECTOR_WEIGHT = exports.RRF_FTS_WEIGHT = exports.RRF_COMPOSITE_WEIGHT = exports.RRF_K = exports.SCOPE_AFFINITY_WEIGHT = exports.SUPERSEDED_DEMOTION = exports.RECENCY_HEAD_DAYS = exports.RECENCY_HEAD_WEIGHT = exports.RECENCY_HOURLY_DECAY = exports.SCORE_RECENCY_WEIGHT = exports.CONNECTIONS_SATURATION_DEGREE = exports.SCORE_CONNECTIONS_WEIGHT = exports.SCORE_STRENGTH_WEIGHT = exports.SCORE_SIMILARITY_WEIGHT = exports.RECALL_USES_AXIS_MAX = exports.RECALL_USES_NORMAL_MAX = exports.RECALL_USES_LOW_MAX = exports.RECALL_RESHOW_TURNS = exports.NOVELTY_FLOOR_SLOTS = exports.RECALL_TITLE_CHARS = exports.RECALL_MIN_PROMPT_CHARS = exports.RECALL_MAX_ITEMS = exports.RECALL_MIN_SIMILARITY = exports.SESSION_INTENT_WEIGHT = exports.COLD_EXPOSURE_SLOTS = exports.RECENT_WINDOW_DAYS = exports.RECENT_LIMIT = exports.SEARCH_LIMIT = exports.PROMOTION_STRENGTH_FLOOR = exports.PROMOTION_RATE = exports.DECAY_HALF_LIFE_DAYS = void 0;
28
- exports.DIAGNOSTIC_ENV_TIER = void 0;
27
+ exports.STAGE_TRUTH_STRENGTH = exports.STAGE_BELIEF_STRENGTH = exports.STAGE_FADING_STRENGTH = exports.STAGE_FADING_DAYS = exports.WEAK_PRIMARY_FLOOR = exports.CORRECTION_REWRITE_MIN_CONFIDENCE = exports.CORRECTION_MIN_SIMILARITY = exports.SUPERSESSION_MIN_SIMILARITY = exports.DEDUP_AUTO_MERGE_THRESHOLD = exports.IMPORTANCE_SETTLE_WINDOW_DAYS = exports.IMPORTANCE_CEILING = exports.BM25_WEIGHT_DOMAIN = exports.BM25_WEIGHT_PROJECT = exports.BM25_WEIGHT_BODY = exports.BOTH_CHANNEL_BOOST = exports.RRF_VECTOR_WEIGHT = exports.RRF_FTS_WEIGHT = exports.RRF_COMPOSITE_WEIGHT = exports.RRF_K = exports.SCOPE_AFFINITY_WEIGHT = exports.SUPERSEDED_DEMOTION = exports.RECENCY_HEAD_DAYS = exports.RECENCY_HEAD_WEIGHT = exports.RECENCY_HOURLY_DECAY = exports.SCORE_RECENCY_WEIGHT = exports.CONNECTIONS_SATURATION_DEGREE = exports.SCORE_CONNECTIONS_WEIGHT = exports.SCORE_STRENGTH_WEIGHT = exports.SCORE_SIMILARITY_WEIGHT = exports.MEMORY_PRECISION_PROMPT_EXCERPT_CHARS = exports.MEMORY_PRECISION_DIVERGENCE_MIN_SHOWN = exports.MEMORY_PRECISION_REDUNDANT_ABOVE = exports.MEMORY_PRECISION_WINDOW_DAYS = exports.RECALL_USES_AXIS_MAX = exports.RECALL_USES_NORMAL_MAX = exports.RECALL_USES_LOW_MAX = exports.RECALL_RESHOW_TURNS = exports.NOVELTY_FLOOR_SLOTS = exports.RECALL_TITLE_CHARS = exports.RECALL_MIN_PROMPT_CHARS = exports.RECALL_MAX_ITEMS = exports.RECALL_MIN_SIMILARITY = exports.SESSION_INTENT_WEIGHT = exports.COLD_EXPOSURE_SLOTS = exports.RECENT_WINDOW_DAYS = exports.RECENT_LIMIT = exports.SEARCH_LIMIT = exports.PROMOTION_STRENGTH_FLOOR = exports.PROMOTION_RATE = exports.DECAY_HALF_LIFE_DAYS = void 0;
28
+ exports.DIAGNOSTIC_ENV_TIER = exports.OLLAMA_FLUSH_WAIT_MS = exports.OLLAMA_FLUSH_EVERY = exports.NUM_CTX = exports.DEFAULT_FIRST_RUN_LOOKBACK_DAYS = exports.ENRICH_STRENGTH_DELTA = void 0;
29
29
  exports.resolveNumCtx = resolveNumCtx;
30
30
  exports.resolveOllamaFlushEvery = resolveOllamaFlushEvery;
31
31
  exports.resolveOllamaFlushWaitMs = resolveOllamaFlushWaitMs;
@@ -127,6 +127,44 @@ exports.RECALL_USES_NORMAL_MAX = 0.25;
127
127
  * PROVISIONAL (owner anchor 2026-09-13, #426) — chosen so Overfetching
128
128
  * keeps a visible span, not a measured bound. */
129
129
  exports.RECALL_USES_AXIS_MAX = 0.30;
130
+ // Memory-precision family (#476) — the console card's deterministic proxies.
131
+ // All four are release-managed per this module's evolution contract: no config
132
+ // keys, nothing hardcoded elsewhere. Values are stored THRESHOLD-FREE (raw
133
+ // cosines in recall_events); the thresholds below are applied at RENDER time
134
+ // only, so recalibrating them never rewrites history.
135
+ /**
136
+ * Retention horizon for the recall_pushes/recall_events sidecar tables AND the
137
+ * longest window the Memory Precision card shows — ONE constant feeds both
138
+ * (the CAPTURE_HEALTH_WINDOW_DAYS single-constant law: the card can never
139
+ * claim a window the store no longer has rows for). 90 = the longest #452
140
+ * range option that the retention can honestly serve; 180d and all clamp to
141
+ * it server-side and the payload echoes the effective window_days.
142
+ */
143
+ exports.MEMORY_PRECISION_WINDOW_DAYS = 90;
144
+ /**
145
+ * Cosine at/above which a pushed line reads "redundant" (restating the
146
+ * session's standing context — the top lessons + identity every session-start
147
+ * hook injects). Direction-only display guidance, never a gate. 0.80 mirrors
148
+ * SUPERSESSION_MIN_SIMILARITY: at/above it a pair reads as the same
149
+ * statement, below it as topical overlap — a reasonable first anchor until
150
+ * the judged-calibration follow-up measures the real distribution (the
151
+ * #426 provisional-anchor posture).
152
+ */
153
+ exports.MEMORY_PRECISION_REDUNDANT_ABOVE = 0.8;
154
+ /**
155
+ * Window showings a live memory needs (with zero window fetches) to count as
156
+ * DIVERGING — the index keeps pushing it, nothing ever reads it. 5 is the
157
+ * spec's anchor (#476): enough showings that the silence is a pattern, not
158
+ * one prompt's turn-suppression.
159
+ */
160
+ exports.MEMORY_PRECISION_DIVERGENCE_MIN_SHOWN = 5;
161
+ /**
162
+ * Cap on the prompt excerpt persisted per recall_pushes row (Option A,
163
+ * owner-blessed 2026-09-19: the first-ever server-side persistence of raw
164
+ * prompt text, bounded to 256 chars and pruned at the retention horizon —
165
+ * the same trust boundary as /distill's denoised conversation text).
166
+ */
167
+ exports.MEMORY_PRECISION_PROMPT_EXCERPT_CHARS = 256;
130
168
  // ---------------------------------------------------------------------------
131
169
  // Composite ranking weights (was: score*Weight, supersededDemotion,
132
170
  // *AffinityWeight, rrf*; the pre-#430 freshness bonus constants merged into
@@ -260,6 +298,18 @@ exports.BM25_WEIGHT_DOMAIN = 2.0;
260
298
  * base-1.0 rows decay again.
261
299
  */
262
300
  exports.IMPORTANCE_CEILING = 0.95;
301
+ /**
302
+ * How many days a memory stays in the nightly importance-settle pool after
303
+ * ingest (#478): a row's importance re-settles nightly while young — keyed
304
+ * on PER-ROW age (ingested_at), never the global lastConsolidated
305
+ * watermark, which sticks whenever a run defers and used to drag a growing
306
+ * cohort back through re-scoring (each re-settle overwrites base_strength,
307
+ * erasing promotion gains). Rows leave the pool after the window; promoted
308
+ * or enriched rows leave earlier via the paid-gain guard. Release-managed
309
+ * (#408 discipline): moving it is a release decision with soak evidence,
310
+ * not a config knob.
311
+ */
312
+ exports.IMPORTANCE_SETTLE_WINDOW_DAYS = 3;
263
313
  /** Deterministic merge ceiling of the unified resolution pass (#392): pairs
264
314
  * at/above this cosine merge LLM-free; [CORRECTION_MIN_SIMILARITY, this)
265
315
  * get the one verdict call. 0.92 — measured on the #191 mechanical audit
@@ -175,10 +175,15 @@ async function runClassifyDomains(options = {}) {
175
175
  // everything from the final tag sets.
176
176
  const { prototypes } = await (0, schema_prototypes_js_1.computeDomainPrototypes)(db, domains, getEmbedFn);
177
177
  // Scope filter: default = NULL / not-in-set / no tags yet; --all = everything.
178
+ // #477: absorbed dedup losers are excluded from the default scope (dead
179
+ // evidence — the merge already cleared their tags and nulled domain);
180
+ // conjoined OUTSIDE the parenthesized OR group, same as the nightly
181
+ // stageContentDomains twin. --all stays a wholesale operator re-judge.
178
182
  const placeholders = domains.map(() => "?").join(", ");
179
183
  const scopeSql = all
180
184
  ? "rowid > ?"
181
- : `rowid > ? AND (domain IS NULL OR domain NOT IN (${placeholders}) ` +
185
+ : `rowid > ? AND COALESCE(status, '') != 'absorbed' ` +
186
+ `AND (domain IS NULL OR domain NOT IN (${placeholders}) ` +
182
187
  `OR id NOT IN (SELECT DISTINCT memory_id FROM memory_tags))`;
183
188
  const batchStmt = db.prepare(`SELECT rowid AS __rowid, id, content, project, domain FROM memories
184
189
  WHERE ${scopeSql} ORDER BY rowid ASC LIMIT ?`);