dsh-error-atlas 0.11.2 → 0.11.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/test/run.mjs CHANGED
@@ -31,7 +31,7 @@ function eq(name, actual, expected) {
31
31
  }
32
32
 
33
33
  // ── harness ────────────────────────────────────────────────────────────────
34
- async function bootHost(home) {
34
+ async function bootHost(home, config = {}) {
35
35
  process.env.DSH_HOME = home
36
36
  const mod = await import('file://' + HOST.replace(/\\/g, '/') + '?t=' + Math.random())
37
37
  const handlers = new Map()
@@ -75,7 +75,7 @@ async function bootHost(home) {
75
75
  fn()
76
76
  },
77
77
  },
78
- {},
78
+ config,
79
79
  )
80
80
  const fire = (event, ...args) => {
81
81
  for (const fn of handlers.get(event) || []) fn(...args)
@@ -198,7 +198,7 @@ async function retroSection() {
198
198
  const prompt = host.sections.find((entry) => entry.name === 'error-atlas:retrospective')
199
199
  eq('the prompt section keeps its order slot', prompt && prompt.order, 300)
200
200
  const promptText = prompt && typeof prompt.text === 'function' ? prompt.text({}) : prompt && prompt.text
201
- check('the prompt section is short', promptText && promptText.length < 400, 'length=' + (promptText && promptText.length))
201
+ check('the prompt section is short', promptText && promptText.length < 500, 'length=' + (promptText && promptText.length))
202
202
  eq('the atlas_retro tool is registered', host.tools.filter((tool) => tool.name === 'atlas_retro').length, 1)
203
203
  }
204
204
 
@@ -355,12 +355,16 @@ async function clientSection() {
355
355
  section('client')
356
356
  let React
357
357
  let renderToStaticMarkup
358
- // React comes from the running DSH profile, so the client checks exercise the same
359
- // renderer the panel does. The lookup is derived, never absolute: DSH_HOME wins, then
360
- // the conventional home, and a missing profile only skips this section.
358
+ // React comes from the running DSH profile when there is one, so the client checks
359
+ // exercise the same renderer the panel does; a checkout with its own devDependencies
360
+ // (CI) falls back to the package's node_modules. The lookup is derived, never absolute:
361
+ // DSH_HOME wins, then the conventional home, then the two local node_modules roots,
362
+ // and a machine without any of them only skips this section.
361
363
  const homes = []
362
364
  if (process.env.DSH_HOME) homes.push(join(process.env.DSH_HOME, 'profiles', 'node_modules') + '/')
363
365
  homes.push(join(homedir(), '.dsh', 'profiles', 'node_modules') + '/')
366
+ homes.push(join(process.cwd(), 'node_modules') + '/')
367
+ homes.push(join(import.meta.dirname, '..', 'node_modules') + '/')
364
368
  for (const dir of homes) {
365
369
  if (!existsSync(dir)) continue
366
370
  try {
@@ -379,9 +383,11 @@ async function clientSection() {
379
383
  }
380
384
 
381
385
  const RAW = readFileSync(CLIENT, 'utf8')
386
+ // Windows checkouts may carry CRLF (git core.autocrlf), so the seam must not depend on
387
+ // a literal line ending; match the slot list itself instead.
382
388
  const SRC = RAW.replace(
383
- " return {\n inject: ['slots'],",
384
- " return {\n __debug: { createRenderer, isHubNode, withDev },\n inject: ['slots'],",
389
+ /( *)return \{\r?\n( *)inject: \['slots'\],/,
390
+ '$1return {\n$2__debug: { createRenderer, isHubNode, withDev, cloudRadiusFor },\n$2inject: [\'slots\'],',
385
391
  ).replace('useState(() => withDev(null))', 'useState(() => withDev(window.__ATLAS_SEED__))')
386
392
  check('the test seam is present', SRC.includes('__debug'))
387
393
 
@@ -521,6 +527,12 @@ async function clientSection() {
521
527
  check('a clean tag uses the calm bar', html.includes('dshea-board-fill is-clean'))
522
528
  check('the canvas is mounted', html.includes('dshea-canvas'))
523
529
  check('the label switch renders', html.includes('dshea-caption'))
530
+ check('the size dial renders', html.includes('dshea-size') && html.includes('大小'))
531
+ check('the glow dial renders', html.includes('dshea-halo') && html.includes('光晕'))
532
+ check('the pulse dial renders', html.includes('dshea-flow') && html.includes('光点'))
533
+ check('the spread dial renders', html.includes('dshea-spread') && html.includes('间距'))
534
+ check('the dials live in the look popover', html.includes('dshea-style') && html.includes('外观'))
535
+ eq('the look popover carries all five dials', (html.match(/type="range"/g) || []).length, 5)
524
536
  check('the label switch is not the label overlay', !html.includes('dshea-btn dshea-labels'))
525
537
  check('the header draws no fill', /\.dshea-header \{[^}]*background: transparent;/.test(RAW))
526
538
  check('the paused hint is not shown by default', !html.includes('已暂停'))
@@ -551,6 +563,110 @@ async function clientSection() {
551
563
  renderer.setLabels(true)
552
564
  check('the label switch brings the captions back', labelElement.style.display === '' && state.showLabels === true)
553
565
  check('a program is a hub', module.__debug.isHubNode('project') === true)
566
+
567
+ // Colour filters: switching a kind off zeroes its alpha, switching a wire role off
568
+ // zeroes that role's link alpha, and clearing the filter restores both.
569
+ eq('both colour filters render', (html.match(/class="dshea-colors"/g) || []).length, 2)
570
+ check('the colour filters carry chips', (html.match(/class="dshea-chip/g) || []).length >= 10)
571
+ const exitIdx = state.nodes.findIndex((node) => node.kind === 'exit')
572
+ const retroIdx = state.nodes.findIndex((node) => node.kind === 'retro')
573
+ check('the fixture carries an exit failure', exitIdx >= 0, String(exitIdx))
574
+ renderer.setFilters(['exit'], [])
575
+ windowStub.__raf(1032)
576
+ check('a filtered-out kind goes dark', state.alpha[exitIdx] === 0, String(state.alpha[exitIdx]))
577
+ check('other kinds stay lit', state.alpha[retroIdx] > 0.1, String(state.alpha[retroIdx]))
578
+ renderer.setFilters([], [])
579
+ windowStub.__raf(1048)
580
+ check('clearing the kind filter restores the node', state.alpha[exitIdx] > 0.5, String(state.alpha[exitIdx]))
581
+ const lessonLink = state.linkRoles.findIndex((role) => role === 'lesson' || role === 'stale')
582
+ check('a lesson wire is labelled', lessonLink >= 0, String(lessonLink))
583
+ const lessonAlpha = state.linkAlpha[lessonLink * 2]
584
+ renderer.setFilters([], ['lesson', 'stale'])
585
+ check('a filtered-out wire goes dark', state.linkAlpha[lessonLink * 2] === 0, String(state.linkAlpha[lessonLink * 2]))
586
+ renderer.setFilters([], [])
587
+ check('clearing the wire filter restores it', state.linkAlpha[lessonLink * 2] === lessonAlpha)
588
+
589
+ // A program is a satellite, not another node in the ball: it has to settle outside
590
+ // the error cloud while its links keep it tethered to what it produced.
591
+ let tick = 2000
592
+ for (let f = 0; f < 120; f += 1) {
593
+ tick += 16.7
594
+ if (typeof windowStub.__raf !== 'function') break
595
+ const frame = windowStub.__raf
596
+ windowStub.__raf = null
597
+ frame(tick)
598
+ }
599
+ const radiusOf = (id) => {
600
+ const i = index.get(id)
601
+ return Math.hypot(state.pos[i * 3], state.pos[i * 3 + 1], state.pos[i * 3 + 2])
602
+ }
603
+ const projectRadius = radiusOf('p:app')
604
+ const maxErrorRadius = Math.max(radiusOf('e:e1'), radiusOf('e:e2'))
605
+ check('a program orbits outside the cloud', projectRadius > 45, projectRadius.toFixed(1))
606
+ check('a program clears the errors it came from', projectRadius > maxErrorRadius + 4, projectRadius.toFixed(1) + ' vs ' + maxErrorRadius.toFixed(1))
607
+ check('the tether survives the orbit', state.links.some((link) => link.a === index.get('e:e1') && link.b === index.get('p:app')))
608
+
609
+ // The stage grows with the atlas: 2400 failures need a wider cloud, or the islands
610
+ // collapse back into one ball.
611
+ check('a small atlas keeps the old stage', module.__debug.cloudRadiusFor(240) === 34, String(module.__debug.cloudRadiusFor(240)))
612
+ check('a large atlas widens the stage', module.__debug.cloudRadiusFor(2400) > 3 * 34, module.__debug.cloudRadiusFor(2400).toFixed(1))
613
+
614
+ // Session windows are islands: each failure must settle nearest its own window, not the
615
+ // window of another conversation that happened to suffer the same tool.
616
+ const islandPayload = {
617
+ generatedAt: now,
618
+ stats: { total: 4, distinct: 4, shown: 7, matched: 4, truncated: 0, muted: 0, sessions: 2, tools: 2, projects: 0, retro: 0, stale: 0, dev: 0, byKind: {} },
619
+ prefs: { minCount: 1, ignore: [], prompt: true, inject: true },
620
+ trend: payload.trend,
621
+ nodes: [
622
+ { id: 'root:dsh', kind: 'root', label: 'DSH', count: 4 },
623
+ { id: 'sessionhub:sess-a', kind: 'sessionhub', label: 'SESSION aaaa', count: 2 },
624
+ { id: 'sessionhub:sess-b', kind: 'sessionhub', label: 'SESSION bbbb', count: 2 },
625
+ { id: 'e:a1', kind: 'tool', label: 'edit', name: 'edit', message: 'a1', count: 1, sessions: ['sess-a'] },
626
+ { id: 'e:a2', kind: 'exit', label: 'pwsh', name: 'pwsh', message: 'a2', count: 1, sessions: ['sess-a'] },
627
+ { id: 'e:b1', kind: 'tool', label: 'edit', name: 'edit', message: 'b1', count: 1, sessions: ['sess-b'] },
628
+ { id: 'e:b2', kind: 'exit', label: 'pwsh', name: 'pwsh', message: 'b2', count: 1, sessions: ['sess-b'] },
629
+ ],
630
+ links: [
631
+ { s: 'e:a1', t: 'root:dsh' },
632
+ { s: 'e:a2', t: 'root:dsh' },
633
+ { s: 'e:b1', t: 'root:dsh' },
634
+ { s: 'e:b2', t: 'root:dsh' },
635
+ { s: 'e:a1', t: 'sessionhub:sess-a' },
636
+ { s: 'e:a2', t: 'sessionhub:sess-a' },
637
+ { s: 'e:b1', t: 'sessionhub:sess-b' },
638
+ { s: 'e:b2', t: 'sessionhub:sess-b' },
639
+ { s: 'sessionhub:sess-a', t: 'root:dsh' },
640
+ { s: 'sessionhub:sess-b', t: 'root:dsh' },
641
+ ],
642
+ lessons: null,
643
+ retros: [],
644
+ recent: [],
645
+ }
646
+ renderer.setData(islandPayload)
647
+ for (let f = 0; f < 150; f += 1) {
648
+ tick += 16.7
649
+ if (typeof windowStub.__raf === 'function') {
650
+ const frame = windowStub.__raf
651
+ windowStub.__raf = null
652
+ frame(tick)
653
+ }
654
+ }
655
+ const at = (id) => {
656
+ const i = new Map(state.nodes.map((node, k) => [node.id, k])).get(id)
657
+ return [state.pos[i * 3], state.pos[i * 3 + 1], state.pos[i * 3 + 2]]
658
+ }
659
+ const near = (p, q) => Math.hypot(p[0] - q[0], p[1] - q[1], p[2] - q[2])
660
+ const hubA = at('sessionhub:sess-a')
661
+ const hubB = at('sessionhub:sess-b')
662
+ const ownA = Math.max(near(at('e:a1'), hubA), near(at('e:a2'), hubA))
663
+ const crossA = Math.min(near(at('e:a1'), hubB), near(at('e:a2'), hubB))
664
+ const ownB = Math.max(near(at('e:b1'), hubB), near(at('e:b2'), hubB))
665
+ const crossB = Math.min(near(at('e:b1'), hubA), near(at('e:b2'), hubA))
666
+ check('window a holds its own failures', ownA < crossA, ownA.toFixed(1) + ' vs ' + crossA.toFixed(1))
667
+ check('window b holds its own failures', ownB < crossB, ownB.toFixed(1) + ' vs ' + crossB.toFixed(1))
668
+ check('the windows sit on different islands', near(hubA, hubB) > 8, near(hubA, hubB).toFixed(1))
669
+
554
670
  renderer.dispose()
555
671
 
556
672
  // The dictionary follows the DSH language setting: re-apply with an English locale and
@@ -638,7 +754,7 @@ async function lessonSection() {
638
754
  retros: [
639
755
  {
640
756
  id: 'r1',
641
- at: now - hour,
757
+ at: now - 60 * 60 * 1000,
642
758
  auto: false,
643
759
  title: 'fix boom',
644
760
  done: 'edited src/x.ts',
@@ -707,6 +823,107 @@ async function lessonSection() {
707
823
  }
708
824
 
709
825
 
826
+ // ── impact ─────────────────────────────────────────────────────────────────
827
+ // The number the plugin exists for: of the lessons that can be judged at all, how many
828
+ // had their signatures come back anyway.
829
+ async function impactSection() {
830
+ section('impact')
831
+ const home = tempHome('impact')
832
+ const now = Date.now()
833
+ const hour = 60 * 60 * 1000
834
+ const retro = (id, at, errorIds, extra) =>
835
+ Object.assign(
836
+ {
837
+ id,
838
+ at,
839
+ auto: false,
840
+ title: 'task ' + id,
841
+ done: 'changed',
842
+ cause: 'c',
843
+ fix: 'f',
844
+ lesson: 'lesson ' + id,
845
+ tags: ['edit'],
846
+ tools: ['edit'],
847
+ files: ['C:/work/proj-a/src/a.ts'],
848
+ cwd: 'C:/work',
849
+ errors: [],
850
+ errorIds,
851
+ },
852
+ extra || {},
853
+ )
854
+ writeFileSync(
855
+ join(home, 'error-atlas', 'records.json'),
856
+ JSON.stringify({
857
+ version: 2,
858
+ updatedAt: now,
859
+ records: [
860
+ {
861
+ key: 'tool\u0000edit\u0000boom',
862
+ id: 'e1',
863
+ kind: 'tool',
864
+ name: 'edit',
865
+ message: 'boom',
866
+ count: 2,
867
+ first: now - 5 * hour,
868
+ last: now - 10 * 60 * 1000,
869
+ sessions: ['s'],
870
+ },
871
+ {
872
+ key: 'tool\u0000pwsh\u0000sunless',
873
+ id: 'e2',
874
+ kind: 'tool',
875
+ name: 'pwsh',
876
+ message: 'no lesson here',
877
+ count: 1,
878
+ first: now - 4 * hour,
879
+ last: now - 4 * hour,
880
+ sessions: ['s'],
881
+ },
882
+ ],
883
+ log: [
884
+ { at: now - 10 * 60 * 1000, kind: 'tool', name: 'edit', message: 'boom', sessionId: 's' },
885
+ { at: now - 4 * hour, kind: 'tool', name: 'pwsh', message: 'no lesson here', sessionId: 's' },
886
+ { at: now - 40 * 24 * hour, kind: 'tool', name: 'edit', message: 'ancient', sessionId: 's' },
887
+ ],
888
+ retros: [
889
+ retro('r1', now - 3 * hour, ['e1']),
890
+ retro('r2', now - 2 * hour, []),
891
+ retro('r3', now - hour, [], { auto: true }),
892
+ ],
893
+ }),
894
+ )
895
+
896
+ const host = await bootHost(home)
897
+ const tool = host.tools.find((item) => item.name === 'atlas_impact')
898
+ check('the impact tool is registered', Boolean(tool))
899
+ if (!tool) return
900
+
901
+ const impact = await tool.execute({})
902
+ eq('every retrospective is counted', impact.lessons, 3)
903
+ eq('model retrospectives are separated', impact.model, 2)
904
+ eq('auto skeletons are separated', impact.auto, 1)
905
+ eq('only lessons that name a live failure are judgeable', impact.judgeable, 1)
906
+ eq('a lesson whose failure came back counts as failed', impact.failed, 1)
907
+ eq('nothing was held against the only judged lesson', impact.held, 0)
908
+ eq('the failure rate is a percentage', impact.rate, 100)
909
+ eq('the store size is reported', impact.distinct, 2)
910
+ eq('coverage counts signatures with a lesson', impact.withLesson, 1)
911
+ eq('coverage is a percentage', impact.coverage, 50)
912
+ eq('the failure log is counted', impact.hits, 3)
913
+ eq('the window keeps the old failure out', impact.hitsInWindow, 2)
914
+ check('failures after the first lesson are counted', impact.afterFirstLesson >= 1, String(impact.afterFirstLesson))
915
+ eq('the default window is 30 days', impact.days, 30)
916
+ eq('a shorter window is honoured', (await tool.execute({ days: 1 })).days, 1)
917
+ eq('a silly window is clamped', (await tool.execute({ days: 9999 })).days, 365)
918
+
919
+ const text = tool.output.render({}, impact)[0].text
920
+ check('the rendered impact reads like a report', text.includes('judged lessons recurred'), text.slice(0, 120))
921
+ check('the rendered impact reports coverage', text.includes('carry a lesson'))
922
+
923
+ const payload = (await host.call('/error-atlas/data')).json
924
+ eq('the panel payload carries the same numbers', payload.impact ? payload.impact.judgeable : -1, 1)
925
+ }
926
+
710
927
  // ── board ──────────────────────────────────────────────────────────────────
711
928
  // The effectiveness leaderboard: a lesson library is only useful if the reader can see
712
929
  // which kinds of lesson keep failing without opening every entry.
@@ -808,8 +1025,117 @@ async function manifestSection() {
808
1025
  check('the client carries a build stamp', /const BUILD = 'V\d+'/.test(client))
809
1026
  }
810
1027
 
1028
+ // ── caps ───────────────────────────────────────────────────────────────────
1029
+ async function capSection() {
1030
+ section('caps')
1031
+ const home = tempHome('caps')
1032
+ const host = await bootHost(home, { maxNodes: 3 })
1033
+ const agent = { id: 'sess-caps' }
1034
+ for (let i = 0; i < 12; i += 1) toolFailure(host, agent, 'edit', 'C:/work/proj/f' + i + '.js', 'failure ' + i)
1035
+ const small = await host.call('/error-atlas/data')
1036
+ eq('a low cap truncates', small.json.stats.truncated, 9)
1037
+ eq('a low cap keeps the requested nodes', small.json.nodes.filter((node) => node.kind === 'tool' || node.kind === 'exit').length, 3)
1038
+
1039
+ // The default cap has to hold a real atlas: 260 failures used to be cut off at 240.
1040
+ const bigHome = tempHome('caps-big')
1041
+ const big = await bootHost(bigHome)
1042
+ const bigAgent = { id: 'sess-big' }
1043
+ for (let i = 0; i < 260; i += 1) toolFailure(big, bigAgent, 'edit', 'C:/work/proj/f' + i + '.js', 'failure ' + i)
1044
+ const full = await big.call('/error-atlas/data')
1045
+ eq('the default cap holds 260 failures', full.json.nodes.filter((node) => node.kind === 'tool' || node.kind === 'exit').length, 260)
1046
+ eq('the default cap hides nothing', full.json.stats.truncated, 0)
1047
+ }
1048
+
1049
+ async function noiseSection() {
1050
+ section('noise')
1051
+ const home = tempHome('noise')
1052
+ const host = await bootHost(home)
1053
+ const agent = { id: 'sess-noise' }
1054
+
1055
+ // Two different command lines that both run the same script: one signature, not two.
1056
+ host.fire('tools/result', { name: 'pwsh', arguments: { command: 'cd D:/work; & "C:/Program Files/nodejs/node.exe" build.mjs' }, agent }, {
1057
+ isError: false,
1058
+ content: [{ type: 'text', text: 'boom\n[exit code: 1]' }],
1059
+ })
1060
+ host.fire('tools/result', { name: 'pwsh', arguments: { command: 'cd D:/other; node build.mjs' }, agent }, {
1061
+ isError: false,
1062
+ content: [{ type: 'text', text: 'boom\n[exit code: 1]' }],
1063
+ })
1064
+ const first = await host.call('/error-atlas/data')
1065
+ const exits = first.json.nodes.filter((node) => node.kind === 'exit')
1066
+ eq('the shell fingerprint collapses command variants', exits.length, 1)
1067
+ check('the fingerprint names the script', /node build\.mjs/.test(exits[0].message), exits[0].message)
1068
+ check('stderr rides along as the detail', /boom/.test(exits[0].message), exits[0].message)
1069
+ eq('a described exit is not bare noise', first.json.stats.noise, 0)
1070
+
1071
+ // Five identical bare exits (no stderr) are churn: hidden, counted, and swtichable.
1072
+ for (let i = 0; i < 6; i += 1) {
1073
+ host.fire('tools/result', { name: 'pwsh', arguments: { command: 'cd D:/work; node silent.mjs' }, agent }, {
1074
+ isError: false,
1075
+ content: [{ type: 'text', text: '[exit code: 1]' }],
1076
+ })
1077
+ }
1078
+ const hidden = await host.call('/error-atlas/data')
1079
+ eq('bare repeats leave the graph', hidden.json.nodes.filter((node) => /silent\.mjs/.test(node.message)).length, 0)
1080
+ check('and they are counted instead', hidden.json.stats.noise >= 1, 'noise=' + hidden.json.stats.noise)
1081
+
1082
+ const shown = await host.call('/error-atlas/prefs', 'POST', { showNoise: true })
1083
+ eq('the switch is stored', shown.json.prefs.showNoise, true)
1084
+ const revealed = await host.call('/error-atlas/data')
1085
+ eq('asking for noise brings it back', revealed.json.nodes.filter((node) => /silent\.mjs/.test(node.message)).length, 1)
1086
+
1087
+ // Essence is what gets injected, and a retro that arrived without one gets one derived.
1088
+ const withEssence = host.tools.find((tool) => tool.name === 'atlas_retro')
1089
+ await withEssence.execute({
1090
+ title: 'essence test',
1091
+ lesson: 'An essay-length lesson that should never reach the prompt verbatim because it is far too long to be useful in context.',
1092
+ essence: 'if a shell command fails twice -> command churn -> change the command, not the shell',
1093
+ }, { agent })
1094
+ await host.call('/error-atlas/retro', 'POST', {
1095
+ title: 'derived essence',
1096
+ lesson: '数据可视化里标签和相机距离是两个自由度',
1097
+ })
1098
+ const brief = host.contexts.find((entry) => entry.name === 'error-atlas:lessons')
1099
+ const text = brief.text({})
1100
+ check('the briefing uses the essence', /change the command, not the shell/.test(text), text.slice(0, 200))
1101
+ check('and not the essay', !/essay-length lesson/.test(text), 'leaked the essay')
1102
+ check('a missing essence is derived as a rule', /规则:/.test(text), text.slice(0, 300))
1103
+ check('the briefing stays under three lessons', (text.match(/^- /gm) || []).length <= 3, text)
1104
+
1105
+ // Ambient churn must not invalidate every lesson that ran beside it: a signature many
1106
+ // lessons point at is the environment, not the mistake any one of them was about.
1107
+ const ambientHome = tempHome('ambient')
1108
+ const now = Date.now()
1109
+ writeFileSync(
1110
+ join(ambientHome, 'error-atlas', 'records.json'),
1111
+ JSON.stringify({
1112
+ version: 3,
1113
+ updatedAt: now,
1114
+ records: [
1115
+ { key: 'k1', id: 'e1', kind: 'pwsh', name: 'git', message: 'git -c', count: 40, first: now - 3 * 60 * 60 * 1000, last: now + 30 * 1000, sessions: ['s'], turn: 1, step: 1 },
1116
+ ],
1117
+ log: [{ at: now + 30 * 1000, kind: 'pwsh', name: 'git', message: 'git -c', sessionId: 's' }],
1118
+ retros: Array.from({ length: 5 }, (_, index) => ({
1119
+ id: 'r' + (index + 1),
1120
+ at: now - 60 * 60 * 1000,
1121
+ auto: false,
1122
+ title: 'lesson ' + (index + 1),
1123
+ lesson: 'do the thing differently',
1124
+ errorIds: ['e1'],
1125
+ tags: ['git'],
1126
+ tools: ['pwsh'],
1127
+ files: [],
1128
+ })),
1129
+ }),
1130
+ )
1131
+ const ambientHost = await bootHost(ambientHome)
1132
+ const ambientTool = ambientHost.tools.find((item) => item.name === 'atlas_lessons')
1133
+ const ambientLessons = await ambientTool.execute({})
1134
+ eq('an ambient signature does not mark lessons stale', ambientLessons.lessons.filter((item) => item.stale > 0).length, 0)
1135
+ }
1136
+
811
1137
  // ── run ────────────────────────────────────────────────────────────────────
812
- const sections = [storeSection, retroSection, prefsSection, trendSection, projectSection, exportSection, lessonSection, boardSection, clientSection, manifestSection]
1138
+ const sections = [storeSection, retroSection, prefsSection, trendSection, projectSection, exportSection, lessonSection, noiseSection, impactSection, boardSection, clientSection, capSection, manifestSection]
813
1139
  for (const run of sections) {
814
1140
  try {
815
1141
  await run()