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/CHANGELOG.md +148 -0
- package/README.md +61 -10
- package/bin/atlas-digest.mjs +14 -5
- package/bin/atlas-retro.mjs +5 -3
- package/client.js +959 -90
- package/docs/icon-180.png +0 -0
- package/index.js +356 -20
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +9 -3
- package/test/run.mjs +336 -10
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 <
|
|
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
|
|
359
|
-
// renderer the panel does
|
|
360
|
-
//
|
|
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
|
-
|
|
384
|
-
|
|
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 -
|
|
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()
|