@transitrix/diagrams 1.6.0 → 1.7.6
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/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/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/repo-validate/types.js +2 -3
- package/dist/webview/__tests__/render-goals.test.js +17 -3
- package/dist/webview/render-goals.js +2 -1
- package/package.json +1 -1
- 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/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/repo-validate/types.ts +2 -3
- package/src/webview/__tests__/render-goals.test.ts +17 -3
- package/src/webview/render-goals.ts +2 -1
|
@@ -297,6 +297,54 @@ describe('GoalTreeView', () => {
|
|
|
297
297
|
});
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
+
it('fires onFactorClick with the clicked factor when a popover item is clicked', async () => {
|
|
301
|
+
const onFactorClick = vi.fn();
|
|
302
|
+
render(<GoalTreeView tree={makeTree()} onFactorClick={onFactorClick} />);
|
|
303
|
+
await waitFor(() => {
|
|
304
|
+
expect(screen.getByText('Cut churn')).toBeTruthy();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Open the negative-factor panel on "Cut churn" (id=3, has risk factor "Support backlog")
|
|
308
|
+
const negativeBadge = screen.getByLabelText('1 negative factor');
|
|
309
|
+
negativeBadge.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
310
|
+
|
|
311
|
+
// The popover item for the risk factor should now be in the DOM
|
|
312
|
+
const factorItem = await waitFor(() => screen.getByText('Support backlog'));
|
|
313
|
+
factorItem.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
314
|
+
|
|
315
|
+
await waitFor(() => expect(onFactorClick).toHaveBeenCalledTimes(1));
|
|
316
|
+
expect(onFactorClick.mock.calls[0][0]).toMatchObject({ id: 10, name: 'Support backlog', impact_type: 'risk' });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('backlog panel is collapsible — toggle hides and restores content', async () => {
|
|
320
|
+
const tree: GoalTree = {
|
|
321
|
+
goal_types: [
|
|
322
|
+
{ name: 'Strategy', level: 0 },
|
|
323
|
+
{ name: 'Business Goal', level: 1 },
|
|
324
|
+
],
|
|
325
|
+
goals: [
|
|
326
|
+
{ id: 1, name: 'Root goal', type: 'Strategy', level: 0, parent_id: 0 },
|
|
327
|
+
{ id: 2, name: 'Orphaned goal', type: 'Business Goal', level: 1, parent_id: 999 },
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
render(<GoalTreeView tree={tree} showBacklog />);
|
|
331
|
+
await waitFor(() => {
|
|
332
|
+
expect(screen.getByText('Backlog')).toBeTruthy();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Collapse
|
|
336
|
+
screen.getByLabelText('Collapse backlog').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
337
|
+
await waitFor(() => {
|
|
338
|
+
expect(screen.queryByText('Backlog')).toBeNull();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Expand again
|
|
342
|
+
screen.getByLabelText('Expand backlog').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
343
|
+
await waitFor(() => {
|
|
344
|
+
expect(screen.getByText('Backlog')).toBeTruthy();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
300
348
|
it('fires onEditRequest on double-click', async () => {
|
|
301
349
|
const onEditRequest = vi.fn();
|
|
302
350
|
render(<GoalTreeView tree={makeTree()} onEditRequest={onEditRequest} />);
|
|
@@ -109,3 +109,45 @@ describe('restoreFromBacklog', () => {
|
|
|
109
109
|
expect(r.result!.goals.find(g => g.id === 3)!.parent_id).toBe(1);
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
describe('reparent — descendant type relabeling and level cap', () => {
|
|
114
|
+
const DEEP: GoalTree = {
|
|
115
|
+
goal_types: [
|
|
116
|
+
{ name: 'Strategy', level: 0 },
|
|
117
|
+
{ name: 'Business Goal', level: 1 },
|
|
118
|
+
{ name: 'Project', level: 2 },
|
|
119
|
+
{ name: 'Task', level: 3 },
|
|
120
|
+
],
|
|
121
|
+
goals: [
|
|
122
|
+
{ id: 1, name: 'Root', type: 'Strategy', level: 0, parent_id: 0 },
|
|
123
|
+
{ id: 2, name: 'BG1', type: 'Business Goal', level: 1, parent_id: 1 },
|
|
124
|
+
{ id: 3, name: 'Proj1', type: 'Project', level: 2, parent_id: 2 },
|
|
125
|
+
{ id: 4, name: 'Task1', type: 'Task', level: 3, parent_id: 3 },
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
it('relabels descendant types via getTypeForLevel when reparenting shifts levels', () => {
|
|
130
|
+
// Move Proj1 (id=3, level=2) directly under Root (id=1, level=0); delta = -1
|
|
131
|
+
// Task1 (child of Proj1, level=3): new level = 2, type should be 'Project'
|
|
132
|
+
const r = reparent(DEEP, 3, 1);
|
|
133
|
+
expect(r.ok).toBe(true);
|
|
134
|
+
const proj = r.result!.goals.find(g => g.id === 3)!;
|
|
135
|
+
const task = r.result!.goals.find(g => g.id === 4)!;
|
|
136
|
+
expect(proj.level).toBe(1);
|
|
137
|
+
expect(proj.type).toBe('Business Goal');
|
|
138
|
+
expect(task.level).toBe(2);
|
|
139
|
+
expect(task.type).toBe('Project');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('caps descendant level at maxLevel and refuses when that violates GOALS-012', () => {
|
|
143
|
+
// Move Root (id=1, level=0) under Root2 (id=5, level=0); delta = +1
|
|
144
|
+
// BG1: 1→2, Proj1: 2→3 (maxLevel), Task1: 3→4, capped at 3
|
|
145
|
+
// Task1 (capped level=3) under Proj1 (level=3) violates GOALS-012 → refused
|
|
146
|
+
const tree: GoalTree = {
|
|
147
|
+
...DEEP,
|
|
148
|
+
goals: [...DEEP.goals, { id: 5, name: 'Root2', type: 'Strategy', level: 0, parent_id: 0 }],
|
|
149
|
+
};
|
|
150
|
+
const r = reparent(tree, 1, 5);
|
|
151
|
+
expect(r.ok).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -122,8 +122,10 @@ describe('parseCanonicalGoals', () => {
|
|
|
122
122
|
it('maps canonical IDs to internal numeric ids; parent_id is the parent\'s mapped id', () => {
|
|
123
123
|
const r = parseCanonicalGoals(VALID);
|
|
124
124
|
expect(r.parsed?.goals[0].id).toBe(1);
|
|
125
|
+
expect(r.parsed?.goals[0].canonical_id).toBe('GOAL-1');
|
|
125
126
|
expect(r.parsed?.goals[0].parent_id).toBe(0);
|
|
126
127
|
expect(r.parsed?.goals[1].id).toBe(2);
|
|
128
|
+
expect(r.parsed?.goals[1].canonical_id).toBe('GOAL-2');
|
|
127
129
|
expect(r.parsed?.goals[1].parent_id).toBe(1);
|
|
128
130
|
});
|
|
129
131
|
});
|
package/src/goals/layout.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Goal, GoalTree, LayoutOptions, GoalTreeLayout, LaidOutNode, LaidOutEdge } from './types.js';
|
|
2
2
|
import type { Scope } from '../scope.js';
|
|
3
|
+
import { displayGoalId } from './parse-canonical.js';
|
|
3
4
|
|
|
4
5
|
const DEFAULT_NODE_WIDTH = 250;
|
|
5
6
|
const DEFAULT_NODE_HEIGHT = 80;
|
|
@@ -23,7 +24,7 @@ export function selectScopedGoals(goals: Goal[], scope: Scope): Goal[] {
|
|
|
23
24
|
if (!children.has(g.parent_id)) children.set(g.parent_id, []);
|
|
24
25
|
children.get(g.parent_id)!.push(g.id);
|
|
25
26
|
}
|
|
26
|
-
const root = goals.find(g =>
|
|
27
|
+
const root = goals.find(g => displayGoalId(g) === scope.rootGoalId);
|
|
27
28
|
if (!root) return [];
|
|
28
29
|
const keep = new Set<number>();
|
|
29
30
|
const stack = [root.id];
|
package/src/goals/mutations.ts
CHANGED
|
@@ -22,12 +22,12 @@ function getTypeForLevel(tree: GoalTree, level: number): string {
|
|
|
22
22
|
return gt?.name ?? '';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function updateDescendantLevels(
|
|
26
|
-
for (const g of goals) {
|
|
25
|
+
function updateDescendantLevels(clone: GoalTree, id: number, levelDelta: number, maxLevel: number): void {
|
|
26
|
+
for (const g of clone.goals) {
|
|
27
27
|
if (g.parent_id === id) {
|
|
28
|
-
g.level
|
|
29
|
-
g.type =
|
|
30
|
-
updateDescendantLevels(
|
|
28
|
+
g.level = Math.min(g.level + levelDelta, maxLevel);
|
|
29
|
+
g.type = getTypeForLevel(clone, g.level);
|
|
30
|
+
updateDescendantLevels(clone, g.id, levelDelta, maxLevel);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -70,7 +70,7 @@ export function reparent(tree: GoalTree, sourceId: number, targetId: number): Mu
|
|
|
70
70
|
source.parent_id = targetId;
|
|
71
71
|
source.level = newLevel;
|
|
72
72
|
source.type = getTypeForLevel(clone, newLevel);
|
|
73
|
-
if (levelDelta !== 0) updateDescendantLevels(clone
|
|
73
|
+
if (levelDelta !== 0) updateDescendantLevels(clone, sourceId, levelDelta, maxLevel);
|
|
74
74
|
|
|
75
75
|
const v = validateGoalTree(clone);
|
|
76
76
|
if (!v.valid) return refusalError(v.errors[0]?.message ?? 'Validation failed after reparent');
|
|
@@ -36,6 +36,11 @@ export interface CanonicalGoalsResult extends ValidationResult {
|
|
|
36
36
|
const GOAL_ID_RE = /^GOAL(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
37
37
|
const GOALS_DOC_ID_RE = /^GOALS(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
38
38
|
|
|
39
|
+
/** Display id for previews — canonical `GOAL-…` when present, else internal numeric id. */
|
|
40
|
+
export function displayGoalId(goal: { id: number; canonical_id?: string }): string {
|
|
41
|
+
return goal.canonical_id ?? String(goal.id);
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
function isNonEmptyString(v: unknown): v is string {
|
|
40
45
|
return typeof v === 'string' && v.trim().length > 0;
|
|
41
46
|
}
|
|
@@ -267,6 +272,7 @@ export function parseCanonicalGoals(input: unknown): CanonicalGoalsResult {
|
|
|
267
272
|
isNonEmptyString(parent) && idToInternal.has(parent) ? idToInternal.get(parent)! : 0;
|
|
268
273
|
return {
|
|
269
274
|
id: idToInternal.get(o['id'] as string)!,
|
|
275
|
+
canonical_id: o['id'] as string,
|
|
270
276
|
name: o['name'] as string,
|
|
271
277
|
type: o['type'] as string,
|
|
272
278
|
level: o['level'] as number,
|
package/src/goals/types.ts
CHANGED
|
@@ -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
|
|
@@ -65,8 +65,22 @@ describe('renderGoalsSvg', () => {
|
|
|
65
65
|
expect(svg).toContain('height="0"');
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('
|
|
69
|
-
const
|
|
70
|
-
|
|
68
|
+
it('shows canonical goal ids in node labels when present', () => {
|
|
69
|
+
const tree: GoalTree = {
|
|
70
|
+
goal_types: [{ name: 'Strategy', level: 0 }],
|
|
71
|
+
goals: [
|
|
72
|
+
{
|
|
73
|
+
id: 1,
|
|
74
|
+
canonical_id: 'GOAL-REVENUE-1',
|
|
75
|
+
name: 'Triple revenue',
|
|
76
|
+
type: 'Strategy',
|
|
77
|
+
level: 0,
|
|
78
|
+
parent_id: 0,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
const svg = renderGoalsSvg(tree);
|
|
83
|
+
expect(svg).toContain('GOAL-REVENUE-1');
|
|
84
|
+
expect(svg).not.toMatch(/>\s*1\s*</);
|
|
71
85
|
});
|
|
72
86
|
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { layoutGoalTree } from '../goals/layout.js';
|
|
14
14
|
import type { GoalTree, GoalTreeLayout, LaidOutEdge } from '../goals/types.js';
|
|
15
|
+
import { displayGoalId } from '../goals/parse-canonical.js';
|
|
15
16
|
import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
16
17
|
import { generateSvgEmbedCss, type ThemeId } from '../theme/index.js';
|
|
17
18
|
import { escXml } from './render-util.js';
|
|
@@ -129,7 +130,7 @@ export function renderGoalsLayoutSvg(layout: GoalTreeLayout, options: RenderGoal
|
|
|
129
130
|
const typeY = twoLines ? y + 43 : y + 35;
|
|
130
131
|
const idY = twoLines ? y + 59 : y + 55;
|
|
131
132
|
const typeLabel = n.data.type ?? '';
|
|
132
|
-
const idText =
|
|
133
|
+
const idText = displayGoalId(n.data);
|
|
133
134
|
return `<g>
|
|
134
135
|
<rect class="diagram-node level-${level}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="${ENTITY_NODE_RX}"/>
|
|
135
136
|
<text class="text-primary" x="${x + n.width / 2}" y="${nameY1}" text-anchor="middle" dominant-baseline="central">${escXml(line1)}</text>${twoLines ? `
|