fix: retain historical events in the story spiral

Render every selected review event, including events without current story membership, and label current grouping without inventing historical assignments. Keep history selection inside the central canvas heading, use shared camera controls, and fit the default scene within the viewport.

Validation: 46 focused tests, synthetic and read-only portable-data Electron checks, Node/Agent/Web typechecks, targeted ESLint, and diff checks passed. All 21 reported events remain represented at wide and narrow widths; no live data writes or model calls.

Release note: Historical story spirals now retain unassigned events. Review history sits inside the canvas column, and camera controls have consistent spacing and framing.
This commit is contained in:
mesalogo
2026-10-04 21:09:32 +08:00
parent 49fd0d3054
commit b9186a616c
15 changed files with 262 additions and 44 deletions
@@ -61,6 +61,77 @@ app
await settle()
}
const reports = []
if (process.env.GOODBUDDY_SUPERVISOR_BACKUP) {
const resultId = process.env.GOODBUDDY_SUPERVISOR_RESULT
assert(resultId, 'Specify the historical result to measure')
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?spiral=1&portable=1')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.querySelector("#supervisor-tab-graph").click()')
await js(`(() => { const e = document.querySelector('.supervisor-workspace__result-navigation select'); e.value = ${JSON.stringify(resultId)}; e.dispatchEvent(new Event('change', {bubbles:true})); })()`)
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false" && document.querySelectorAll(".supervisor-workspace__list-panel > button").length === 21')
await js('[...document.querySelectorAll("button")].find(b => b.textContent === "时间螺旋").click()')
await wait('JSON.parse(document.documentElement.dataset.helix || "null")?.dots.length === 21')
await js('document.fonts.ready')
await settle()
const geometry = await js('JSON.parse(document.documentElement.dataset.helix)')
const expected = await js(`fetch('/portable-review.json').then(r=>r.json()).then(data=>data.graphs[${JSON.stringify(resultId)}].events.map(e=>e.id))`)
assert.deepEqual([...geometry.events].sort(), [...expected].sort())
assert.deepEqual([...geometry.dots].sort(), [...expected].sort(), 'Real event clusters on the helix, not just empty staves')
assert.equal(geometry.points.length, 1401)
assert(geometry.clusters > 1 && geometry.clusters <= 21, 'Events occupy multiple real time positions after layout')
const radii = geometry.points.map(point => point[0])
assert(Math.max(...radii) - Math.min(...radii) > 40)
for (const [width, theme] of [[1440, 'light'], [390, 'dark']]) {
win.setContentSize(width, 1000)
await js(`document.documentElement.dataset.theme = '${theme}'`)
await settle()
const boxes = await js(`(() => {
const box = e => { const r=e.getBoundingClientRect(); return {left:r.left,right:r.right,top:r.top,bottom:r.bottom,width:r.width,height:r.height}; };
const column=document.querySelector('.supervisor-workspace__graph-canvas');
const selector=column.querySelector('.supervisor-workspace__result-navigation select');
const toolbar=document.querySelector('.story-graph-3d__toolbar');
const views=toolbar.querySelector('.story-graph-3d__views');
const segment=views.querySelector('.segmented-control');
const buttons=[...segment.querySelectorAll('button')].map(box);
return {width:innerWidth,pageWidth:document.documentElement.scrollWidth,column:box(column),selector:box(selector),
heading:box(column.querySelector('.supervisor-workspace__canvas-heading')),toolbar:box(toolbar),views:box(views),
canvas:box(document.querySelector('.story-graph-3d__gl')),gaps:buttons.slice(1).map((b,i)=>b.left-buttons[i].right),
segmentPadding:getComputedStyle(segment).paddingTop,controlGap:getComputedStyle(views).gap,
events:document.querySelectorAll('.supervisor-workspace__list-panel > button').length,
bounds:JSON.parse(document.documentElement.dataset.sceneBounds),
scene:JSON.parse(document.documentElement.dataset.helix).dots.length,
clusters:JSON.parse(document.documentElement.dataset.helix).clusters};
})()`)
assert(boxes.selector.left >= boxes.column.left + 16 && boxes.selector.right <= boxes.column.right - 16, 'Selector inside central-column padding')
assert(boxes.selector.top >= boxes.column.top && boxes.selector.bottom <= boxes.heading.bottom, 'History inside heading, not above three columns')
assert.equal(Math.round(boxes.toolbar.top - boxes.heading.bottom), 16, 'Toolbar top inset')
assert(boxes.gaps.every(gap => gap >= 2), 'Shared camera segment gaps')
assert.equal(boxes.segmentPadding, '3px')
assert.equal(boxes.controlGap, '8px')
assert(boxes.views.right <= boxes.column.right - 16 && boxes.pageWidth <= width, 'Controls fit without overflow')
assert(boxes.canvas.width > 0 && boxes.canvas.height > 0)
assert.equal(boxes.events, 21); assert.equal(boxes.scene, 21)
assert(Object.values(boxes.bounds).every(value => Math.abs(value) < 1), 'All root scene vertices fit within the camera viewport')
reports.push({ theme, ...boxes })
if (width === 1440 && !process.env.GOODBUDDY_SUPERVISOR_NO_SCREENSHOT) await writeFile(join(artifacts, 'portable-spiral.png'), (await win.webContents.capturePage()).toPNG())
}
await js('[...document.querySelectorAll(".story-graph-3d__picker")].find(b=>b.textContent.includes("事件")).click()')
await wait('document.querySelectorAll(".story-graph-3d__menu [role=menuitem]").length === 21')
await js('document.querySelector(".story-graph-3d__menu [role=menuitem]").click()')
await wait('document.querySelector(".supervisor-workspace__list-panel > button")?.getAttribute("aria-pressed") === "true"')
// The pre-assignment case uses the same real events with an explicitly empty membership response.
await js(`window.goodbuddy.supervision.stories = async () => ({stories:[],experiences:[],unassigned:21,canUndo:false}); document.querySelector('.supervisor-workspace__canvas-heading > .icon-button').click()`)
await wait('JSON.parse(document.documentElement.dataset.helix || "null")?.dots.length === 21 && document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.querySelector(".story-graph-3d__picker").click()')
await wait('document.querySelectorAll(".story-graph-3d__menu [role=menuitem]").length === 1')
await js('document.querySelector(".story-graph-3d__menu [role=menuitem]").click()')
await wait('document.querySelector(".story-graph-3d__picker")?.textContent.includes("未归属事件")')
assert.equal(await js('JSON.parse(document.documentElement.dataset.helix).dots.length'), 21)
assert.deepEqual(errors, [])
await writeFile(join(artifacts, 'portable-measurements.json'), JSON.stringify({ resultId, reports, clusters: geometry.clusters, radiusRange: [Math.min(...radii), Math.max(...radii)], errors }, null, 2))
console.log(JSON.stringify({ resultId, reports, clusters: geometry.clusters, radiusRange: [Math.min(...radii), Math.max(...radii)], emptyMembershipEvents: 21, errors }))
win.destroy(); app.quit(); return
}
if (process.env.GOODBUDDY_SUPERVISOR_SPIRAL) {
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?spiral=1&recap=1')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')