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"')
+43 -5
View File
@@ -1,10 +1,11 @@
// Simulated visual fixture only. No preload, user database, or model is used.
// Simulated fixture, or read-only production queries from an explicitly supplied local backup. No models.
import { createRoot } from 'react-dom/client'
import { HeartbeatCenter } from '../../src/renderer/src/HeartbeatCenter'
import { SupervisionCard } from '../../src/renderer/src/RightAssistantSidebar'
import { PageShell } from '../../src/renderer/src/WorkspacePrimitives'
import { UiLocaleProvider } from '../../src/renderer/src/i18n/UiLocaleProvider'
import type { SupervisionGraphView } from '../../src/shared/supervision-contracts'
import type { AssistantProject } from '../../src/shared/assistant-contracts'
import { applicationSettingsSchema, defaultLocalToolEnvironmentSettings } from '../../src/shared/application-settings-contracts'
import '../../src/renderer/src/styles.css'
import '@fontsource-variable/noto-sans-sc/wght.css'
@@ -16,8 +17,15 @@ const params = new URLSearchParams(location.search)
if (params.has('spiral')) {
const THREE = await import('three')
const add = THREE.Group.prototype.add
const clear = THREE.Group.prototype.clear
let points: number[][] = []
const events = new Set<string>()
const dots = new Set<string>()
let clusters = 0
THREE.Group.prototype.clear = function () {
events.clear(); dots.clear(); clusters = 0
return clear.call(this)
}
THREE.Group.prototype.add = function (...objects) {
for (const object of objects) {
const path = ((object as import('three').Mesh).geometry as import('three').TubeGeometry)?.parameters?.path as import('three').CatmullRomCurve3 | undefined
@@ -26,10 +34,29 @@ if (params.has('spiral')) {
events.clear()
}
for (const event of object.userData.node?.events ?? []) events.add(event.id)
if (object.userData.cluster) {
clusters++
for (const event of object.userData.cluster.events) dots.add(event.id)
}
}
document.documentElement.dataset.helix = JSON.stringify({ points, events: [...events] })
document.documentElement.dataset.helix = JSON.stringify({ points, events: [...events], dots: [...dots], clusters })
return add.apply(this, objects)
}
if (params.has('portable')) {
THREE.Scene.prototype.onAfterRender = function (_renderer, scene, camera) {
const point = new THREE.Vector3(), bounds = { left: 1, right: -1, top: -1, bottom: 1 }
scene.traverse(object => {
const positions = (object as import('three').Mesh).geometry?.getAttribute('position')
if (!positions) return
for (let i = 0; i < positions.count; i++) {
point.fromBufferAttribute(positions, i).applyMatrix4(object.matrixWorld).project(camera)
bounds.left = Math.min(bounds.left, point.x); bounds.right = Math.max(bounds.right, point.x)
bounds.top = Math.max(bounds.top, point.y); bounds.bottom = Math.min(bounds.bottom, point.y)
}
})
document.documentElement.dataset.sceneBounds = JSON.stringify(bounds)
}
}
}
const stories = [
{
@@ -256,6 +283,17 @@ Object.defineProperty(window, 'goodbuddy', {
}
}
})
let portableProjects: AssistantProject[] = []
if (params.has('portable')) {
const data = await (await fetch('/portable-review.json')).json()
portableProjects = data.projects
Object.assign(window.goodbuddy.supervision, {
overview: async () => data.results,
graph: async ({ resultId }: { resultId: string }) => data.graphs[resultId],
stories: async ({ scope }: { scope: unknown }) => data.stories[JSON.stringify(scope)],
run: async () => { throw new Error('Read-only acceptance must not run reviews') }
})
}
const noop = async () => {}
createRoot(document.getElementById('root')!).render(
<UiLocaleProvider initialPreference="zh-CN">
@@ -277,8 +315,8 @@ createRoot(document.getElementById('root')!).render(
gap: 'var(--space-3)'
}}
>
<span>SIMULATED FIXTURE / 单故事 API / {story.title}</span>
{stories.map((item, index) => (
<span>{params.has('portable') ? 'READ-ONLY PORTABLE BACKUP / 只读回顾验证' : `SIMULATED FIXTURE / 单故事 API / ${story.title}`}</span>
{!params.has('portable') && stories.map((item, index) => (
<a key={item.title} href={`?story=${index}`}>
{item.title}
</a>
@@ -302,7 +340,7 @@ createRoot(document.getElementById('root')!).render(
runs={menu ? [{ id: 'run', configId: 'plan', trigger: 'scheduled', scheduledFor: createdAt, status: 'completed', attemptCount: 1, createdAt, updatedAt: createdAt }] : []}
entries={menu ? [{ id: 'report', configId: 'plan', runId: 'run', scheduledFor: createdAt, summary: '模拟历史心跳报告:交付计划已更新,待核对负责人和验收日期。', highlights: ['保留原始依据,核对交付时间。'], proposedMemoryIds: ['memory'], followUpTaskIds: ['task'], createdAt }] : []}
memories={menu ? [{ id: 'memory', scope: 'global', type: 'preference', content: '模拟建议:周会总结保留负责人和下一次检查日期。', confidence: 0.9, salience: 0.8, status: 'proposed', createdAt, updatedAt: createdAt }] : []}
projects={[]}
projects={portableProjects}
tasks={menu ? [{ id: 'task', title: '模拟行动:核对交付清单', instructions: '核对负责人、验收日期和原始讨论依据。', origin: 'assistant', status: 'paused', createdAt }] : []}
onCreate={noop}
onUpdate={noop}