@gamaze/hicortex 0.22.1 → 0.22.3

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.
@@ -39,7 +39,7 @@
39
39
  - Overview: the counter/bar/headroom key off LIVE memories (absorbed
40
40
  rows show as an "absorbed (kept as evidence)" note); the counter is
41
41
  STATIC (no replay tracking — owner ruling 2026-09-13).
42
- - Recall card: the RAW uses-per-showing ratio + a 7v7-nights trend, NO
42
+ - Recall card: the RAW uses-per-showing ratio, NO
43
43
  grade verdict (#426 owner semantics ruling: recall depends on the
44
44
  conversation, higher is not a target).
45
45
  - Rail header: "Fleet".
@@ -89,7 +89,7 @@
89
89
  default, which used to degrade the corner to a standalone Upgrade
90
90
  button. The upgrade CTA is a MENU ITEM (same plan-unset eligibility
91
91
  the shipped #365 code used); no standalone upgrade button remains.
92
- - RECALL CARD: below the band at most ONE subtle line (trend glyph +
92
+ - RECALL CARD: below the band at most ONE subtle line (zone word +
93
93
  muted "provisional"; pill chrome dropped); the zone word rides the
94
94
  marker, the zone-label row is gone — the card stops setting the row
95
95
  height.
@@ -135,7 +135,7 @@
135
135
  DOWN into the siblings' chart slot (header → big → band → foot).
136
136
  "provisional" is hover-only now — a tooltip on the band (the card
137
137
  stays marked; the visible word failed to communicate). Vocabulary
138
- amendment: Low / HEALTHY / Overfetching. Marker + trend stay.
138
+ amendment: Low / HEALTHY / Overfetching. Marker + zone word stay.
139
139
 
140
140
  REPLAY + RANGE (#452, owner directives 2026-09-16):
141
141
  - 30-DAY DEFAULT FOR ALL VIEWS: the replay ledger, the band and the
@@ -397,10 +397,6 @@
397
397
  .big .n.noconf{ font-size:14.5px; font-weight:520; letter-spacing:0; color:var(--dim); }
398
398
  html:not([data-theme="light"]) .big .n.noconf{ text-shadow:none; }
399
399
  .big .l{ font-size:10px; color:var(--faint); text-transform:uppercase; letter-spacing:.13em; }
400
- .trend{ font-size:12px; line-height:1; flex:none; }
401
- .trend.up{ color:var(--green-mid); }
402
- .trend.down{ color:var(--red); }
403
- .trend.flat{ color:var(--dim); }
404
400
  /* THE card sub-line — one text type */
405
401
  .csub{ font-size:11px; color:var(--dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
406
402
  .csub b{ color:var(--green-mid); font-weight:600; }
@@ -419,7 +415,7 @@
419
415
  row). The visible "provisional" word is GONE — the marking is hover-only
420
416
  now, a tooltip on the band (the visible word failed to communicate).
421
417
  Vocabulary (owner 2026-09-14): Low / Healthy / Overfetching. The raw
422
- number stays tooltip-only on the marker; the trend glyph stays (mock
418
+ number stays tooltip-only on the marker (mock
423
419
  family). Colors: the band + word carry concrete per-theme colors from
424
420
  the JS stop table (setTheme re-renders), the dot rides theme tokens. */
425
421
  .rec-word.none{ color:var(--dim); }
@@ -427,6 +423,24 @@
427
423
  .rec-band{ position:absolute; inset:0; border-radius:4px; }
428
424
  .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
425
  .rec-foot{ display:flex; align-items:center; gap:7px; }
426
+ /* Memory Precision Level 1 (#476) — the pushed-index row: a compact
427
+ second line in the .csub scale (the sibling-rhythm ruling 2026-09-14:
428
+ the card keeps the shared .bseg gap rhythm, header → big → band →
429
+ index row → foot). Direction-only coloring: green at/above the echoed
430
+ recall floor, muted below (the index bar reads conservative); the
431
+ floor tick is amber like the pace marker. Theme tokens only — both
432
+ themes render from their own table, no JS colors here. */
433
+ .rec-l1{ display:block; font-size:10.5px; color:var(--dim); min-width:0; line-height:1.6; }
434
+ .rec-l1 .l1row{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
435
+ .rec-l1 .lbl{ display:none; } /* #476 rename: the three-bucket text is self-labeled */
436
+ .rec-l1 .l1bar{ position:relative; flex:1 1 46px; height:5px; border-radius:3px; background:var(--tbar-bg); min-width:36px; }
437
+ .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); }
438
+ .rec-l1.below .l1bar .l1-fill{ background:color-mix(in srgb, var(--dim) 55%, transparent); }
439
+ .rec-l1 .l1bar .l1-floor{ position:absolute; top:-2px; bottom:-2px; width:1px; background:var(--amber); }
440
+ .rec-l1 .l1txt{ color:var(--dim); overflow:hidden; text-overflow:ellipsis; font-size:10.5px; letter-spacing:-.01em; }
441
+ .rec-l1 b{ color:var(--green-mid); font-weight:600; }
442
+ .rec-l1.below b{ color:var(--dim); }
443
+ #rec-div{ color:var(--amber); overflow:hidden; text-overflow:ellipsis; }
430
444
  .b-health{ display:flex; align-items:center; gap:7px; font-size:11px; color:var(--dim); white-space:nowrap; }
431
445
  .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
446
  .dot.blue{ background:var(--blue); box-shadow:0 0 10px color-mix(in srgb, var(--blue) 70%, transparent); }
@@ -1159,21 +1173,32 @@
1159
1173
  <div class="csub" id="tok-sub">&nbsp;</div>
1160
1174
  <div id="tok-nights" aria-label="LLM tokens per nightly run this billing period"></div>
1161
1175
  </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). -->
1176
+ <!-- Memory Precision (#476, owner retitle) — ONE card, two context
1177
+ levels. Level 2 (recall depth) keeps the #426 U-band + zone word +
1178
+ provisional-hover semantics; the marker/tooltip read the WINDOW
1179
+ ratio off D.memory_precision.level2 (a pre-#476 server has no block
1180
+ → the headline ratio — today's rendering exactly). Level 1 (pushed
1181
+ index) is a compact second row: mean similarity of pushed lines to
1182
+ their prompt, with a floor marker at the echoed
1183
+ recall_min_similarity and muted below it (the index's bar reads
1184
+ conservative — its noise costs compound per prompt) + the
1185
+ redundancy share; direction-only coloring, NO verdict words (the
1186
+ #426 semantics ruling extended: similarity is
1187
+ conversation-dependent). Divergence count on the FACE, the ≤3
1188
+ titles hover-only (no memory lists on the main page — owner ruling
1189
+ 2026-09-12; the header search panel is the list surface). The
1190
+ window rides the #452 selector via the echoed window_days — never
1191
+ hardcoded, never a second range control. -->
1172
1192
  <div class="bseg" id="lim-rec">
1173
- <div class="gh"><span class="t">Recall level</span><span class="sub">uses per showing</span></div>
1193
+ <div class="gh"><span class="t">Memory Precision</span><span class="sub" id="rec-sub">uses per showing</span></div>
1174
1194
  <div class="big"><b class="n rec-word" id="rec-zone">—</b></div>
1175
1195
  <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>
1196
+ <div class="rec-l1" id="rec-l1" hidden style="display:block">
1197
+ <div class="l1row" id="l1-rel">&nbsp;</div>
1198
+ <div class="l1row" id="l1-rep">&nbsp;</div>
1199
+ <div class="l1row" id="l1-off">&nbsp;</div>
1200
+ </div>
1201
+ <div class="csub rec-foot"><span id="rec-div" hidden></span></div>
1177
1202
  </div>
1178
1203
  </div>
1179
1204
  <!-- row 2 · ops & maturity — capture health · 30 nights · maturity split · model -->
@@ -1488,8 +1513,11 @@ const DEPTH_BANDS = { truth:[.9,1.05], belief:[1.05,1.3], forming:[1.3,1.55], fa
1488
1513
  post-backfill store still shows a visible gradient and the inflated
1489
1514
  pre-#425 store shows no monsters. Robust by construction: the mapping is
1490
1515
  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; }
1516
+ a skewed store. Pure functions — the ratio is behaviorally tested.
1517
+ #474 graph final (owner 2026-09-19): the balls at 30% — 1.8→2.7, exactly
1518
+ 0.30× of the 6→9 law; the depth divisor and the ruled ratio band are
1519
+ scale-invariant and unchanged. */
1520
+ function bubbleRadius(s){ return 1.8 + Math.sqrt(clamp(s??0,0,1))*0.9; }
1493
1521
  function depthSizeDivisor(dz){ return Math.pow(Math.max(.14,dz),0.35); }
1494
1522
 
1495
1523
  /* ---------- live-data state ---------- */
@@ -1506,7 +1534,7 @@ let TOTAL=0, removedNow=0;
1506
1534
  clock (owner ruling 2026-09-13; only the boot reveal counts up) */
1507
1535
 
1508
1536
  /* =====================================================================
1509
- WORLD — domains, memories (every one), the interactive sample, real
1537
+ WORLD — domains, memories, the dateline window (the visible set), real
1510
1538
  links, the replay ledger. All positions derive from id hashes → stable
1511
1539
  across reloads and across the refreshes after Update/Remove.
1512
1540
  ===================================================================== */
@@ -1515,8 +1543,14 @@ const PORT = [ [.50,.20,.15],[.28,.38,.12],[.74,.40,.12],[.50,.62,.13],[.25,.68,
1515
1543
  let DOMAINS = []; // {name,count,land,port,cx,cy,cr}
1516
1544
  const mems = new Map(); // id -> memory object (live + replay ghosts)
1517
1545
  const links = []; // real link edges, engine-decorated
1518
- const bubbles = []; // the ~120-mem interactive importance sample
1519
1546
  let crowdByStage = {}; // stage -> [mem refs] (EVERY memory paints)
1547
+ const bubbles = []; // the latest-N-by-dateline visible set (#474
1548
+ // return; owned by refreshWindow, rebuilt on T)
1549
+ const FIELD_WINDOW_N = 1000; // #474 graph final (owner 2026-09-19): the field shows the latest N memories by dateline
1550
+ let WIN_ORDER = []; // every mem (live + ghosts), effCreated DESC, id ASC
1551
+ let winSet = new Set(); // ids in the window at the current T
1552
+ let winCount = []; // per-domain window counts (labels + tip)
1553
+ let winT = -1;
1520
1554
  let byAgentCounts = {}; // source_agent -> count
1521
1555
  let CONTEXTS = []; // [{id,name,machine,count}] — #421 machine × harness grouping
1522
1556
  let adj = new Map(); // id -> [neighbor ids] (real links)
@@ -1525,6 +1559,13 @@ let learnedL = [], mergedL = [], enrichedL = [];
1525
1559
  let NIGHTS = []; // the events payload's night list
1526
1560
  let WINDOW = 1, NOW_T = .999, REPLAY_OK = false;
1527
1561
 
1562
+ /* Replay pacing (owner 2026-09-19: 2x faster default). Event-driven cruise:
1563
+ each ledger event lands every REPLAY_TICK_MS; a heralded (birth/merge)
1564
+ event holds the clock REPLAY_HERALD_MS then applies; boundary beats rest
1565
+ REPLAY_REST_MS. Halve all three to run 2x; the constants are the only
1566
+ pacing surface. */
1567
+ const REPLAY_TICK_MS=205, REPLAY_HERALD_MS=108, REPLAY_REST_MS=800;
1568
+
1528
1569
  function domIndex(domain){ const i = DOMAINS.findIndex(d=>d.name===domain); return i<0?0:i; }
1529
1570
  function titleOf(id){ const m=mems.get(id); return m?m.title:'(absorbed)'; }
1530
1571
 
@@ -1589,6 +1630,10 @@ function buildWorld(){
1589
1630
  if(!m.ghost && !keep.has(id)) mems.delete(id);
1590
1631
  }
1591
1632
 
1633
+ // the crowd: EVERY memory, bucketed by stage for one fillStyle per pass
1634
+ crowdByStage = { forming:[], belief:[], truth:[], fading:[] };
1635
+ for(const m of mems.values()) (crowdByStage[m.stage]||crowdByStage.forming).push(m);
1636
+
1592
1637
  // replay metadata from the events ledger: in-window births + merge-outs
1593
1638
  // (absorbed losers get ghost entries so their merge animation has a
1594
1639
  // position and their birth a bloom — they are not part of the live field).
@@ -1657,34 +1702,12 @@ function buildWorld(){
1657
1702
  }
1658
1703
  }
1659
1704
 
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
- }
1705
+ // #474 return: the visible set is the dateline WINDOW, not a sample —
1706
+ // WIN_ORDER walks every mem (live + ghosts) newest-first (effCreated DESC,
1707
+ // id ASC tie-break; ghosts sort via their birth night's date), and
1708
+ // refreshWindow (draw) slices the latest FIELD_WINDOW_N alive-at-T rows
1709
+ // out of it each time T moves.
1710
+ WIN_ORDER = Array.from(mems.values()).sort(winCmp);
1688
1711
 
1689
1712
  // contexts (source_agent values) with their machine stamps (#421 machine ×
1690
1713
  // harness): the rail + capture card group on (machine ?? legacy) × harness.
@@ -1746,6 +1769,31 @@ function aliveAt(b,t){
1746
1769
  return true;
1747
1770
  }
1748
1771
  function dateAt(t){ const i=clamp(Math.floor(t),0,NIGHTS.length-1); return NIGHTS[i]?NIGHTS[i].date:''; }
1772
+ /* #474 return: the visible set is a DATELINE WINDOW — at replay clock T the
1773
+ field draws the latest FIELD_WINDOW_N memories by effective created date
1774
+ (created_at; ghosts via their birth night), not a sample and not the whole
1775
+ store. Pure helpers (behaviorally tested). */
1776
+ function effCreated(m){ return m.created || (m.born>=0 ? dateAt(m.born) : ''); }
1777
+ function windowPick(order, t, n, dl, isNow){
1778
+ const out=[];
1779
+ for(const m of order){
1780
+ if(out.length>=n) break;
1781
+ if(!aliveAt(m,t)) continue;
1782
+ if(!isNow && m.born<0 && effCreated(m)>dl) continue; // untracked + newer than the dateline
1783
+ out.push(m);
1784
+ }
1785
+ return out;
1786
+ }
1787
+ function winCmp(a,b){ return effCreated(b)<effCreated(a)?-1: effCreated(b)>effCreated(a)?1: a.id<b.id?-1:1; }
1788
+ function refreshWindow(t){
1789
+ if(t===winT) return;
1790
+ winT=t;
1791
+ const picked = windowPick(WIN_ORDER, t, FIELD_WINDOW_N, dateAt(t), t>=NOW_T);
1792
+ winSet = new Set(picked.map(m=>m.id));
1793
+ winCount = new Array(DOMAINS.length).fill(0);
1794
+ for(const m of picked) winCount[m.dom]++;
1795
+ bubbles.length=0; bubbles.push(...picked);
1796
+ }
1749
1797
 
1750
1798
  /* =====================================================================
1751
1799
  THEME — one engine, two palettes. Dark (the brand night: glow) is the
@@ -1903,22 +1951,16 @@ function sprite(stage, r){
1903
1951
  const st = PAL.stages[stage] || PAL.stages.forming;
1904
1952
  const rgb = st.rgb;
1905
1953
  if(PAL.name==='dark'){
1954
+ /* #474 final polish (owner 2026-09-20): plain dot — no glow halo, no
1955
+ white core gradient. A filled circle with a slightly brighter edge;
1956
+ the maturity color carries the reading, the size stays small. */
1906
1957
  const S = Math.ceil(rad*PAL.sprK), c2 = document.createElement('canvas');
1907
1958
  c2.width=S; c2.height=S;
1908
1959
  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();
1960
+ g.fillStyle=rgba(rgb,.88*st.alpha);
1961
+ g.beginPath(); g.arc(cxp,cxp,rad*.82,0,7); g.fill();
1962
+ g.strokeStyle=rgba(rgb,.45*st.alpha); g.lineWidth=1;
1963
+ g.beginPath(); g.arc(cxp,cxp,rad*.82,0,7); g.stroke();
1922
1964
  /* #422 round 2: fading's DISTINCT outer ring — the rim stage's fill runs
1923
1965
  at low alpha by design; without its own edge treatment the dots were
1924
1966
  illegible against the night ground */
@@ -2001,7 +2043,7 @@ function buildPaths(){
2001
2043
  }
2002
2044
  }
2003
2045
  function pathAlive(p,t){
2004
- for(const id of p.nodes){ const b=mems.get(id); if(!b||!aliveAt(b,t)) return false; }
2046
+ for(const id of p.nodes){ const b=mems.get(id); if(!b||!aliveAt(b,t)||!winSet.has(id)) return false; }
2005
2047
  return t>=p.born;
2006
2048
  }
2007
2049
  function spawnSignal(showcase){
@@ -2060,8 +2102,10 @@ function advanceSignals(now, dt){
2060
2102
  }
2061
2103
 
2062
2104
  /* =====================================================================
2063
- THE FRAME — theme-aware; crowd first (micro-stars), then synapses,
2064
- signals, bubbles (painter-sorted far→near), waymarkers, furniture.
2105
+ THE FRAME — theme-aware; synapses, signals, bubbles (painter-sorted
2106
+ far→near), waymarkers, furniture. #474 return: the ONLY dots drawn are
2107
+ the dateline window (latest FIELD_WINDOW_N by effective created date at
2108
+ the replay clock — see refreshWindow); no crowd pass, no sample.
2065
2109
  ===================================================================== */
2066
2110
  let labelRects=[];
2067
2111
  /* emergence queue (#422 live review): a night's births/links become VISIBLE
@@ -2105,6 +2149,7 @@ function processEmergence(t){
2105
2149
  }
2106
2150
  function draw(now, dt){
2107
2151
  const t=T;
2152
+ refreshWindow(t); // #474: the visible set = latest-N by dateline at this T (bubbles, winSet, winCount)
2108
2153
  const dark = PAL.name==='dark';
2109
2154
  const ADD = dark ? 'lighter' : 'source-over';
2110
2155
  ctx.clearRect(0,0,view.w,view.h);
@@ -2170,6 +2215,12 @@ function draw(now, dt){
2170
2215
  }
2171
2216
  ctx.globalAlpha=1;
2172
2217
 
2218
+ /* emergence flags resolve FIRST (once per frame) — the visible set and
2219
+ the link pass below read them; dots still queued (_emerged===false)
2220
+ stay invisible until their frame budget comes up. (#474: the old
2221
+ every-memory crowd pass is gone — the window IS the field.) */
2222
+ processEmergence(t);
2223
+
2173
2224
  /* THE CROWD — every memory in the store as maturity-colored micro-stars
2174
2225
  behind the touchable sample (no sampling: the dot count IS the store).
2175
2226
  One fillStyle per stage pass. The emergence queue is resolved FIRST
@@ -2187,7 +2238,7 @@ function draw(now, dt){
2187
2238
  if(fog<.15) continue;
2188
2239
  const [sx,sy]=proj3(m.nx||.5,m.ny||.5,dz);
2189
2240
  if(sx<-2||sy<-2||sx>view.w+2||sy>view.h+2) continue;
2190
- ctx.globalAlpha=fog*.5;
2241
+ ctx.globalAlpha=fog*.5*(focusDom!=null&&m.dom!==focusDom?.15:1);
2191
2242
  const px=Math.max(1,Math.round((rand01(m.id,9)<.85?1:1.8)/dz*1.4));
2192
2243
  ctx.fillRect(sx,sy,px,px);
2193
2244
  }
@@ -2241,6 +2292,7 @@ function draw(now, dt){
2241
2292
  if(L.rk>=lodK) continue; // zoom LOD (lines only)
2242
2293
  const a=mems.get(L.a), b=mems.get(L.b);
2243
2294
  if(!a||!b||!aliveAt(a,t)||!aliveAt(b,t)) continue;
2295
+ /* #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
2296
  if(a._emerged===false||b._emerged===false) continue; // thread waits for both endpoints
2245
2297
  const [ax,ay]=bpos(a,now,t), [bx,by]=bpos(b,now,t);
2246
2298
  const ctrl=segCtrlN(ax,ay,bx,by,(L.use>18?1:-1));
@@ -2255,6 +2307,9 @@ function draw(now, dt){
2255
2307
  const prog = L.forming ? clamp((t-(L._emergeT!==undefined?L._emergeT:L.born))/1.4,0,1) : 1;
2256
2308
  const drawP=Math.min(prog,linkBoot);
2257
2309
  if(drawP<=0) continue;
2310
+ /* #474 final polish: domain focus — a thread stays lit only if an
2311
+ endpoint belongs to the focused domain (owner 2026-09-20) */
2312
+ const focusDim = (focusDom!=null && a.dom!==focusDom && b.dom!==focusDom) ? .15 : 1;
2258
2313
  const len=Math.hypot(x2-x1,y2-y1)*1.15;
2259
2314
  const superFam = L.rel==='superseded_by'||L.rel==='corrected_by';
2260
2315
  /* fix-round-3 weights: settled alphas roughly halved (both themes) so the
@@ -2262,7 +2317,7 @@ function draw(now, dt){
2262
2317
  const bright=(a.stage==='truth'||b.stage==='truth') ? (dark?.07:.11) : (dark?.035:.06);
2263
2318
  const baseW=.34+useF*.55+heat*.5;
2264
2319
  /* 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);
2320
+ ctx.strokeStyle=rgba(PAL.thread,(bright*(superFam?.45:1)+useF*(dark?.05:.07)+heat*.28)*linkBoot*tfog*focusDim);
2266
2321
  ctx.lineWidth=baseW;
2267
2322
  ctx.setLineDash(superFam?[4,4]:[len*drawP,1e5]);
2268
2323
  ctx.beginPath(); ctx.moveTo(x1,y1); ctx.quadraticCurveTo(cx1,cy1,x2,y2); ctx.stroke();
@@ -2271,7 +2326,7 @@ function draw(now, dt){
2271
2326
  in the same proportion as the settled stroke) */
2272
2327
  const settle=1-prog;
2273
2328
  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);
2329
+ ctx.strokeStyle=rgba(PAL.threadHot,(baseA+.14*heat)*tfog*(superFam?.5:1)*settle*focusDim);
2275
2330
  ctx.lineWidth=baseW+(.18+drawP*.3)*settle;
2276
2331
  ctx.setLineDash(superFam?[4,4]:[len*drawP,1e5]);
2277
2332
  ctx.beginPath(); ctx.moveTo(x1,y1); ctx.quadraticCurveTo(cx1,cy1,x2,y2); ctx.stroke();
@@ -2346,8 +2401,9 @@ function draw(now, dt){
2346
2401
  dark-blue-on-near-black at ~4px read invisible to a vision model);
2347
2402
  the projection factor is known here, and far-stage bubbles (rim depth
2348
2403
  / small viewports) were collapsing to ~1-2px. Every stage stays a
2349
- legible glimmer; ordering untouched. */
2350
- r=Math.max(r,5);
2404
+ legible glimmer; ordering untouched. #474 graph final: floor 1.5 —
2405
+ 0.30× of 5, proportional with the 30% balls. */
2406
+ r=Math.max(r,1.5);
2351
2407
  let remA=1;
2352
2408
  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
2409
  const ex=b._ex||0;
@@ -2447,8 +2503,11 @@ function draw(now, dt){
2447
2503
  ctx.font=(9.5*k).toFixed(1)+'px '+FF;
2448
2504
  try{ ctx.letterSpacing=(1*k).toFixed(1)+'px'; }catch(e){}
2449
2505
  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;
2506
+ /* #474: the label count is the WINDOW count (what is drawn at this T),
2507
+ not the store total — honest with the dateline view. */
2508
+ const cnt=fmt(winCount[i]||0);
2509
+ ctx.fillText(cnt, sx+w+9*k, sy);
2510
+ const bw=w+9*k+ctx.measureText(cnt).width;
2452
2511
  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
2512
  ctx.lineWidth=1;
2454
2513
  const bx=sx-9*k, by=sy-11*k, bxx=bx+bw+9*k, byy=by+15*k, tk=4*k;
@@ -2613,18 +2672,18 @@ function autoTick(now){
2613
2672
  if(T>=NOW_T-.001){
2614
2673
  /* a beat at "now": re-kick to the range start ONLY before the first
2615
2674
  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;
2675
+ if(played){ autoPlay=false; autoNextAt=now+REPLAY_REST_MS; return; }
2676
+ autoEvIdx=0; autoNextAt=now+REPLAY_REST_MS; seek(.001); return;
2618
2677
  }
2619
2678
  while(autoEvIdx<EVENTS.length && EVENTS[autoEvIdx].t<=T+.0001) autoEvIdx++;
2620
2679
  if(autoEvIdx>=EVENTS.length){
2621
2680
  /* ledger exhausted: arrive at today and complete the playback (the
2622
2681
  flags go BEFORE the seek so onTChanged's control swap sees them) */
2623
- autoNextAt=now+3200; played=true; autoPlay=false; seek(WINDOW); return;
2682
+ autoNextAt=now+REPLAY_REST_MS; played=true; autoPlay=false; seek(WINDOW); return;
2624
2683
  }
2625
2684
  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; }
2685
+ if(ev.id && signals.length<8){ heraldTo(ev.id); pendingApply={t:ev.t+.001, at:now+REPLAY_HERALD_MS}; }
2686
+ else { seek(ev.t+.001); autoNextAt=now+REPLAY_TICK_MS; }
2628
2687
  }
2629
2688
  track.addEventListener('pointerdown',e=>{ if(!REPLAY_OK) return; scrubbing=true; manualTake(); track.setPointerCapture(e.pointerId); seek(trackFrac(e)*WINDOW); });
2630
2689
  track.addEventListener('pointermove',e=>{ if(scrubbing) seek(trackFrac(e)*WINDOW); });
@@ -2841,7 +2900,8 @@ cv.addEventListener('pointermove',e=>{
2841
2900
  tip.style.display='block';
2842
2901
  tip.style.left=Math.min(e.clientX+14,window.innerWidth-300)+'px';
2843
2902
  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>';
2903
+ /* #474: the window count at this T — honest with what is drawn */
2904
+ tip.innerHTML='<b>'+esc(d.name)+'</b><span style="color:var(--dim)">'+fmt(winCount[lb]||0)+' in view</span>';
2845
2905
  }else tip.style.display='none';
2846
2906
  dirty=true;
2847
2907
  });
@@ -3114,23 +3174,6 @@ function buildTokNights(used, cap, periodStart){
3114
3174
  }
3115
3175
  }
3116
3176
 
3117
- /* trend = mean of the last 7 nights vs the prior 7 (a DEFINED window — the
3118
- old first-vs-last-over-the-range comparison had no window at all). Titles
3119
- state the window so the arrow is never mysterious. */
3120
- function recallTrend(){
3121
- const flat={cls:'flat',sym:'&#9644;'};
3122
- if(!D||!D.series) return Object.assign(flat,{title:'no snapshot data'});
3123
- const vals=D.series.map(s=>s.metrics&&s.metrics.adoption?s.metrics.adoption.uses_per_showing:null).filter(v=>typeof v==='number');
3124
- const last7=vals.slice(-7), prior7=vals.slice(-14,-7);
3125
- const mean=a=>a.length?a.reduce((x,y)=>x+y,0)/a.length:null;
3126
- const a=mean(prior7), b=mean(last7);
3127
- if(a===null||b===null) return Object.assign(flat,{title:'trend needs 7+ nights of history'});
3128
- const d=b-a;
3129
- if(d>0.005) return {cls:'up',sym:'&#8599;',title:'up — mean of the last 7 nights vs the prior 7 ('+b.toFixed(3)+' vs '+a.toFixed(3)+')'};
3130
- if(d<-0.005) return {cls:'down',sym:'&#8600;',title:'down — mean of the last 7 nights vs the prior 7 ('+b.toFixed(3)+' vs '+a.toFixed(3)+')'};
3131
- return Object.assign(flat,{title:'flat — mean of the last 7 nights vs the prior 7 ('+b.toFixed(3)+' vs '+a.toFixed(3)+')'});
3132
- }
3133
-
3134
3177
  /* ---------- Recall level — the designed U-band (#426 final ruling
3135
3178
  2026-09-13). A continuous red→green→red gradient spans the PROVISIONAL
3136
3179
  zone edges (echoed by /dashboard/field as thresholds.recall_uses — the
@@ -3180,7 +3223,14 @@ function recallGradAt(frac,z,theme){
3180
3223
  }
3181
3224
  let lastRecUses=null; /* setTheme's re-render carries the last value */
3182
3225
  function renderRecBand(ups){
3226
+ /* #476: the marker/tooltip read the WINDOW ratio when the memory_precision
3227
+ block is present; a pre-#476 server (no block) degrades to the headline
3228
+ ratio — today's rendering exactly (the capture card's block-absence
3229
+ guard pattern). A null window value honestly renders "—" below. */
3230
+ const mp=D&&D.memory_precision?D.memory_precision:null;
3231
+ if(mp&&mp.level2) ups=mp.level2.uses_per_showing;
3183
3232
  lastRecUses=ups;
3233
+ renderRecL1(mp);
3184
3234
  const z=F&&F.thresholds?F.thresholds.recall_uses:null;
3185
3235
  const axis=z&&typeof z.axis_max==='number'&&z.axis_max>0?z.axis_max:null;
3186
3236
  const row=el('rec-bandrow'), band=el('rec-band'), dot=el('rec-dot'), word=el('rec-zone');
@@ -3204,7 +3254,7 @@ function renderRecBand(ups){
3204
3254
  if(zk){
3205
3255
  dot.hidden=false;
3206
3256
  dot.style.left=pct(ups)+'%';
3207
- dot.title=ups.toFixed(3)+' uses per showing — last 30 nights';
3257
+ dot.title=ups.toFixed(3)+' uses per showing — last '+(mp&&mp.window_days?mp.window_days:30)+' days';
3208
3258
  word.textContent=RECALL_ZONE_WORD[zk];
3209
3259
  word.className='n rec-word';
3210
3260
  /* the big word's color = the gradient sampled AT the marker — the word
@@ -3220,16 +3270,67 @@ function renderRecBand(ups){
3220
3270
  word.style.color=''; word.style.textShadow='none';
3221
3271
  word.title='no showings yet';
3222
3272
  }
3223
- /* trend arrow — the mock family's 7v7 window, unchanged */
3224
- if(typeof ups==='number'){
3225
- const tr=recallTrend();
3226
- el('rec-trend').className='trend '+tr.cls;
3227
- el('rec-trend').innerHTML=tr.sym;
3228
- el('rec-trend').title=tr.title;
3229
- }else{
3230
- el('rec-trend').className='trend flat'; el('rec-trend').innerHTML='&#9644;';
3231
- el('rec-trend').title='no showings yet';
3273
+ /* No trend arrow (owner 2026-09-20: removed — unclear). Its renderer and
3274
+ the element writes are removed WITH it: a leftover write to a removed
3275
+ element is a boot-killing TypeError whenever the recall band carries
3276
+ real data (bedrock 0.22.2: "0 memories" — boot died before the counter
3277
+ was ever seeded). */
3278
+ }
3279
+
3280
+ /* ---------- Memory Precision — Level 1, the pushed index (#476). A compact
3281
+ second row beside Level 2's U-band: mean similarity of pushed lines to
3282
+ their prompt (a 0..1 bar with a floor tick at the /dashboard/field echo of
3283
+ recall_min_similarity — the server's own recall floor, never a page
3284
+ literal), muted below the floor (the index row reads conservative: its
3285
+ noise lands on every prompt), plus the redundancy share (lines restating
3286
+ the session's standing context at/above the echoed cosine). Direction-only
3287
+ coloring, NO verdict words — the #426 semantics ruling extended.
3288
+ Divergence: the COUNT on the card face, the ≤3 titles hover-only (no
3289
+ memory lists on the main page, owner 2026-09-12 — the search panel is the
3290
+ list surface). Absent block (pre-#476 server) → the whole row hides and
3291
+ the sub keeps the old label — today's rendering exactly. */
3292
+ function renderRecL1(mp){
3293
+ const row=el('rec-l1'), sub=el('rec-sub'), dv=el('rec-div');
3294
+ if(!mp||!mp.level1||!mp.level2){
3295
+ row.hidden=true; dv.hidden=true; dv.title='';
3296
+ if(sub) sub.textContent='uses per showing';
3297
+ return;
3232
3298
  }
3299
+ if(sub) sub.textContent='last '+(mp.window_days||90)+' days';
3300
+ const l1=mp.level1;
3301
+ const floor=(F&&F.thresholds&&typeof F.thresholds.recall_min_similarity==='number')?F.thresholds.recall_min_similarity:null;
3302
+ const ra=(l1.thresholds&&typeof l1.thresholds.redundant_above==='number')?l1.thresholds.redundant_above:null;
3303
+ const mins=(l1.thresholds&&typeof l1.thresholds.divergence_min_shown==='number')?l1.thresholds.divergence_min_shown:5;
3304
+ const sim=(typeof l1.mean_similarity==='number'&&isFinite(l1.mean_similarity))?l1.mean_similarity:null;
3305
+ const red=(typeof l1.redundant_share==='number'&&isFinite(l1.redundant_share))?l1.redundant_share:null;
3306
+ if(sim===null){
3307
+ row.hidden=true; /* nothing measured in the window — the row degrades away */
3308
+ }else{
3309
+ row.hidden=false;
3310
+ row.classList.toggle('below',floor!==null&&sim<floor);
3311
+ /* numbers only on the face — direction carries the meaning, no verdict words */
3312
+ /* #476 owner rename (2026-09-20): the three buckets as named — Relevant /
3313
+ Repeated known / Off-target. Relevant = the mean relevance; Repeated =
3314
+ the redundant share; Off-target = the remainder (pushed lines that were
3315
+ neither relevant nor new). Numbers, no verdict words (#426). */
3316
+ const relPct=Math.round(clamp(sim,0,1)*100);
3317
+ const repPct=red!==null?Math.round(clamp(red,0,1)*100):0;
3318
+ const offPct=Math.max(0,100-relPct-repPct);
3319
+ const dv=mp.divergence, nUsed=(dv&&typeof dv.count==='number')?dv.count:0;
3320
+ el('l1-rel').innerHTML='<b>Relevant: '+relPct+'%</b>';
3321
+ el('l1-rep').textContent='Repeated known: '+repPct+'%';
3322
+ el('l1-off').textContent='Off-target: '+offPct+'%'+(nUsed>0?' ('+nUsed+' never used)':'');
3323
+ row.title='pushed index, last '+(mp.window_days||90)+' days — mean similarity of pushed lines to their prompt (tick at the recall floor '+
3324
+ (floor!==null?floor.toFixed(2):'—')+'; muted below it)'+
3325
+ (ra!==null?'; redundant = share of lines at ≥'+ra.toFixed(2)+' cosine vs the session\'s standing context (lessons + identity)':'')+
3326
+ ' · '+fmt(l1.pushes)+' pushes, '+fmt(l1.lines)+' lines';
3327
+ }
3328
+ /* divergence — the face count is amber; the ≤3 lines ride the native
3329
+ tooltip ONLY (text via .title, never face content) */
3330
+ /* #476 owner bundling (2026-09-20): the never-used count rides inside
3331
+ Off-target; the separate face element stays hidden (titles on hover) */
3332
+ const dEl=el('rec-div');
3333
+ if(dEl){ dEl.hidden=true; }
3233
3334
  }
3234
3335
 
3235
3336
  let MODEL_LABEL='';
@@ -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