Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"smoke:delegation-preflight-browser": "node --experimental-strip-types smoke/delegation-preflight-browser-smoke.mjs",
"smoke:personal-workspace": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:workspace-locale": "LOOPX_PERSONAL_WORKSPACE_SCENARIO=workspace-locale node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-evidence-return-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-feedback-recovery-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-evidence-return-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-feedback-recovery-smoke.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/goal-workspace-craft-browser-smoke.mjs",
"smoke:todo-resume-condition": "rm -rf /tmp/loopx-todo-resume-condition-smoke && tsc --ignoreConfig --target ES2022 --module commonjs --moduleResolution node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-todo-resume-condition-smoke smoke/todo-resume-condition-smoke.ts src/features/personal-workspace/todo-resume-condition.ts && node /tmp/loopx-todo-resume-condition-smoke/smoke/todo-resume-condition-smoke.js",
"smoke:presentation-surface-schema": "rm -rf /tmp/loopx-presentation-surface-schema-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --resolveJsonModule --esModuleInterop --outDir /tmp/loopx-presentation-surface-schema-smoke smoke/presentation-surface-schema-smoke.ts src/data/status.ts src/data/decision-research.ts src/data/goal-channel-frontstage.ts && NODE_PATH=\"$PWD/node_modules\" node /tmp/loopx-presentation-surface-schema-smoke/apps/presentation/dashboard/smoke/presentation-surface-schema-smoke.js",
"smoke:projection-localization": "rm -rf /tmp/loopx-projection-localization-smoke && tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck --strict --outDir /tmp/loopx-projection-localization-smoke smoke/projection-localization-smoke.ts src/features/personal-workspace/projection-localization.ts && node /tmp/loopx-projection-localization-smoke/smoke/projection-localization-smoke.js",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions apps/presentation/dashboard/smoke/delivery-review-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,35 @@ for (const copy of Object.values(deliveryReviewCopy)) {
console.log("delivery review: identity, scope, relationships, partial coverage, filtering, export and negative contracts passed");
await import("./goal-work-map-smoke.mjs");
await import("./goal-acceptance-contract-smoke.mjs");

// A visual route must not run through another task, irrespective of title,
// state, projected depth, duplicate relation or prerequisite direction.
const { goalWorkMapGeometry, goalWorkMapCard, goalWorkMapLayout } = await import("../node_modules/.cache/delivery-review/data/goal-work-map.js");
const geometryNodes = Array.from({ length: 12 }, (_, index) => ({
node_id: `node-${index}`, kind: "deliverable", state: "open", depth: index % 4,
title: "A long task with a recorded prerequisite", refs: {},
}));
const geometryEdges = geometryNodes.flatMap((node, index) => [1, 3, 5].map(offset => ({
edge_id: `${index}-${offset}`, from_node_id: node.node_id, to_node_id: geometryNodes[(index + offset) % geometryNodes.length].node_id,
relation: "depends_on", enforcement: "typed_condition", reason: "Recorded input",
})));
for (const available of [320, 700, 1200, 2000]) {
const input = { nodes: geometryNodes, edges: geometryEdges };
const original = JSON.stringify(input);
const geometry = goalWorkMapGeometry(goalWorkMapLayout(input, "all"), available);
assert.equal(JSON.stringify(input), original, "Layout cannot mutate task authority");
assert.equal(geometry.positions.size, geometryNodes.length);
assert.equal(geometry.routes.size, geometryEdges.length);
for (const points of geometry.routes.values()) for (let index = 1; index < points.length; index++) {
const a = points[index - 1], b = points[index];
assert.ok(a.x === b.x || a.y === b.y, "Segments stay in orthogonal routing gutters");
for (const { x, y } of geometry.positions.values()) {
const crosses = a.x === b.x
? a.x > x && a.x < x + goalWorkMapCard.width && Math.max(a.y, b.y) > y && Math.min(a.y, b.y) < y + goalWorkMapCard.height
: a.y > y && a.y < y + goalWorkMapCard.height && Math.max(a.x, b.x) > x && Math.min(a.x, b.x) < x + goalWorkMapCard.width;
assert.equal(crosses, false, "A relation cannot cross a card interior");
}
for (const point of [a, b]) assert.ok(point.x >= 0 && point.y >= 0 && point.x <= geometry.width && point.y <= geometry.height, "Routes remain inside their scrollable canvas");
}
}
console.log("Work map geometry: skip, reverse, same-column and dense multi-row routes avoid every card; small viewports preserve dimensions");
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Packaged visual/interaction contract. All task names are invented; source
// request/recovery and state parity are covered by the owning work-map smoke.
import assert from 'node:assert/strict';
import { mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';
import { startServer, port, outputDir } from '../../../../examples/personal-workspace-browser/fixture.mjs';
import { launchBrowser, loadPlaywright, waitForHttp } from '../../../../examples/dashboard-browser-smoke-support.mjs';
import { openWorkspacePage } from '../../../../examples/personal-workspace-browser/scenario-context.mjs';
import { installShowcaseWorkspace, openShowcaseGoal, showcaseGoalId, showcaseGoalTitle } from '../../../../examples/personal-workspace-browser/showcase-workspace.mjs';

const longTitle = `${showcaseGoalTitle}: preserve all consumer requirements, reconcile historical exports, qualify replay safety and prove recovery across two independent workers before retiring the old export service`;
const nodes = Array.from({ length: 8 }, (_, index) => ({
node_id: `craft-${index}`, kind: 'deliverable', state: 'open', depth: index % 4,
title: ['Inventory every downstream export consumer and preserve the complete original requirements', 'Verify replay safety with repeated writes and independent acceptance evidence', 'Reconcile archived totals and investigate every missing or inconsistent record', 'Qualify reconnect, interrupted delivery and result adoption by the original requester'][index % 4],
owner_agent: 'ledger-migrator', task_domain: 'validation', refs: {},
}));
const edges = nodes.slice(1).map((node, index) => ({ edge_id: `craft-edge-${index}`, from_node_id: node.node_id, to_node_id: nodes[index].node_id,
relation: 'depends_on', enforcement: 'typed_condition', reason: 'Recorded prerequisite' }));
edges.push({ edge_id: 'craft-skip', from_node_id: nodes[3].node_id, to_node_id: nodes[0].node_id, relation: 'continues', enforcement: 'lineage_only', reason: 'Recorded successor' });
const server = await startServer();
let browser;
try {
const url = `http://127.0.0.1:${port}/chat/?statusUrl=/status.json`;
await waitForHttp(url);
await mkdir(outputDir, { recursive: true });
browser = await launchBrowser(loadPlaywright().chromium);
const state = { done: 23, cost: 24.8, deliveryFails: false };
const session = await openWorkspacePage({ newPage: options => browser.newPage({ locale: 'zh-CN', ...options }) }, url, {
viewport: { width: 1440, height: 1000 },
beforeGoto: async (api, page) => {
await installShowcaseWorkspace(api, page, state);
const transform = api.transformStatus;
api.transformStatus = fixture => {
transform(fixture);
fixture.run_history.goals.find(goal => goal.id === showcaseGoalId).display_name = longTitle;
};
await page.route('**/api/chat/delivery-review?*', route => route.fulfill({ json: {
ok: true, goal_id: showcaseGoalId, observed_at: '2026-09-01T10:00:00Z', acceptance: null, graph: null,
goal_map: { schema_version: 'goal_task_map_v0', mode: 'read_only', goal_id: showcaseGoalId,
limits: { node_limit: 100, emitted_node_count: nodes.length, omitted_node_count: 0, source_truncated: false, missing_endpoint_count: 0, cycle_edge_count: 0, topology_complete: true }, nodes, edges },
} }));
},
});
const { page } = session;
try {
await openShowcaseGoal(page);
const heading = page.locator('.personal-channel-title h1');
assert.equal(await heading.innerText(), longTitle, 'The original Goal title remains complete');
assert.doesNotMatch(await page.locator('.goal-masthead-breadcrumb').evaluate(element => [...element.children].filter(child => child.getBoundingClientRect().width > 2).map(child => child.textContent).join(' ')), /preserve all consumer/, 'Visible breadcrumb does not repeat the long request');
assert.equal(await page.locator('.goal-masthead-breadcrumb [aria-current]').textContent(), longTitle, 'Assistive readers retain the complete current-page identity');
assert.doesNotMatch(await page.getByTestId('overview-composer-pill').innerText(), /ledger|consumer/, 'The input invitation stays concise');
const map = page.locator('.work-map');
await map.scrollIntoViewIfNeeded();
const scroll = page.locator('.personal-channel-scroll');
const target = map.locator('.work-map-canvas .work-map-node').last();
await target.scrollIntoViewIfNeeded();
const scrollBefore = await scroll.evaluate(element => element.scrollTop);
await target.focus();
await page.keyboard.press('Enter');
await map.getByRole('button', { name: '关闭事项详情' }).waitFor();
assert.ok(Math.abs((await scroll.evaluate(element => element.scrollTop)) - scrollBefore) <= 2, 'Selecting a task preserves the reading position');
const visible = await target.evaluate(element => { const node = element.getBoundingClientRect(), viewport = element.closest('.work-map-scroll').getBoundingClientRect(); return node.left >= viewport.left && node.right <= viewport.right && node.top >= viewport.top && node.bottom <= viewport.bottom; });
assert.ok(visible, 'The selected task stays inside the canvas when the inspector opens');
const card = await target.evaluate(element => {
const title = element.querySelector('strong').getBoundingClientRect(), footer = element.querySelector('small').getBoundingClientRect();
return { titleBottom: title.bottom, footerTop: footer.top, font: parseFloat(getComputedStyle(element.querySelector('strong')).fontSize), scale: getComputedStyle(element.closest('.work-map-canvas')).transform };
});
assert.ok(card.titleBottom <= card.footerTop, 'Task title cannot overlap its metadata');
assert.ok(card.font >= 14, 'Task text stays at the design minimum');
assert.equal(card.scale, 'matrix(1, 0, 0, 1, 0, 0)', 'A wide graph scrolls instead of shrinking text');
await page.screenshot({ path: resolve(outputDir, 'goal-craft-map-desktop.png'), animations: 'disabled' });
await map.getByRole('button', { name: '关闭事项详情' }).click();
assert.equal(await map.locator('.work-map-node[aria-pressed="true"]').count(), 0);
assert.ok(await target.evaluate(element => element === document.activeElement), 'Closing details returns focus to the task');
await page.keyboard.press('Enter'); await page.keyboard.press('Escape');
assert.equal(await map.locator('.work-map-node[aria-pressed="true"]').count(), 0, 'Escape closes the map selection');
await page.setViewportSize({ width: 1024, height: 768 });
const measured = await heading.evaluate(element => ({ height: element.clientHeight, line: parseFloat(getComputedStyle(element).lineHeight), overflow: element.scrollWidth - element.clientWidth }));
assert.ok(measured.height > measured.line && measured.overflow <= 1, 'A long title wraps without clipping or page overflow');
await page.setViewportSize({ width: 390, height: 844 });
const headerHeight = await page.locator('.personal-channel-header').evaluate(element => element.getBoundingClientRect().height);
assert.ok(headerHeight <= 220, 'A long Goal title cannot consume the phone viewport');
assert.equal(await heading.evaluate(element => getComputedStyle(element).overflow), 'hidden', 'Phone title clipping must survive the packaged CSS cascade, not just constrain the box height');
await page.getByRole('button', { name: '查看完整 Goal' }).click();
await page.locator('.personal-context-drawer').waitFor({ state: 'visible' });
assert.match(await page.locator('.personal-context-drawer').innerText(), /prove recovery across two independent workers/, 'The compact title opens the original Goal in its existing details');
await page.locator('.personal-context-drawer').getByRole('button', { name: /关闭|Close/ }).first().click();
await page.emulateMedia({ reducedMotion: 'reduce' });
await map.locator('.work-map-list').waitFor({ state: 'visible' });
assert.equal(await map.locator('.work-map-canvas').isVisible(), false, 'Phone uses a readable task list');
const mobile = map.locator('.work-map-list .work-map-node').first();
assert.equal(await mobile.locator('strong').evaluate(element => getComputedStyle(element).maxHeight), 'none', 'Phone titles are not line-clipped');
await mobile.click();
await page.waitForFunction(() => document.activeElement?.classList.contains('work-map-inspector'));
assert.ok(await map.locator('.work-map-inspector').evaluate(element => element === document.activeElement), 'Phone selection reaches keyboard-accessible details');
await page.screenshot({ path: resolve(outputDir, 'goal-craft-map-phone.png'), animations: 'disabled' });
await map.getByRole('button', { name: '关闭事项详情' }).click();
assert.ok(await mobile.evaluate(element => element === document.activeElement));
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'The phone has no page-wide horizontal overflow');
assert.deepEqual(session.errors, [], 'Packaged composition emits no browser errors');
console.log('Goal workspace craft: packaged long-title, readable geometry, stationary desktop selection, close/Escape/focus, 390px and reduced-motion checks passed');
} finally { await session.close(); }
} finally { await browser?.close(); server.kill('SIGTERM'); }
53 changes: 53 additions & 0 deletions apps/presentation/dashboard/src/data/goal-work-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,56 @@ export function goalWorkMapSharedOwner(map: GoalWorkMap) {
const owners = new Set(map.nodes.flatMap(node => node.owner_agent ? [node.owner_agent] : []));
return owners.size === 1 && map.nodes.filter(node => node.owner_agent).length > 1 ? [...owners][0] : null;
}

/** Presentation geometry only: roomy cards and routes confined to empty gutters.
* Keep one set of dimensions for the canvas and CSS; never shrink text to fit.
*/
export const goalWorkMapCard = { width: 236, height: 136 };
export function goalWorkMapGeometry(layout: ReturnType<typeof goalWorkMapLayout>, available: number) {
const { width: cardWidth, height: cardHeight } = goalWorkMapCard;
const columnStep = cardWidth + 64, rowStep = cardHeight + 36, padding = 24;
const positions = new Map<string, { x: number; y: number }>();
const routes = new Map<string, { x: number; y: number }[]>();
const frames: { x: number; y: number; w: number; h: number }[] = [];
let x = 0, y = 0, shelf = 0, right = 0;
for (const group of layout.groups) {
const columnIndex = new Map(group.columns.flatMap((column, index) => column.map(node => [node.node_id, index] as const)));
const pairs = new Map<string, GoalWorkMapEdge[]>();
for (const edge of layout.edges) {
if (!columnIndex.has(edge.from_node_id)) continue;
const key = `${edge.from_node_id}\0${edge.to_node_id}`;
pairs.set(key, [...pairs.get(key) ?? [], edge]);
}
const detours = [...pairs.values()].filter(([edge]) => columnIndex.get(edge.from_node_id)! !== columnIndex.get(edge.to_node_id)! + 1);
const headroom = detours.length ? 20 + detours.length * 8 : 0;
const w = group.columns.length * columnStep - 64;
const h = Math.max(...group.columns.map(column => column.length)) * rowStep - 36 + headroom;
if (x > 0 && x + w > available - padding * 2) { y += shelf + 48; x = 0; shelf = 0; }
const left = padding + x, top = padding + y;
group.columns.forEach((column, c) => column.forEach((node, r) => positions.set(node.node_id, { x: left + c * columnStep, y: top + headroom + r * rowStep })));
let detour = 0;
for (const edges of pairs.values()) {
const edge = edges[0], source = positions.get(edge.to_node_id)!, target = positions.get(edge.from_node_id)!;
const start = { x: source.x + cardWidth, y: source.y + cardHeight / 2 };
const end = { x: target.x, y: target.y + cardHeight / 2 };
let points;
if (columnIndex.get(edge.from_node_id)! === columnIndex.get(edge.to_node_id)! + 1) {
const middle = (start.x + end.x) / 2;
points = [start, { x: middle, y: start.y }, { x: middle, y: end.y }, end];
} else {
// Skip-column, reverse and same-column links travel above the group.
// Their vertical sections stay outside all card columns, including
// other rows in the source and destination columns.
const track = top + 8 + detour++ * 8;
points = [start, { x: start.x + 16, y: start.y }, { x: start.x + 16, y: track },
{ x: end.x - 16, y: track }, { x: end.x - 16, y: end.y }, end];
}
edges.forEach(item => routes.set(item.edge_id, points));
}
frames.push({ x: left - 12, y: top - 12, w: w + 24, h: h + 24 });
right = Math.max(right, x + w);
x += w + 48;
shelf = Math.max(shelf, h);
}
return { positions, routes, frames, width: padding * 2 + right, height: padding * 2 + y + shelf };
}
Loading
Loading