@transitrix/diagrams 1.8.13 → 1.8.19

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.
Files changed (45) hide show
  1. package/dist/activities/validate.js +9 -1
  2. package/dist/blocks/index.d.ts +5 -4
  3. package/dist/blocks/index.js +3 -2
  4. package/dist/blocks/layout.d.ts +8 -1
  5. package/dist/blocks/layout.js +57 -0
  6. package/dist/blocks/templates/index.d.ts +9 -0
  7. package/dist/blocks/templates/index.js +11 -0
  8. package/dist/blocks/templates/raci.d.ts +9 -0
  9. package/dist/blocks/templates/raci.js +24 -0
  10. package/dist/blocks/types.d.ts +58 -0
  11. package/dist/blocks/validate.d.ts +31 -1
  12. package/dist/blocks/validate.js +127 -0
  13. package/dist/capability-map/validate.js +41 -0
  14. package/dist/edge-path.d.ts +15 -0
  15. package/dist/edge-path.js +19 -0
  16. package/dist/fgca/parse-canonical.js +8 -0
  17. package/dist/repo-validate/check-element-hygiene.d.ts +8 -0
  18. package/dist/repo-validate/check-element-hygiene.js +211 -0
  19. package/dist/repo-validate/check-strategy-chain.d.ts +8 -0
  20. package/dist/repo-validate/check-strategy-chain.js +491 -0
  21. package/dist/repo-validate/types.d.ts +14 -1
  22. package/dist/repo-validate/types.js +22 -3
  23. package/dist/repo-validate/validate-repo.d.ts +3 -2
  24. package/dist/repo-validate/validate-repo.js +16 -4
  25. package/dist/webview/render-activities.d.ts +9 -0
  26. package/dist/webview/render-activities.js +72 -4
  27. package/dist/webview/render-blocks.d.ts +23 -1
  28. package/dist/webview/render-blocks.js +100 -2
  29. package/package.json +1 -1
  30. package/src/activities/validate.ts +9 -1
  31. package/src/blocks/index.ts +16 -2
  32. package/src/blocks/layout.ts +70 -0
  33. package/src/blocks/templates/index.ts +13 -0
  34. package/src/blocks/templates/raci.ts +28 -0
  35. package/src/blocks/types.ts +67 -0
  36. package/src/blocks/validate.ts +157 -1
  37. package/src/capability-map/validate.ts +40 -0
  38. package/src/edge-path.ts +27 -0
  39. package/src/fgca/parse-canonical.ts +8 -0
  40. package/src/repo-validate/check-element-hygiene.ts +236 -0
  41. package/src/repo-validate/check-strategy-chain.ts +528 -0
  42. package/src/repo-validate/types.ts +36 -4
  43. package/src/repo-validate/validate-repo.ts +16 -4
  44. package/src/webview/render-activities.ts +76 -4
  45. package/src/webview/render-blocks.ts +159 -2
@@ -23,7 +23,7 @@ import type {
23
23
  ActivitiesLayout,
24
24
  ActivitiesLayoutOptions,
25
25
  } from '../activities/types.js';
26
- import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
26
+ import { horizontalCubicEdgePath, bowedCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
27
27
  import { parseNodeSizePreset, resolveActionNodeSize, type NodeSizePreset } from '../node-size-presets.js';
28
28
  import { generateSvgEmbedCss } from '../theme/index.js';
29
29
  import { emitCenteredTextSvg, layoutCenteredEntityText } from './entity-text-layout.js';
@@ -55,6 +55,72 @@ export const ACTIVITIES_NETWORK_DEFS = `<defs>
55
55
  </marker>
56
56
  </defs>`;
57
57
 
58
+ type ActivityLayoutNode = ActivitiesLayout['nodes'][number];
59
+
60
+ /**
61
+ * Nodes horizontally between `source` and `target` (a "skip" edge passes over
62
+ * their column) whose vertical span overlaps the edge's Y band. A
63
+ * straight-tangent cubic (`horizontalCubicEdgePath`) stays within
64
+ * [min(sourceCenterY,targetCenterY), max(...)] by convex hull, so any such
65
+ * node sits directly on the edge's path — the "arrow goes straight through
66
+ * the node" bug reported against a linear PSND chain where a multi-predecessor
67
+ * activity's nearer predecessor shares a row with an intermediate column.
68
+ */
69
+ function blockingNodes(
70
+ nodes: ActivityLayoutNode[],
71
+ source: ActivityLayoutNode,
72
+ target: ActivityLayoutNode,
73
+ ): ActivityLayoutNode[] {
74
+ const sCenterY = source.y + source.height / 2;
75
+ const tCenterY = target.y + target.height / 2;
76
+ const loY = Math.min(sCenterY, tCenterY);
77
+ const hiY = Math.max(sCenterY, tCenterY);
78
+ const sRight = source.x + source.width;
79
+ const tLeft = target.x;
80
+ return nodes.filter((n) => {
81
+ if (n.id === source.id || n.id === target.id) return false;
82
+ if (n.x + n.width <= sRight || n.x >= tLeft) return false; // not between the two columns
83
+ return n.y < hiY && n.y + n.height > loY; // vertical overlap with the edge's band
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Pre-offset bow-control-point Y for a skip edge arcing over `blockers`: one
89
+ * full node-height of clearance above the topmost blocker's top edge. A
90
+ * cubic's apex only reaches part-way toward its control point (see
91
+ * `bowedCubicEdgePath` doc), so this generous margin is what actually keeps
92
+ * the visible curve clear of the box — verified numerically in
93
+ * `render-activities.test.ts`.
94
+ */
95
+ function bowY(blockers: ActivityLayoutNode[]): number {
96
+ const topY = Math.min(...blockers.map((n) => n.y));
97
+ const clearance = Math.max(...blockers.map((n) => n.height));
98
+ return topY - clearance;
99
+ }
100
+
101
+ /**
102
+ * Extra top padding (px) a network-view canvas must add so skip-edge bows
103
+ * (see `blockingNodes`/`bowY` above) never draw above the SVG's y=0 and get
104
+ * clipped. `oyBase` is the canvas's Y offset *before* this padding — the
105
+ * historical `-bounds.y + N_PAD + titleH` both hosts compute — since a bow
106
+ * near the topmost row can reach above the diagram's own node bounds.
107
+ * Callers add the returned value to both `oyBase` and the canvas height.
108
+ */
109
+ export function computeNetworkTopPad(layout: ActivitiesLayout, oyBase: number): number {
110
+ const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
111
+ let pad = 0;
112
+ for (const e of layout.edges) {
113
+ const s = nodeMap.get(e.sourceId);
114
+ const t = nodeMap.get(e.targetId);
115
+ if (!s || !t) continue;
116
+ const blockers = blockingNodes(layout.nodes, s, t);
117
+ if (blockers.length === 0) continue;
118
+ const shortfall = -(oyBase + bowY(blockers));
119
+ if (shortfall > pad) pad = shortfall;
120
+ }
121
+ return pad;
122
+ }
123
+
58
124
  function activityNodeSvg(
59
125
  n: ActivitiesLayout['nodes'][number],
60
126
  x: number,
@@ -119,7 +185,11 @@ export function renderActivitiesNetworkBody(
119
185
  const ty = t.y + oy + t.height / 2;
120
186
  const cls = e.isCritical ? 'diagram-edge critical-edge' : 'diagram-edge';
121
187
  const marker = `url(#${e.isCritical ? 'arrow-crit' : 'arrow'})`;
122
- return `<path d="${horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature)}" class="${cls}" marker-end="${marker}"/>`;
188
+ const blockers = blockingNodes(layout.nodes, s, t);
189
+ const d = blockers.length > 0
190
+ ? bowedCubicEdgePath(sx, sy, tx, ty, bowY(blockers) + oy, curvature)
191
+ : horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature);
192
+ return `<path d="${d}" class="${cls}" marker-end="${marker}"/>`;
123
193
  })
124
194
  .join('\n');
125
195
 
@@ -207,10 +277,12 @@ export function renderActivitiesSvg(doc: ActivityDoc, options: RenderActivitiesO
207
277
 
208
278
  const cpm = computeCpm(renderDoc.activities ?? []);
209
279
  const titleH = title ? 24 : 0;
280
+ const oyBase = -layout.bounds.y + N_PAD + titleH;
281
+ const topPad = computeNetworkTopPad(layout, oyBase);
210
282
  const w = layout.bounds.width + N_PAD * 2;
211
- const h = layout.bounds.height + N_PAD * 2 + titleH;
283
+ const h = layout.bounds.height + N_PAD * 2 + titleH + topPad;
212
284
  const ox = -layout.bounds.x + N_PAD;
213
- const oy = -layout.bounds.y + N_PAD + titleH;
285
+ const oy = oyBase + topPad;
214
286
 
215
287
  const body = renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entryCurvature);
216
288
 
@@ -12,14 +12,25 @@
12
12
  * the shared theme CSS embedded in a `<style>` element and a simple optional
13
13
  * title `<text>`.
14
14
  */
15
- import { layoutNestedBlocks } from '../blocks/layout.js';
16
- import type { BlocksFile, BlocksLayout, BlocksLayoutOptions, LaidOutBlock } from '../blocks/types.js';
15
+ import { layoutNestedBlocks, layoutGrid } from '../blocks/layout.js';
16
+ import type {
17
+ BlocksFile,
18
+ BlocksLayout,
19
+ BlocksLayoutOptions,
20
+ GridFile,
21
+ GridLayout,
22
+ GridLayoutOptions,
23
+ LaidOutBlock,
24
+ } from '../blocks/types.js';
17
25
  import { parseNodeSizePreset, resolveBlocksLeafSize, type NodeSizePreset } from '../node-size-presets.js';
18
26
  import { generateSvgEmbedCss, type ThemeId } from '../theme/index.js';
19
27
  import {
28
+ CHAR_W_PRIMARY,
20
29
  emitCenteredTextSvg,
21
30
  layoutHeaderBlockText,
22
31
  layoutLeafBlockText,
32
+ maxCharsForInnerWidth,
33
+ wrapWords,
23
34
  } from './entity-text-layout.js';
24
35
  import { escXml } from './render-util.js';
25
36
  import { ENTITY_NODE_RX } from './notation-style.js';
@@ -141,3 +152,149 @@ export function renderBlocksSvg(doc: BlocksFile, options: RenderBlocksOptions =
141
152
 
142
153
  return renderBlocksLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
143
154
  }
155
+
156
+ /**
157
+ * Matrix subset (08-blocks.md §4a) — first cut: a single-layer rectangular
158
+ * `grid:` document (the RACI-style matrix case) rendered as a plain
159
+ * HTML/CSS-table-like SVG grid. Layers, arbitrary
160
+ * (non-rectangular) cell sets, and nested sub-grids belong to the general
161
+ * layered-grid superset, which is still in design upstream (methodology) and
162
+ * out of scope here — see 08-blocks.md §4a / §8.
163
+ */
164
+ const GRID_CELL_PAD_X = 8;
165
+ const GRID_LINE_HEIGHT = 15;
166
+ const GRID_EMBED_CSS = `
167
+ .blocks-grid-border { fill: none; stroke: var(--ts-border, #cbd5e1); }`;
168
+
169
+ function centeredGridCellTextSvg(
170
+ lines: string[],
171
+ cls: string,
172
+ cx: number,
173
+ cellTop: number,
174
+ cellHeight: number,
175
+ ): string {
176
+ const ls = lines.length > 0 ? lines : [''];
177
+ const firstY = cellTop + cellHeight / 2 - ((ls.length - 1) / 2) * GRID_LINE_HEIGHT;
178
+ const tspans = ls
179
+ .map((ln, i) => `<tspan x="${cx}" y="${firstY + i * GRID_LINE_HEIGHT}">${escXml(ln)}</tspan>`)
180
+ .join('');
181
+ return `<text class="${cls}" text-anchor="middle" dominant-baseline="central">${tspans}</text>`;
182
+ }
183
+
184
+ /**
185
+ * Emit the grid body (headers, cells, grid lines) at the given origin offset.
186
+ * Shared by the host-neutral wrapper below; VS Code's blocks preview wraps
187
+ * this the same way it wraps {@link renderBlocksLayoutSvg} for the tree form.
188
+ */
189
+ export function renderGridBody(layout: GridLayout, ox: number, oy: number): string {
190
+ const tw = layout.bounds.width;
191
+ const th = layout.bounds.height;
192
+ const parts: string[] = [];
193
+
194
+ parts.push(`<rect class="diagram-node level-0" x="${ox}" y="${oy}" width="${tw}" height="${th}" stroke="none"/>`);
195
+
196
+ // Corner cell (above the row headers, left of the column headers).
197
+ parts.push(
198
+ `<rect class="diagram-node level-1" x="${ox}" y="${oy}" width="${layout.rowHeaderWidth}" height="${layout.headerHeight}" stroke="none"/>`,
199
+ );
200
+
201
+ for (const col of layout.columns) {
202
+ const x = col.x + ox;
203
+ parts.push(
204
+ `<rect class="diagram-node level-1" x="${x}" y="${oy}" width="${col.width}" height="${layout.headerHeight}" stroke="none"/>`,
205
+ );
206
+ const maxChars = maxCharsForInnerWidth(col.width - GRID_CELL_PAD_X * 2, CHAR_W_PRIMARY);
207
+ const lines = wrapWords(col.name, maxChars, 2);
208
+ parts.push(centeredGridCellTextSvg(lines, 'text-header', x + col.width / 2, oy, layout.headerHeight));
209
+ }
210
+
211
+ for (const row of layout.rows) {
212
+ const y = row.y + oy;
213
+ parts.push(
214
+ `<rect class="diagram-node level-2" x="${ox}" y="${y}" width="${layout.rowHeaderWidth}" height="${row.height}" stroke="none"/>`,
215
+ );
216
+ const maxChars = maxCharsForInnerWidth(layout.rowHeaderWidth - GRID_CELL_PAD_X * 2, CHAR_W_PRIMARY);
217
+ const lines = wrapWords(row.name, maxChars, 2);
218
+ parts.push(centeredGridCellTextSvg(lines, 'text-primary', ox + layout.rowHeaderWidth / 2, y, row.height));
219
+ }
220
+
221
+ for (const cell of layout.cells) {
222
+ if (cell.value === undefined) continue;
223
+ const cx = cell.x + ox + cell.width / 2;
224
+ const cy = cell.y + oy + cell.height / 2;
225
+ parts.push(
226
+ `<text class="text-secondary" x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="central">${escXml(cell.value)}</text>`,
227
+ );
228
+ }
229
+
230
+ const gridX1 = ox;
231
+ const gridX2 = ox + tw;
232
+ const gridY1 = oy;
233
+ const gridY2 = oy + th;
234
+
235
+ const headerBottomY = oy + layout.headerHeight;
236
+ parts.push(`<line class="diagram-edge" x1="${gridX1}" y1="${headerBottomY}" x2="${gridX2}" y2="${headerBottomY}"/>`);
237
+ for (const row of layout.rows) {
238
+ const rowBottomY = oy + row.y + row.height;
239
+ parts.push(`<line class="diagram-edge" x1="${gridX1}" y1="${rowBottomY}" x2="${gridX2}" y2="${rowBottomY}"/>`);
240
+ }
241
+
242
+ const rowHeaderLineX = ox + layout.rowHeaderWidth;
243
+ parts.push(`<line class="diagram-edge" x1="${rowHeaderLineX}" y1="${gridY1}" x2="${rowHeaderLineX}" y2="${gridY2}"/>`);
244
+ for (const col of layout.columns) {
245
+ const colLineX = ox + col.x + col.width;
246
+ parts.push(`<line class="diagram-edge" x1="${colLineX}" y1="${gridY1}" x2="${colLineX}" y2="${gridY2}"/>`);
247
+ }
248
+
249
+ parts.push(`<rect class="blocks-grid-border" x="${ox}" y="${oy}" width="${tw}" height="${th}"/>`);
250
+
251
+ return parts.join('\n');
252
+ }
253
+
254
+ export interface RenderGridLayoutOptions {
255
+ /** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
256
+ topInset?: number;
257
+ /** Raw SVG injected immediately after the opening tag — a header line or a full title block. */
258
+ title?: string;
259
+ /** When set, the theme CSS is embedded as `<style>` so the SVG is self-contained. */
260
+ embedCssTheme?: ThemeId;
261
+ }
262
+
263
+ /** The single grid (matrix subset) SVG emitter shared by every host. */
264
+ export function renderGridLayoutSvg(layout: GridLayout, options: RenderGridLayoutOptions = {}): string {
265
+ const { topInset = 0, title = '', embedCssTheme } = options;
266
+
267
+ const w = layout.bounds.width + PAD * 2;
268
+ const h = layout.bounds.height + PAD * 2 + topInset;
269
+ const ox = PAD;
270
+ const oy = PAD + topInset;
271
+
272
+ const body = renderGridBody(layout, ox, oy);
273
+ const styleLine = embedCssTheme ? `\n<style>${generateSvgEmbedCss(embedCssTheme)}${GRID_EMBED_CSS}</style>` : '';
274
+
275
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${styleLine}
276
+ ${title}
277
+ ${body}
278
+ </svg>`;
279
+ }
280
+
281
+ export interface RenderGridOptions {
282
+ title?: string;
283
+ layoutOptions?: GridLayoutOptions;
284
+ }
285
+
286
+ /** Host-neutral grid (matrix subset) renderer (IntelliJ/UI). */
287
+ export function renderGridSvg(doc: GridFile, options: RenderGridOptions = {}): string {
288
+ const { title = '', layoutOptions } = options;
289
+ const layout = layoutGrid(doc, layoutOptions);
290
+
291
+ if (layout.columns.length === 0 || layout.rows.length === 0) {
292
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
293
+ }
294
+
295
+ const titleSvg = title
296
+ ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Nested Blocks — ${title}`)}</text>`
297
+ : '';
298
+
299
+ return renderGridLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
300
+ }