@transitrix/diagrams 1.6.0 → 1.8.1
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/dist/activities/layout.js +7 -7
- package/dist/activities/types.d.ts +4 -0
- package/dist/capability-map/dsm-schema.d.ts +1 -1
- package/dist/capability-map/dsm-schema.js +1 -1
- package/dist/capability-map/layout-tree.d.ts +5 -1
- package/dist/capability-map/layout-tree.js +11 -9
- package/dist/capability-map/render-capability-tree.d.ts +2 -9
- package/dist/capability-map/render-capability-tree.js +16 -28
- package/dist/codex/__tests__/example.test.d.ts +1 -0
- package/dist/codex/__tests__/example.test.js +33 -0
- package/dist/codex/__tests__/validate.test.d.ts +1 -0
- package/dist/codex/__tests__/validate.test.js +68 -0
- package/dist/codex/index.d.ts +4 -0
- package/dist/codex/index.js +2 -0
- package/dist/codex/types.d.ts +7 -0
- package/dist/codex/types.js +8 -0
- package/dist/codex/validate.d.ts +11 -0
- package/dist/codex/validate.js +103 -0
- package/dist/compliance/__tests__/canon-catalog.test.d.ts +1 -0
- package/dist/compliance/__tests__/canon-catalog.test.js +60 -0
- package/dist/compliance/__tests__/view-resolution.test.d.ts +1 -0
- package/dist/compliance/__tests__/view-resolution.test.js +59 -0
- package/dist/compliance/canon-catalog.d.ts +19 -0
- package/dist/compliance/canon-catalog.js +59 -0
- package/dist/compliance/coverage-metric.d.ts +12 -0
- package/dist/compliance/coverage-metric.js +35 -0
- package/dist/compliance/impact.d.ts +7 -1
- package/dist/compliance/impact.js +53 -0
- package/dist/compliance/index.d.ts +6 -4
- package/dist/compliance/index.js +3 -2
- package/dist/constraint/__tests__/example.test.d.ts +1 -0
- package/dist/constraint/__tests__/example.test.js +14 -0
- package/dist/constraint/__tests__/validate.test.d.ts +1 -0
- package/dist/constraint/__tests__/validate.test.js +58 -0
- package/dist/constraint/index.d.ts +4 -0
- package/dist/constraint/index.js +2 -0
- package/dist/constraint/types.d.ts +4 -0
- package/dist/constraint/types.js +2 -0
- package/dist/constraint/validate.d.ts +7 -0
- package/dist/constraint/validate.js +115 -0
- package/dist/fgca/__tests__/parse-canonical.test.js +31 -0
- package/dist/fgca/parse-canonical.d.ts +1 -1
- package/dist/fgca/parse-canonical.js +18 -4
- package/dist/fgca/preview-layout.d.ts +8 -4
- package/dist/fgca/preview-layout.js +13 -13
- package/dist/goals/GoalNode.d.ts +2 -1
- package/dist/goals/GoalNode.js +7 -2
- package/dist/goals/GoalTreeView.d.ts +3 -1
- package/dist/goals/GoalTreeView.js +31 -14
- package/dist/goals/__tests__/GoalTreeView.test.js +41 -0
- package/dist/goals/__tests__/mutations.test.js +39 -0
- package/dist/goals/__tests__/parse-canonical.test.js +2 -0
- package/dist/goals/layout.js +2 -1
- package/dist/goals/mutations.js +6 -6
- package/dist/goals/parse-canonical.d.ts +5 -0
- package/dist/goals/parse-canonical.js +5 -0
- package/dist/goals/types.d.ts +2 -0
- package/dist/node-size-presets.d.ts +50 -0
- package/dist/node-size-presets.js +103 -0
- package/dist/process-blueprint/layout.js +4 -47
- package/dist/repo-validate/types.js +2 -3
- package/dist/webview/__tests__/entity-text-layout.test.d.ts +1 -0
- package/dist/webview/__tests__/entity-text-layout.test.js +43 -0
- package/dist/webview/__tests__/entry.test.js +3 -3
- package/dist/webview/__tests__/render-blocks.test.js +1 -7
- package/dist/webview/__tests__/render-goals.test.js +17 -3
- package/dist/webview/entity-text-layout.d.ts +95 -0
- package/dist/webview/entity-text-layout.js +279 -0
- package/dist/webview/entry.js +7 -3
- package/dist/webview/notation-style.d.ts +1 -1
- package/dist/webview/notation-style.js +1 -1
- package/dist/webview/render-activities.d.ts +3 -1
- package/dist/webview/render-activities.js +29 -32
- package/dist/webview/render-blocks.d.ts +4 -1
- package/dist/webview/render-blocks.js +27 -108
- package/dist/webview/render-fgca.d.ts +15 -8
- package/dist/webview/render-fgca.js +41 -44
- package/dist/webview/render-goals.d.ts +2 -0
- package/dist/webview/render-goals.js +23 -37
- package/dist/webview/render-process-blueprint.d.ts +4 -10
- package/dist/webview/render-process-blueprint.js +18 -64
- package/package.json +1 -1
- package/src/activities/layout.ts +12 -7
- package/src/activities/types.ts +4 -0
- package/src/capability-map/dsm-schema.ts +1 -1
- package/src/capability-map/index.ts +1 -1
- package/src/capability-map/layout-tree.ts +16 -8
- package/src/capability-map/render-capability-tree.ts +18 -30
- package/src/codex/__tests__/example.test.ts +40 -0
- package/src/codex/__tests__/validate.test.ts +84 -0
- package/src/codex/index.ts +4 -0
- package/src/codex/types.ts +13 -0
- package/src/codex/validate.ts +125 -0
- package/src/compliance/__tests__/canon-catalog.test.ts +68 -0
- package/src/compliance/__tests__/view-resolution.test.ts +80 -0
- package/src/compliance/canon-catalog.ts +76 -0
- package/src/compliance/coverage-metric.ts +50 -0
- package/src/compliance/impact.ts +67 -1
- package/src/compliance/index.ts +10 -1
- package/src/constraint/__tests__/example.test.ts +19 -0
- package/src/constraint/__tests__/validate.test.ts +68 -0
- package/src/constraint/index.ts +4 -0
- package/src/constraint/types.ts +5 -0
- package/src/constraint/validate.ts +131 -0
- package/src/fgca/__tests__/parse-canonical.test.ts +34 -0
- package/src/fgca/parse-canonical.ts +17 -4
- package/src/fgca/preview-layout.ts +21 -15
- package/src/goals/GoalNode.tsx +8 -1
- package/src/goals/GoalTreeView.tsx +61 -27
- package/src/goals/__tests__/GoalTreeView.test.tsx +48 -0
- package/src/goals/__tests__/mutations.test.ts +42 -0
- package/src/goals/__tests__/parse-canonical.test.ts +2 -0
- package/src/goals/layout.ts +2 -1
- package/src/goals/mutations.ts +6 -6
- package/src/goals/parse-canonical.ts +6 -0
- package/src/goals/types.ts +2 -0
- package/src/node-size-presets.ts +157 -0
- package/src/process-blueprint/layout.ts +4 -48
- package/src/repo-validate/types.ts +2 -3
- package/src/webview/__tests__/entity-text-layout.test.ts +52 -0
- package/src/webview/__tests__/entry.test.ts +3 -3
- package/src/webview/__tests__/render-blocks.test.ts +1 -9
- package/src/webview/__tests__/render-goals.test.ts +17 -3
- package/src/webview/entity-text-layout.ts +355 -0
- package/src/webview/entry.ts +8 -4
- package/src/webview/notation-style.ts +1 -1
- package/src/webview/render-activities.ts +38 -27
- package/src/webview/render-blocks.ts +35 -118
- package/src/webview/render-fgca.ts +59 -44
- package/src/webview/render-goals.ts +24 -35
- package/src/webview/render-process-blueprint.ts +30 -65
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
// Pure column-layout geometry for the static
|
|
1
|
+
// Pure column-layout geometry for the static DGCA / DGA previews.
|
|
2
2
|
//
|
|
3
|
-
// This is the layout the Studio extension's `
|
|
3
|
+
// This is the layout the Studio extension's `dgca-preview.ts` renders to SVG
|
|
4
4
|
// (NOT the ReactFlow `buildFGCALayout` in `./layout.ts`, which targets the
|
|
5
5
|
// interactive web UI). It lives here — rather than inline in the extension —
|
|
6
6
|
// so the gap geometry is unit-testable: the extension has no test harness.
|
|
7
7
|
//
|
|
8
8
|
// No React, no DOM, no vscode. Safe to call in Node.js or a test environment.
|
|
9
9
|
/**
|
|
10
|
-
* Trims
|
|
10
|
+
* Trims a DGCA/DGA doc to a scope.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* DGCA/DGA goals are flat (no parent_id), so:
|
|
13
13
|
* - 'level' → goals with `(level ?? 0) <= maxLevel`.
|
|
14
14
|
* - 'root' → the single goal whose id matches `rootGoalId` (empty when absent).
|
|
15
15
|
*
|
|
@@ -38,8 +38,8 @@ export function selectScopedFGCA(doc, scope) {
|
|
|
38
38
|
activities: visibleActivities,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
//
|
|
42
|
-
//
|
|
41
|
+
// Default node + frame geometry. Inter-node gaps are user-configurable via
|
|
42
|
+
// Inter-node gaps are user-configurable via spacing settings; node size uses presets.
|
|
43
43
|
export const FGCA_NODE_W = 220;
|
|
44
44
|
export const FGCA_NODE_H = 72;
|
|
45
45
|
export const FGCA_HEADER_H = 32;
|
|
@@ -47,10 +47,10 @@ export const FGCA_PAD = 20;
|
|
|
47
47
|
export const FGCA_DEFAULT_COL_GAP = 160;
|
|
48
48
|
export const FGCA_DEFAULT_ROW_GAP = 20;
|
|
49
49
|
export function layoutFGCAPreview(inputDoc, options = {}) {
|
|
50
|
-
const { hideChanges = false, colGap = FGCA_DEFAULT_COL_GAP, rowGap = FGCA_DEFAULT_ROW_GAP, scope = { mode: 'all' }, } = options;
|
|
50
|
+
const { hideChanges = false, colGap = FGCA_DEFAULT_COL_GAP, rowGap = FGCA_DEFAULT_ROW_GAP, scope = { mode: 'all' }, nodeWidth = FGCA_NODE_W, nodeHeight = FGCA_NODE_H, } = options;
|
|
51
51
|
// Trim to scope first; everything below lays out the visible subset only.
|
|
52
52
|
const doc = selectScopedFGCA(inputDoc, scope);
|
|
53
|
-
const colStride =
|
|
53
|
+
const colStride = nodeWidth + colGap;
|
|
54
54
|
const cols = hideChanges
|
|
55
55
|
? ['driver', 'goal', 'activity']
|
|
56
56
|
: ['driver', 'goal', 'change', 'activity'];
|
|
@@ -110,7 +110,7 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
|
|
|
110
110
|
const nodeMap = new Map();
|
|
111
111
|
const nodes = [];
|
|
112
112
|
const columns = [];
|
|
113
|
-
const yCenters = new Map(); // nodeId → y +
|
|
113
|
+
const yCenters = new Map(); // nodeId → y + nodeHeight / 2
|
|
114
114
|
for (let ci = 0; ci < cols.length; ci++) {
|
|
115
115
|
const col = cols[ci];
|
|
116
116
|
const x = FGCA_PAD + ci * colStride;
|
|
@@ -121,8 +121,8 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
|
|
|
121
121
|
const node = { id: item.id, x, y, label: item.label, col };
|
|
122
122
|
nodes.push(node);
|
|
123
123
|
nodeMap.set(item.id, node);
|
|
124
|
-
yCenters.set(item.id, y +
|
|
125
|
-
y +=
|
|
124
|
+
yCenters.set(item.id, y + nodeHeight / 2);
|
|
125
|
+
y += nodeHeight + rowGap;
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
const edges = [];
|
|
@@ -131,7 +131,7 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
|
|
|
131
131
|
const t = nodeMap.get(targetId);
|
|
132
132
|
if (!s || !t)
|
|
133
133
|
return;
|
|
134
|
-
edges.push({ sx: s.x +
|
|
134
|
+
edges.push({ sx: s.x + nodeWidth, sy: s.y + nodeHeight / 2, tx: t.x, ty: t.y + nodeHeight / 2 });
|
|
135
135
|
}
|
|
136
136
|
for (const g of doc.goals) {
|
|
137
137
|
for (const f of (g.factor ?? []))
|
|
@@ -164,7 +164,7 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
-
const maxNodeBottom = nodes.reduce((m, n) => Math.max(m, n.y +
|
|
167
|
+
const maxNodeBottom = nodes.reduce((m, n) => Math.max(m, n.y + nodeHeight), FGCA_PAD + FGCA_HEADER_H + rowGap + nodeHeight);
|
|
168
168
|
const width = FGCA_PAD * 2 + cols.length * colStride - colGap;
|
|
169
169
|
const height = maxNodeBottom + FGCA_PAD;
|
|
170
170
|
return { nodes, edges, columns, width, height };
|
package/dist/goals/GoalNode.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { Goal } from './types.js';
|
|
2
|
+
import type { Factor, Goal } from './types.js';
|
|
3
3
|
import type { ThemeTokens } from './theme.js';
|
|
4
4
|
export interface GoalNodeData {
|
|
5
5
|
goal: Goal;
|
|
@@ -11,6 +11,7 @@ export interface GoalNodeData {
|
|
|
11
11
|
onAddChild: (parentId: number, parentLevel: number) => void;
|
|
12
12
|
onDelete: (id: number) => void;
|
|
13
13
|
onToggleCollapse: (id: number) => void;
|
|
14
|
+
onFactorClick?: (factor: Factor) => void;
|
|
14
15
|
}
|
|
15
16
|
declare const GoalNode: React.MemoExoticComponent<({ data }: {
|
|
16
17
|
data: GoalNodeData;
|
package/dist/goals/GoalNode.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useState } from 'react';
|
|
3
3
|
import { Handle, Position } from 'reactflow';
|
|
4
|
+
import { displayGoalId } from './parse-canonical.js';
|
|
4
5
|
const NODE_WIDTH = 250;
|
|
5
6
|
const NODE_HEIGHT = 80;
|
|
6
7
|
function impactSign(f) {
|
|
@@ -44,7 +45,7 @@ const GoalNode = memo(({ data }) => {
|
|
|
44
45
|
WebkitLineClamp: 2,
|
|
45
46
|
WebkitBoxOrient: 'vertical',
|
|
46
47
|
wordBreak: 'break-word',
|
|
47
|
-
}, children: goal.name }), _jsxs("div", { style: { fontSize: 10, color: theme.cardMetaColor, marginTop: 2 }, children: ["ID: ", goal
|
|
48
|
+
}, children: goal.name }), _jsxs("div", { style: { fontSize: 10, color: theme.cardMetaColor, marginTop: 2 }, children: ["ID: ", displayGoalId(goal), " | Lvl: ", goal.level, goal.tag ? ` | #${goal.tag}` : ''] })] }), _jsx("div", { style: { flexShrink: 0, display: 'flex', alignItems: 'center', width: 16, justifyContent: 'flex-end' }, children: negative.length > 0 && (_jsxs("span", { role: "button", "aria-label": `${negative.length} negative factor${negative.length > 1 ? 's' : ''}`, title: `${negative.length} negative factor${negative.length > 1 ? 's' : ''}`, onClick: (e) => {
|
|
48
49
|
e.stopPropagation();
|
|
49
50
|
setOpenPanel(openPanel === 'negative' ? null : 'negative');
|
|
50
51
|
}, style: { cursor: 'pointer', color: '#dc2626', fontSize: 14, lineHeight: 1, userSelect: 'none' }, children: ["\u25BC", negative.length > 1 && _jsx("span", { style: { fontSize: 9, fontWeight: 700, marginLeft: 2 }, children: negative.length })] })) }), openPanel && (_jsx("div", { style: {
|
|
@@ -56,7 +57,10 @@ const GoalNode = memo(({ data }) => {
|
|
|
56
57
|
flexDirection: 'column',
|
|
57
58
|
gap: 4,
|
|
58
59
|
...(openPanel === 'positive' ? { bottom: 'calc(100% + 8px)' } : { top: 'calc(100% + 8px)' }),
|
|
59
|
-
}, children: (openPanel === 'positive' ? positive : negative).map((f) => (_jsxs("div", {
|
|
60
|
+
}, children: (openPanel === 'positive' ? positive : negative).map((f) => (_jsxs("div", { onClick: (e) => {
|
|
61
|
+
e.stopPropagation();
|
|
62
|
+
data.onFactorClick?.(f);
|
|
63
|
+
}, style: {
|
|
60
64
|
background: openPanel === 'positive' ? '#f0fdf4' : '#fef2f2',
|
|
61
65
|
border: `1px solid ${openPanel === 'positive' ? '#bbf7d0' : '#fecaca'}`,
|
|
62
66
|
color: openPanel === 'positive' ? '#166534' : '#991b1b',
|
|
@@ -64,6 +68,7 @@ const GoalNode = memo(({ data }) => {
|
|
|
64
68
|
padding: '4px 6px',
|
|
65
69
|
fontSize: 10,
|
|
66
70
|
textAlign: 'left',
|
|
71
|
+
cursor: data.onFactorClick ? 'pointer' : 'default',
|
|
67
72
|
}, children: [_jsx("div", { style: { fontWeight: 700 }, children: f.name }), f.description && _jsx("div", { style: { opacity: 0.85 }, children: f.description })] }, f.id))) })), !readOnly && (_jsx("button", { type: "button", "aria-label": "Add child goal", title: "Add child goal", onClick: (e) => {
|
|
68
73
|
e.stopPropagation();
|
|
69
74
|
data.onAddChild(goal.id, goal.level);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import 'reactflow/dist/style.css';
|
|
3
|
-
import type { Goal, GoalTree, GoalTreeLayout, LayoutOptions } from './types.js';
|
|
3
|
+
import type { Factor, Goal, GoalTree, GoalTreeLayout, LayoutOptions } from './types.js';
|
|
4
4
|
import type { ThemeTokens } from './theme.js';
|
|
5
5
|
export type { ThemeTokens } from './theme.js';
|
|
6
6
|
export interface GoalTreeViewProps {
|
|
@@ -10,9 +10,11 @@ export interface GoalTreeViewProps {
|
|
|
10
10
|
theme?: ThemeTokens;
|
|
11
11
|
readOnly?: boolean;
|
|
12
12
|
showBacklog?: boolean;
|
|
13
|
+
defaultBacklogOpen?: boolean;
|
|
13
14
|
showMiniMap?: boolean;
|
|
14
15
|
onChange?: (event: GoalTreeChange) => void;
|
|
15
16
|
onEditRequest?: (goal: Goal) => void;
|
|
17
|
+
onFactorClick?: (factor: Factor) => void;
|
|
16
18
|
}
|
|
17
19
|
export type GoalTreeChange = {
|
|
18
20
|
kind: 'reparent';
|
|
@@ -61,10 +61,11 @@ function partitionCanvasAndBacklog(goals) {
|
|
|
61
61
|
}
|
|
62
62
|
return { canvas, backlog };
|
|
63
63
|
}
|
|
64
|
-
function GoalTreeViewInner({ tree, layout: layoutProp, layoutOptions, theme: themeProp, readOnly = false, showBacklog = false, showMiniMap = false, onChange, onEditRequest, }) {
|
|
64
|
+
function GoalTreeViewInner({ tree, layout: layoutProp, layoutOptions, theme: themeProp, readOnly = false, showBacklog = false, defaultBacklogOpen = true, showMiniMap = false, onChange, onEditRequest, onFactorClick, }) {
|
|
65
65
|
const theme = useMemo(() => resolveTheme(themeProp), [themeProp]);
|
|
66
66
|
const [collapsedIds, setCollapsedIds] = useState(new Set());
|
|
67
67
|
const [hoveredId, setHoveredId] = useState(null);
|
|
68
|
+
const [isBacklogOpen, setIsBacklogOpen] = useState(defaultBacklogOpen);
|
|
68
69
|
const { canvas: canvasGoals, backlog: backlogGoals } = useMemo(() => partitionCanvasAndBacklog(tree.goals), [tree.goals]);
|
|
69
70
|
const layout = useMemo(() => {
|
|
70
71
|
if (layoutProp)
|
|
@@ -126,9 +127,10 @@ function GoalTreeViewInner({ tree, layout: layoutProp, layoutOptions, theme: the
|
|
|
126
127
|
onAddChild,
|
|
127
128
|
onDelete: onDeleteNode,
|
|
128
129
|
onToggleCollapse,
|
|
130
|
+
onFactorClick,
|
|
129
131
|
},
|
|
130
132
|
}));
|
|
131
|
-
}, [layout.nodes, theme, readOnly, hoveredId, collapsedIds, onAddChild, onDeleteNode, onToggleCollapse]);
|
|
133
|
+
}, [layout.nodes, theme, readOnly, hoveredId, collapsedIds, onAddChild, onDeleteNode, onToggleCollapse, onFactorClick]);
|
|
132
134
|
const buildEdges = useCallback(() => {
|
|
133
135
|
return layout.edges.map((e) => ({
|
|
134
136
|
id: `e${e.source}-${e.target}`,
|
|
@@ -206,22 +208,37 @@ function GoalTreeViewInner({ tree, layout: layoutProp, layoutOptions, theme: the
|
|
|
206
208
|
onRestoreFromBacklog(goalId, Number(targetIdAttr));
|
|
207
209
|
}, [readOnly, onRestoreFromBacklog]);
|
|
208
210
|
const showBacklogPanel = showBacklog && backlogGoals.length > 0;
|
|
209
|
-
return (_jsxs("div", { style: { display: 'flex', width: '100%', height: '100%' }, children: [showBacklogPanel && (_jsxs("div", {
|
|
210
|
-
width: 220,
|
|
211
|
+
return (_jsxs("div", { style: { display: 'flex', width: '100%', height: '100%' }, children: [showBacklogPanel && (_jsxs("div", { ...(isBacklogOpen ? { 'data-goaltree-backlog-zone': true } : {}), style: {
|
|
212
|
+
width: isBacklogOpen ? 220 : 28,
|
|
211
213
|
flexShrink: 0,
|
|
212
214
|
borderRight: '1px solid #e2e8f0',
|
|
213
|
-
padding: 10,
|
|
214
|
-
overflowY: 'auto',
|
|
215
215
|
background: '#f8fafc',
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
216
|
+
display: 'flex',
|
|
217
|
+
flexDirection: 'column',
|
|
218
|
+
position: 'relative',
|
|
219
|
+
}, children: [_jsx("button", { type: "button", "aria-label": isBacklogOpen ? 'Collapse backlog' : 'Expand backlog', title: isBacklogOpen ? 'Collapse backlog' : 'Expand backlog', onClick: () => setIsBacklogOpen((v) => !v), style: {
|
|
220
|
+
alignSelf: 'flex-end',
|
|
221
|
+
margin: '6px 6px 0 6px',
|
|
222
|
+
width: 16,
|
|
223
|
+
height: 16,
|
|
220
224
|
border: '1px solid #cbd5e1',
|
|
221
|
-
borderRadius:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
+
borderRadius: 4,
|
|
226
|
+
background: '#ffffff',
|
|
227
|
+
color: '#64748b',
|
|
228
|
+
fontSize: 10,
|
|
229
|
+
lineHeight: '14px',
|
|
230
|
+
padding: 0,
|
|
231
|
+
cursor: 'pointer',
|
|
232
|
+
flexShrink: 0,
|
|
233
|
+
}, children: isBacklogOpen ? '◀' : '▶' }), isBacklogOpen && (_jsxs("div", { style: { padding: '4px 10px 10px', overflowY: 'auto', flex: 1 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 12, color: '#334155', marginBottom: 8 }, children: "Backlog" }), backlogGoals.map((goal) => (_jsxs("div", { draggable: !readOnly, onDragStart: (e) => onBacklogItemDragStart(e, goal.id), onDoubleClick: () => onEditRequest?.(goal), title: "Drag onto a canvas card to set its parent", style: {
|
|
234
|
+
padding: 8,
|
|
235
|
+
marginBottom: 6,
|
|
236
|
+
background: '#ffffff',
|
|
237
|
+
border: '1px solid #cbd5e1',
|
|
238
|
+
borderRadius: 6,
|
|
239
|
+
fontSize: 11,
|
|
240
|
+
cursor: readOnly ? 'default' : 'grab',
|
|
241
|
+
}, children: [_jsx("div", { style: { fontWeight: 600 }, children: goal.name }), _jsxs("div", { style: { color: '#64748b' }, children: ["Level: ", goal.level] })] }, goal.id)))] }))] })), _jsx("div", { style: { flex: 1, position: 'relative' }, onDragOver: onCanvasDragOver, onDrop: onCanvasDrop, children: _jsxs(ReactFlow, { nodes: nodes, edges: edges, onNodesChange: readOnly ? undefined : onNodesChange, onEdgesChange: readOnly ? undefined : onEdgesChange, nodeTypes: nodeTypes, nodesDraggable: !readOnly, nodesConnectable: false, onNodeDrag: onNodeDrag, onNodeDragStop: onNodeDragStop, onNodeDoubleClick: onNodeDoubleClick, connectionLineType: ConnectionLineType.Bezier, fitView: true, children: [_jsx(Background, {}), _jsx(Controls, {}), showMiniMap && _jsx(MiniMap, { pannable: true, zoomable: true })] }) })] }));
|
|
225
242
|
}
|
|
226
243
|
export function GoalTreeView(props) {
|
|
227
244
|
return _jsx(GoalTreeViewInner, { ...props });
|
|
@@ -281,6 +281,47 @@ describe('GoalTreeView', () => {
|
|
|
281
281
|
expect(computeDragOutcome(deeper, 3, { kind: 'node', id: 4 })).toBeNull();
|
|
282
282
|
});
|
|
283
283
|
});
|
|
284
|
+
it('fires onFactorClick with the clicked factor when a popover item is clicked', async () => {
|
|
285
|
+
const onFactorClick = vi.fn();
|
|
286
|
+
render(_jsx(GoalTreeView, { tree: makeTree(), onFactorClick: onFactorClick }));
|
|
287
|
+
await waitFor(() => {
|
|
288
|
+
expect(screen.getByText('Cut churn')).toBeTruthy();
|
|
289
|
+
});
|
|
290
|
+
// Open the negative-factor panel on "Cut churn" (id=3, has risk factor "Support backlog")
|
|
291
|
+
const negativeBadge = screen.getByLabelText('1 negative factor');
|
|
292
|
+
negativeBadge.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
293
|
+
// The popover item for the risk factor should now be in the DOM
|
|
294
|
+
const factorItem = await waitFor(() => screen.getByText('Support backlog'));
|
|
295
|
+
factorItem.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
296
|
+
await waitFor(() => expect(onFactorClick).toHaveBeenCalledTimes(1));
|
|
297
|
+
expect(onFactorClick.mock.calls[0][0]).toMatchObject({ id: 10, name: 'Support backlog', impact_type: 'risk' });
|
|
298
|
+
});
|
|
299
|
+
it('backlog panel is collapsible — toggle hides and restores content', async () => {
|
|
300
|
+
const tree = {
|
|
301
|
+
goal_types: [
|
|
302
|
+
{ name: 'Strategy', level: 0 },
|
|
303
|
+
{ name: 'Business Goal', level: 1 },
|
|
304
|
+
],
|
|
305
|
+
goals: [
|
|
306
|
+
{ id: 1, name: 'Root goal', type: 'Strategy', level: 0, parent_id: 0 },
|
|
307
|
+
{ id: 2, name: 'Orphaned goal', type: 'Business Goal', level: 1, parent_id: 999 },
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
render(_jsx(GoalTreeView, { tree: tree, showBacklog: true }));
|
|
311
|
+
await waitFor(() => {
|
|
312
|
+
expect(screen.getByText('Backlog')).toBeTruthy();
|
|
313
|
+
});
|
|
314
|
+
// Collapse
|
|
315
|
+
screen.getByLabelText('Collapse backlog').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
316
|
+
await waitFor(() => {
|
|
317
|
+
expect(screen.queryByText('Backlog')).toBeNull();
|
|
318
|
+
});
|
|
319
|
+
// Expand again
|
|
320
|
+
screen.getByLabelText('Expand backlog').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
321
|
+
await waitFor(() => {
|
|
322
|
+
expect(screen.getByText('Backlog')).toBeTruthy();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
284
325
|
it('fires onEditRequest on double-click', async () => {
|
|
285
326
|
const onEditRequest = vi.fn();
|
|
286
327
|
render(_jsx(GoalTreeView, { tree: makeTree(), onEditRequest: onEditRequest }));
|
|
@@ -93,3 +93,42 @@ describe('restoreFromBacklog', () => {
|
|
|
93
93
|
expect(r.result.goals.find(g => g.id === 3).parent_id).toBe(1);
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
|
+
describe('reparent — descendant type relabeling and level cap', () => {
|
|
97
|
+
const DEEP = {
|
|
98
|
+
goal_types: [
|
|
99
|
+
{ name: 'Strategy', level: 0 },
|
|
100
|
+
{ name: 'Business Goal', level: 1 },
|
|
101
|
+
{ name: 'Project', level: 2 },
|
|
102
|
+
{ name: 'Task', level: 3 },
|
|
103
|
+
],
|
|
104
|
+
goals: [
|
|
105
|
+
{ id: 1, name: 'Root', type: 'Strategy', level: 0, parent_id: 0 },
|
|
106
|
+
{ id: 2, name: 'BG1', type: 'Business Goal', level: 1, parent_id: 1 },
|
|
107
|
+
{ id: 3, name: 'Proj1', type: 'Project', level: 2, parent_id: 2 },
|
|
108
|
+
{ id: 4, name: 'Task1', type: 'Task', level: 3, parent_id: 3 },
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
it('relabels descendant types via getTypeForLevel when reparenting shifts levels', () => {
|
|
112
|
+
// Move Proj1 (id=3, level=2) directly under Root (id=1, level=0); delta = -1
|
|
113
|
+
// Task1 (child of Proj1, level=3): new level = 2, type should be 'Project'
|
|
114
|
+
const r = reparent(DEEP, 3, 1);
|
|
115
|
+
expect(r.ok).toBe(true);
|
|
116
|
+
const proj = r.result.goals.find(g => g.id === 3);
|
|
117
|
+
const task = r.result.goals.find(g => g.id === 4);
|
|
118
|
+
expect(proj.level).toBe(1);
|
|
119
|
+
expect(proj.type).toBe('Business Goal');
|
|
120
|
+
expect(task.level).toBe(2);
|
|
121
|
+
expect(task.type).toBe('Project');
|
|
122
|
+
});
|
|
123
|
+
it('caps descendant level at maxLevel and refuses when that violates GOALS-012', () => {
|
|
124
|
+
// Move Root (id=1, level=0) under Root2 (id=5, level=0); delta = +1
|
|
125
|
+
// BG1: 1→2, Proj1: 2→3 (maxLevel), Task1: 3→4, capped at 3
|
|
126
|
+
// Task1 (capped level=3) under Proj1 (level=3) violates GOALS-012 → refused
|
|
127
|
+
const tree = {
|
|
128
|
+
...DEEP,
|
|
129
|
+
goals: [...DEEP.goals, { id: 5, name: 'Root2', type: 'Strategy', level: 0, parent_id: 0 }],
|
|
130
|
+
};
|
|
131
|
+
const r = reparent(tree, 1, 5);
|
|
132
|
+
expect(r.ok).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -107,8 +107,10 @@ describe('parseCanonicalGoals', () => {
|
|
|
107
107
|
it('maps canonical IDs to internal numeric ids; parent_id is the parent\'s mapped id', () => {
|
|
108
108
|
const r = parseCanonicalGoals(VALID);
|
|
109
109
|
expect(r.parsed?.goals[0].id).toBe(1);
|
|
110
|
+
expect(r.parsed?.goals[0].canonical_id).toBe('GOAL-1');
|
|
110
111
|
expect(r.parsed?.goals[0].parent_id).toBe(0);
|
|
111
112
|
expect(r.parsed?.goals[1].id).toBe(2);
|
|
113
|
+
expect(r.parsed?.goals[1].canonical_id).toBe('GOAL-2');
|
|
112
114
|
expect(r.parsed?.goals[1].parent_id).toBe(1);
|
|
113
115
|
});
|
|
114
116
|
});
|
package/dist/goals/layout.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { displayGoalId } from './parse-canonical.js';
|
|
1
2
|
const DEFAULT_NODE_WIDTH = 250;
|
|
2
3
|
const DEFAULT_NODE_HEIGHT = 80;
|
|
3
4
|
const DEFAULT_RANK_SEP = 80;
|
|
@@ -21,7 +22,7 @@ export function selectScopedGoals(goals, scope) {
|
|
|
21
22
|
children.set(g.parent_id, []);
|
|
22
23
|
children.get(g.parent_id).push(g.id);
|
|
23
24
|
}
|
|
24
|
-
const root = goals.find(g =>
|
|
25
|
+
const root = goals.find(g => displayGoalId(g) === scope.rootGoalId);
|
|
25
26
|
if (!root)
|
|
26
27
|
return [];
|
|
27
28
|
const keep = new Set();
|
package/dist/goals/mutations.js
CHANGED
|
@@ -16,12 +16,12 @@ function getTypeForLevel(tree, level) {
|
|
|
16
16
|
const gt = tree.goal_types.find(t => t.level === level);
|
|
17
17
|
return gt?.name ?? '';
|
|
18
18
|
}
|
|
19
|
-
function updateDescendantLevels(
|
|
20
|
-
for (const g of goals) {
|
|
19
|
+
function updateDescendantLevels(clone, id, levelDelta, maxLevel) {
|
|
20
|
+
for (const g of clone.goals) {
|
|
21
21
|
if (g.parent_id === id) {
|
|
22
|
-
g.level
|
|
23
|
-
g.type =
|
|
24
|
-
updateDescendantLevels(
|
|
22
|
+
g.level = Math.min(g.level + levelDelta, maxLevel);
|
|
23
|
+
g.type = getTypeForLevel(clone, g.level);
|
|
24
|
+
updateDescendantLevels(clone, g.id, levelDelta, maxLevel);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -67,7 +67,7 @@ export function reparent(tree, sourceId, targetId) {
|
|
|
67
67
|
source.level = newLevel;
|
|
68
68
|
source.type = getTypeForLevel(clone, newLevel);
|
|
69
69
|
if (levelDelta !== 0)
|
|
70
|
-
updateDescendantLevels(clone
|
|
70
|
+
updateDescendantLevels(clone, sourceId, levelDelta, maxLevel);
|
|
71
71
|
const v = validateGoalTree(clone);
|
|
72
72
|
if (!v.valid)
|
|
73
73
|
return refusalError(v.errors[0]?.message ?? 'Validation failed after reparent');
|
|
@@ -26,4 +26,9 @@ export interface CanonicalGoalsResult extends ValidationResult {
|
|
|
26
26
|
/** On success, the internal-form GoalTree derived from the canonical input. */
|
|
27
27
|
parsed?: GoalTree;
|
|
28
28
|
}
|
|
29
|
+
/** Display id for previews — canonical `GOAL-…` when present, else internal numeric id. */
|
|
30
|
+
export declare function displayGoalId(goal: {
|
|
31
|
+
id: number;
|
|
32
|
+
canonical_id?: string;
|
|
33
|
+
}): string;
|
|
29
34
|
export declare function parseCanonicalGoals(input: unknown): CanonicalGoalsResult;
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
*/
|
|
23
23
|
const GOAL_ID_RE = /^GOAL(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
24
24
|
const GOALS_DOC_ID_RE = /^GOALS(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
25
|
+
/** Display id for previews — canonical `GOAL-…` when present, else internal numeric id. */
|
|
26
|
+
export function displayGoalId(goal) {
|
|
27
|
+
return goal.canonical_id ?? String(goal.id);
|
|
28
|
+
}
|
|
25
29
|
function isNonEmptyString(v) {
|
|
26
30
|
return typeof v === 'string' && v.trim().length > 0;
|
|
27
31
|
}
|
|
@@ -240,6 +244,7 @@ export function parseCanonicalGoals(input) {
|
|
|
240
244
|
const parentId = isNonEmptyString(parent) && idToInternal.has(parent) ? idToInternal.get(parent) : 0;
|
|
241
245
|
return {
|
|
242
246
|
id: idToInternal.get(o['id']),
|
|
247
|
+
canonical_id: o['id'],
|
|
243
248
|
name: o['name'],
|
|
244
249
|
type: o['type'],
|
|
245
250
|
level: o['level'],
|
package/dist/goals/types.d.ts
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block/cell size presets for diagram renderers.
|
|
3
|
+
*
|
|
4
|
+
* Presets drive both layout geometry and shared text-layout char budgets.
|
|
5
|
+
* Smooth per-pixel sliders are intentionally deferred.
|
|
6
|
+
*/
|
|
7
|
+
export type NodeSizePreset = 'compact' | 'normal' | 'wide';
|
|
8
|
+
export declare const NODE_SIZE_PRESET_VALUES: readonly NodeSizePreset[];
|
|
9
|
+
export interface BoxDimensions {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}
|
|
13
|
+
export interface BlocksLeafDimensions extends BoxDimensions {
|
|
14
|
+
}
|
|
15
|
+
export interface ProcessBlueprintDimensions {
|
|
16
|
+
legendColumnWidth: number;
|
|
17
|
+
stageColumnWidth: number;
|
|
18
|
+
stageHeaderHeight: number;
|
|
19
|
+
goalRowHeight: number;
|
|
20
|
+
resultRowHeight: number;
|
|
21
|
+
aspectRowMinHeight: number;
|
|
22
|
+
pillHeight: number;
|
|
23
|
+
pillGap: number;
|
|
24
|
+
cellPadding: number;
|
|
25
|
+
textLineHeight: number;
|
|
26
|
+
textCharWidth: number;
|
|
27
|
+
cellTextPadX: number;
|
|
28
|
+
cellTextPadY: number;
|
|
29
|
+
}
|
|
30
|
+
export interface CapabilityMapDimensions {
|
|
31
|
+
nodeWidth: number;
|
|
32
|
+
nodeHeight: number;
|
|
33
|
+
}
|
|
34
|
+
type PresetTable<T> = Record<NodeSizePreset, T>;
|
|
35
|
+
export declare const GOALS_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
36
|
+
export declare const DGCA_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
37
|
+
export declare const BLOCKS_LEAF_SIZE: PresetTable<BlocksLeafDimensions>;
|
|
38
|
+
export declare const ACTION_NODE_SIZE: PresetTable<BoxDimensions>;
|
|
39
|
+
export declare const PROCESS_BLUEPRINT_SIZE: PresetTable<ProcessBlueprintDimensions>;
|
|
40
|
+
export declare const CAPABILITY_MAP_NODE_SIZE: PresetTable<CapabilityMapDimensions>;
|
|
41
|
+
export type NodeSizeNotation = 'goals' | 'dgca' | 'dga' | 'blocks' | 'action' | 'processBlueprint' | 'capabilityMap';
|
|
42
|
+
/** Parse a VS Code setting value into a preset id (defaults to `normal`). */
|
|
43
|
+
export declare function parseNodeSizePreset(value: string | undefined): NodeSizePreset;
|
|
44
|
+
export declare function resolveGoalsNodeSize(preset: NodeSizePreset): BoxDimensions;
|
|
45
|
+
export declare function resolveDgcaNodeSize(preset: NodeSizePreset): BoxDimensions;
|
|
46
|
+
export declare function resolveBlocksLeafSize(preset: NodeSizePreset): BlocksLeafDimensions;
|
|
47
|
+
export declare function resolveActionNodeSize(preset: NodeSizePreset): BoxDimensions;
|
|
48
|
+
export declare function resolveProcessBlueprintSize(preset: NodeSizePreset): ProcessBlueprintDimensions;
|
|
49
|
+
export declare function resolveCapabilityMapNodeSize(preset: NodeSizePreset): CapabilityMapDimensions;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block/cell size presets for diagram renderers.
|
|
3
|
+
*
|
|
4
|
+
* Presets drive both layout geometry and shared text-layout char budgets.
|
|
5
|
+
* Smooth per-pixel sliders are intentionally deferred.
|
|
6
|
+
*/
|
|
7
|
+
export const NODE_SIZE_PRESET_VALUES = ['compact', 'normal', 'wide'];
|
|
8
|
+
export const GOALS_NODE_SIZE = {
|
|
9
|
+
compact: { width: 200, height: 72 },
|
|
10
|
+
normal: { width: 250, height: 72 },
|
|
11
|
+
wide: { width: 320, height: 72 },
|
|
12
|
+
};
|
|
13
|
+
export const DGCA_NODE_SIZE = {
|
|
14
|
+
compact: { width: 200, height: 72 },
|
|
15
|
+
normal: { width: 220, height: 72 },
|
|
16
|
+
wide: { width: 280, height: 72 },
|
|
17
|
+
};
|
|
18
|
+
export const BLOCKS_LEAF_SIZE = {
|
|
19
|
+
compact: { width: 140, height: 72 },
|
|
20
|
+
normal: { width: 160, height: 72 },
|
|
21
|
+
wide: { width: 200, height: 72 },
|
|
22
|
+
};
|
|
23
|
+
export const ACTION_NODE_SIZE = {
|
|
24
|
+
compact: { width: 180, height: 72 },
|
|
25
|
+
normal: { width: 200, height: 80 },
|
|
26
|
+
wide: { width: 260, height: 80 },
|
|
27
|
+
};
|
|
28
|
+
export const PROCESS_BLUEPRINT_SIZE = {
|
|
29
|
+
compact: {
|
|
30
|
+
legendColumnWidth: 120,
|
|
31
|
+
stageColumnWidth: 180,
|
|
32
|
+
stageHeaderHeight: 36,
|
|
33
|
+
goalRowHeight: 52,
|
|
34
|
+
resultRowHeight: 52,
|
|
35
|
+
aspectRowMinHeight: 56,
|
|
36
|
+
pillHeight: 36,
|
|
37
|
+
pillGap: 4,
|
|
38
|
+
cellPadding: 8,
|
|
39
|
+
textLineHeight: 16,
|
|
40
|
+
textCharWidth: 7.5,
|
|
41
|
+
cellTextPadX: 10,
|
|
42
|
+
cellTextPadY: 10,
|
|
43
|
+
},
|
|
44
|
+
normal: {
|
|
45
|
+
legendColumnWidth: 140,
|
|
46
|
+
stageColumnWidth: 220,
|
|
47
|
+
stageHeaderHeight: 40,
|
|
48
|
+
goalRowHeight: 56,
|
|
49
|
+
resultRowHeight: 56,
|
|
50
|
+
aspectRowMinHeight: 60,
|
|
51
|
+
pillHeight: 40,
|
|
52
|
+
pillGap: 4,
|
|
53
|
+
cellPadding: 8,
|
|
54
|
+
textLineHeight: 17,
|
|
55
|
+
textCharWidth: 7.5,
|
|
56
|
+
cellTextPadX: 10,
|
|
57
|
+
cellTextPadY: 10,
|
|
58
|
+
},
|
|
59
|
+
wide: {
|
|
60
|
+
legendColumnWidth: 160,
|
|
61
|
+
stageColumnWidth: 280,
|
|
62
|
+
stageHeaderHeight: 44,
|
|
63
|
+
goalRowHeight: 60,
|
|
64
|
+
resultRowHeight: 60,
|
|
65
|
+
aspectRowMinHeight: 64,
|
|
66
|
+
pillHeight: 44,
|
|
67
|
+
pillGap: 4,
|
|
68
|
+
cellPadding: 8,
|
|
69
|
+
textLineHeight: 18,
|
|
70
|
+
textCharWidth: 7.5,
|
|
71
|
+
cellTextPadX: 10,
|
|
72
|
+
cellTextPadY: 10,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
export const CAPABILITY_MAP_NODE_SIZE = {
|
|
76
|
+
compact: { nodeWidth: 200, nodeHeight: 56 },
|
|
77
|
+
normal: { nodeWidth: 240, nodeHeight: 60 },
|
|
78
|
+
wide: { nodeWidth: 300, nodeHeight: 64 },
|
|
79
|
+
};
|
|
80
|
+
/** Parse a VS Code setting value into a preset id (defaults to `normal`). */
|
|
81
|
+
export function parseNodeSizePreset(value) {
|
|
82
|
+
if (value === 'compact' || value === 'wide')
|
|
83
|
+
return value;
|
|
84
|
+
return 'normal';
|
|
85
|
+
}
|
|
86
|
+
export function resolveGoalsNodeSize(preset) {
|
|
87
|
+
return GOALS_NODE_SIZE[preset];
|
|
88
|
+
}
|
|
89
|
+
export function resolveDgcaNodeSize(preset) {
|
|
90
|
+
return DGCA_NODE_SIZE[preset];
|
|
91
|
+
}
|
|
92
|
+
export function resolveBlocksLeafSize(preset) {
|
|
93
|
+
return BLOCKS_LEAF_SIZE[preset];
|
|
94
|
+
}
|
|
95
|
+
export function resolveActionNodeSize(preset) {
|
|
96
|
+
return ACTION_NODE_SIZE[preset];
|
|
97
|
+
}
|
|
98
|
+
export function resolveProcessBlueprintSize(preset) {
|
|
99
|
+
return PROCESS_BLUEPRINT_SIZE[preset];
|
|
100
|
+
}
|
|
101
|
+
export function resolveCapabilityMapNodeSize(preset) {
|
|
102
|
+
return CAPABILITY_MAP_NODE_SIZE[preset];
|
|
103
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapTextLines } from '../webview/entity-text-layout.js';
|
|
1
2
|
const ASPECT_CATEGORIES = ['systems', 'actors', 'equipment', 'information_entities'];
|
|
2
3
|
const ASPECT_LABEL = {
|
|
3
4
|
systems: 'Systems',
|
|
@@ -30,55 +31,11 @@ function resolveOptions(options) {
|
|
|
30
31
|
return { ...DEFAULTS, ...(options ?? {}) };
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
|
-
* Greedy word-wrap to a max characters-per-line budget.
|
|
34
|
-
*
|
|
35
|
-
* is truncated with an ellipsis so a single pathological cell can't grow the
|
|
36
|
-
* whole row unbounded.
|
|
34
|
+
* Greedy word-wrap to a max characters-per-line budget. Delegates to the shared
|
|
35
|
+
* shared text-layout module.
|
|
37
36
|
*/
|
|
38
37
|
function wrapText(text, maxChars, maxLines) {
|
|
39
|
-
|
|
40
|
-
if (trimmed.length === 0)
|
|
41
|
-
return [];
|
|
42
|
-
const budget = Math.max(1, maxChars);
|
|
43
|
-
const words = trimmed.split(/\s+/);
|
|
44
|
-
const lines = [];
|
|
45
|
-
let cur = '';
|
|
46
|
-
const flush = () => {
|
|
47
|
-
if (cur.length > 0) {
|
|
48
|
-
lines.push(cur);
|
|
49
|
-
cur = '';
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
for (const word of words) {
|
|
53
|
-
if (word.length > budget) {
|
|
54
|
-
// Token wider than the cell: hard-break it across lines.
|
|
55
|
-
flush();
|
|
56
|
-
let rest = word;
|
|
57
|
-
while (rest.length > budget) {
|
|
58
|
-
lines.push(rest.slice(0, budget));
|
|
59
|
-
rest = rest.slice(budget);
|
|
60
|
-
}
|
|
61
|
-
cur = rest;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const candidate = cur.length > 0 ? `${cur} ${word}` : word;
|
|
65
|
-
if (candidate.length > budget) {
|
|
66
|
-
flush();
|
|
67
|
-
cur = word;
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
cur = candidate;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
flush();
|
|
74
|
-
if (lines.length > maxLines) {
|
|
75
|
-
const capped = lines.slice(0, maxLines);
|
|
76
|
-
const last = capped[maxLines - 1];
|
|
77
|
-
const clipped = last.length > budget - 1 ? last.slice(0, budget - 1) : last;
|
|
78
|
-
capped[maxLines - 1] = `${clipped.replace(/…$/, '')}…`;
|
|
79
|
-
return capped;
|
|
80
|
-
}
|
|
81
|
-
return lines;
|
|
38
|
+
return wrapTextLines(text, maxChars, maxLines);
|
|
82
39
|
}
|
|
83
40
|
function buildStageIndex(stages) {
|
|
84
41
|
const idx = new Map();
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
// referential integrity, atomicity, ID uniqueness, policy — the checks that the
|
|
5
5
|
// methodology's Python `.validators/lint.py` owns today. This converges those
|
|
6
6
|
// checks onto the shared TypeScript model so there is a single validation
|
|
7
|
-
// runtime (methodology ADR
|
|
8
|
-
//
|
|
9
|
-
// `docs/decisions/2026-06-11-validation-runtime-convergence.md`).
|
|
7
|
+
// runtime (methodology ADR "Validation Two-Axis Model"; Studio decision
|
|
8
|
+
// "Validation Runtime Convergence").
|
|
10
9
|
//
|
|
11
10
|
// The finding shape is deliberately minimal — `{ scope, id, message }`. The
|
|
12
11
|
// richer `target` / `category` reporting taxonomy is explicitly DEFERRED per the
|