@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
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { layoutActivities } from '../activities/layout.js';
20
20
  import { computeCpm } from '../activities/cpm.js';
21
- import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
21
+ import { horizontalCubicEdgePath, bowedCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
22
22
  import { parseNodeSizePreset, resolveActionNodeSize } from '../node-size-presets.js';
23
23
  import { generateSvgEmbedCss } from '../theme/index.js';
24
24
  import { emitCenteredTextSvg, layoutCenteredEntityText } from './entity-text-layout.js';
@@ -46,6 +46,68 @@ export const ACTIVITIES_NETWORK_DEFS = `<defs>
46
46
  <path d="M0,0 L0,6 L8,3 z" class="arrow-fill-critical"/>
47
47
  </marker>
48
48
  </defs>`;
49
+ /**
50
+ * Nodes horizontally between `source` and `target` (a "skip" edge passes over
51
+ * their column) whose vertical span overlaps the edge's Y band. A
52
+ * straight-tangent cubic (`horizontalCubicEdgePath`) stays within
53
+ * [min(sourceCenterY,targetCenterY), max(...)] by convex hull, so any such
54
+ * node sits directly on the edge's path — the "arrow goes straight through
55
+ * the node" bug reported against a linear PSND chain where a multi-predecessor
56
+ * activity's nearer predecessor shares a row with an intermediate column.
57
+ */
58
+ function blockingNodes(nodes, source, target) {
59
+ const sCenterY = source.y + source.height / 2;
60
+ const tCenterY = target.y + target.height / 2;
61
+ const loY = Math.min(sCenterY, tCenterY);
62
+ const hiY = Math.max(sCenterY, tCenterY);
63
+ const sRight = source.x + source.width;
64
+ const tLeft = target.x;
65
+ return nodes.filter((n) => {
66
+ if (n.id === source.id || n.id === target.id)
67
+ return false;
68
+ if (n.x + n.width <= sRight || n.x >= tLeft)
69
+ return false; // not between the two columns
70
+ return n.y < hiY && n.y + n.height > loY; // vertical overlap with the edge's band
71
+ });
72
+ }
73
+ /**
74
+ * Pre-offset bow-control-point Y for a skip edge arcing over `blockers`: one
75
+ * full node-height of clearance above the topmost blocker's top edge. A
76
+ * cubic's apex only reaches part-way toward its control point (see
77
+ * `bowedCubicEdgePath` doc), so this generous margin is what actually keeps
78
+ * the visible curve clear of the box — verified numerically in
79
+ * `render-activities.test.ts`.
80
+ */
81
+ function bowY(blockers) {
82
+ const topY = Math.min(...blockers.map((n) => n.y));
83
+ const clearance = Math.max(...blockers.map((n) => n.height));
84
+ return topY - clearance;
85
+ }
86
+ /**
87
+ * Extra top padding (px) a network-view canvas must add so skip-edge bows
88
+ * (see `blockingNodes`/`bowY` above) never draw above the SVG's y=0 and get
89
+ * clipped. `oyBase` is the canvas's Y offset *before* this padding — the
90
+ * historical `-bounds.y + N_PAD + titleH` both hosts compute — since a bow
91
+ * near the topmost row can reach above the diagram's own node bounds.
92
+ * Callers add the returned value to both `oyBase` and the canvas height.
93
+ */
94
+ export function computeNetworkTopPad(layout, oyBase) {
95
+ const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
96
+ let pad = 0;
97
+ for (const e of layout.edges) {
98
+ const s = nodeMap.get(e.sourceId);
99
+ const t = nodeMap.get(e.targetId);
100
+ if (!s || !t)
101
+ continue;
102
+ const blockers = blockingNodes(layout.nodes, s, t);
103
+ if (blockers.length === 0)
104
+ continue;
105
+ const shortfall = -(oyBase + bowY(blockers));
106
+ if (shortfall > pad)
107
+ pad = shortfall;
108
+ }
109
+ return pad;
110
+ }
49
111
  function activityNodeSvg(n, x, y, isCritical, isMilestone, durLabel) {
50
112
  const cls = `diagram-node act-node ${isCritical ? 'critical-node' : ''} ${isMilestone ? 'milestone-node' : ''}`.trim();
51
113
  const specs = layoutCenteredEntityText({
@@ -92,7 +154,11 @@ export function renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entr
92
154
  const ty = t.y + oy + t.height / 2;
93
155
  const cls = e.isCritical ? 'diagram-edge critical-edge' : 'diagram-edge';
94
156
  const marker = `url(#${e.isCritical ? 'arrow-crit' : 'arrow'})`;
95
- return `<path d="${horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature)}" class="${cls}" marker-end="${marker}"/>`;
157
+ const blockers = blockingNodes(layout.nodes, s, t);
158
+ const d = blockers.length > 0
159
+ ? bowedCubicEdgePath(sx, sy, tx, ty, bowY(blockers) + oy, curvature)
160
+ : horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature);
161
+ return `<path d="${d}" class="${cls}" marker-end="${marker}"/>`;
96
162
  })
97
163
  .join('\n');
98
164
  const nodeSvg = layout.nodes
@@ -142,10 +208,12 @@ export function renderActivitiesSvg(doc, options = {}) {
142
208
  }
143
209
  const cpm = computeCpm(renderDoc.activities ?? []);
144
210
  const titleH = title ? 24 : 0;
211
+ const oyBase = -layout.bounds.y + N_PAD + titleH;
212
+ const topPad = computeNetworkTopPad(layout, oyBase);
145
213
  const w = layout.bounds.width + N_PAD * 2;
146
- const h = layout.bounds.height + N_PAD * 2 + titleH;
214
+ const h = layout.bounds.height + N_PAD * 2 + titleH + topPad;
147
215
  const ox = -layout.bounds.x + N_PAD;
148
- const oy = -layout.bounds.y + N_PAD + titleH;
216
+ const oy = oyBase + topPad;
149
217
  const body = renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entryCurvature);
150
218
  const titleSvg = title
151
219
  ? `<text class="text-header" x="${N_PAD}" y="${N_PAD - 6}">${escXml(title)}</text>`
@@ -1,4 +1,4 @@
1
- import type { BlocksFile, BlocksLayout, BlocksLayoutOptions } from '../blocks/types.js';
1
+ import type { BlocksFile, BlocksLayout, BlocksLayoutOptions, GridFile, GridLayout, GridLayoutOptions } from '../blocks/types.js';
2
2
  import { type NodeSizePreset } from '../node-size-presets.js';
3
3
  import { type ThemeId } from '../theme/index.js';
4
4
  export interface RenderBlocksOptions {
@@ -29,3 +29,25 @@ export declare function renderBlocksLayoutSvg(layout: BlocksLayout, options?: Re
29
29
  * with the shared theme CSS embedded so the output is self-contained.
30
30
  */
31
31
  export declare function renderBlocksSvg(doc: BlocksFile, options?: RenderBlocksOptions): string;
32
+ /**
33
+ * Emit the grid body (headers, cells, grid lines) at the given origin offset.
34
+ * Shared by the host-neutral wrapper below; VS Code's blocks preview wraps
35
+ * this the same way it wraps {@link renderBlocksLayoutSvg} for the tree form.
36
+ */
37
+ export declare function renderGridBody(layout: GridLayout, ox: number, oy: number): string;
38
+ export interface RenderGridLayoutOptions {
39
+ /** Extra vertical space reserved at the top of the canvas (e.g. for a title block). */
40
+ topInset?: number;
41
+ /** Raw SVG injected immediately after the opening tag — a header line or a full title block. */
42
+ title?: string;
43
+ /** When set, the theme CSS is embedded as `<style>` so the SVG is self-contained. */
44
+ embedCssTheme?: ThemeId;
45
+ }
46
+ /** The single grid (matrix subset) SVG emitter shared by every host. */
47
+ export declare function renderGridLayoutSvg(layout: GridLayout, options?: RenderGridLayoutOptions): string;
48
+ export interface RenderGridOptions {
49
+ title?: string;
50
+ layoutOptions?: GridLayoutOptions;
51
+ }
52
+ /** Host-neutral grid (matrix subset) renderer (IntelliJ/UI). */
53
+ export declare function renderGridSvg(doc: GridFile, options?: RenderGridOptions): string;
@@ -12,10 +12,10 @@
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';
15
+ import { layoutNestedBlocks, layoutGrid } from '../blocks/layout.js';
16
16
  import { parseNodeSizePreset, resolveBlocksLeafSize } from '../node-size-presets.js';
17
17
  import { generateSvgEmbedCss } from '../theme/index.js';
18
- import { emitCenteredTextSvg, layoutHeaderBlockText, layoutLeafBlockText, } from './entity-text-layout.js';
18
+ import { CHAR_W_PRIMARY, emitCenteredTextSvg, layoutHeaderBlockText, layoutLeafBlockText, maxCharsForInnerWidth, wrapWords, } from './entity-text-layout.js';
19
19
  import { escXml } from './render-util.js';
20
20
  import { ENTITY_NODE_RX } from './notation-style.js';
21
21
  const PAD = 24;
@@ -105,3 +105,101 @@ export function renderBlocksSvg(doc, options = {}) {
105
105
  : '';
106
106
  return renderBlocksLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
107
107
  }
108
+ /**
109
+ * Matrix subset (08-blocks.md §4a) — first cut: a single-layer rectangular
110
+ * `grid:` document (the RACI-style matrix case) rendered as a plain
111
+ * HTML/CSS-table-like SVG grid. Layers, arbitrary
112
+ * (non-rectangular) cell sets, and nested sub-grids belong to the general
113
+ * layered-grid superset, which is still in design upstream (methodology) and
114
+ * out of scope here — see 08-blocks.md §4a / §8.
115
+ */
116
+ const GRID_CELL_PAD_X = 8;
117
+ const GRID_LINE_HEIGHT = 15;
118
+ const GRID_EMBED_CSS = `
119
+ .blocks-grid-border { fill: none; stroke: var(--ts-border, #cbd5e1); }`;
120
+ function centeredGridCellTextSvg(lines, cls, cx, cellTop, cellHeight) {
121
+ const ls = lines.length > 0 ? lines : [''];
122
+ const firstY = cellTop + cellHeight / 2 - ((ls.length - 1) / 2) * GRID_LINE_HEIGHT;
123
+ const tspans = ls
124
+ .map((ln, i) => `<tspan x="${cx}" y="${firstY + i * GRID_LINE_HEIGHT}">${escXml(ln)}</tspan>`)
125
+ .join('');
126
+ return `<text class="${cls}" text-anchor="middle" dominant-baseline="central">${tspans}</text>`;
127
+ }
128
+ /**
129
+ * Emit the grid body (headers, cells, grid lines) at the given origin offset.
130
+ * Shared by the host-neutral wrapper below; VS Code's blocks preview wraps
131
+ * this the same way it wraps {@link renderBlocksLayoutSvg} for the tree form.
132
+ */
133
+ export function renderGridBody(layout, ox, oy) {
134
+ const tw = layout.bounds.width;
135
+ const th = layout.bounds.height;
136
+ const parts = [];
137
+ parts.push(`<rect class="diagram-node level-0" x="${ox}" y="${oy}" width="${tw}" height="${th}" stroke="none"/>`);
138
+ // Corner cell (above the row headers, left of the column headers).
139
+ parts.push(`<rect class="diagram-node level-1" x="${ox}" y="${oy}" width="${layout.rowHeaderWidth}" height="${layout.headerHeight}" stroke="none"/>`);
140
+ for (const col of layout.columns) {
141
+ const x = col.x + ox;
142
+ parts.push(`<rect class="diagram-node level-1" x="${x}" y="${oy}" width="${col.width}" height="${layout.headerHeight}" stroke="none"/>`);
143
+ const maxChars = maxCharsForInnerWidth(col.width - GRID_CELL_PAD_X * 2, CHAR_W_PRIMARY);
144
+ const lines = wrapWords(col.name, maxChars, 2);
145
+ parts.push(centeredGridCellTextSvg(lines, 'text-header', x + col.width / 2, oy, layout.headerHeight));
146
+ }
147
+ for (const row of layout.rows) {
148
+ const y = row.y + oy;
149
+ parts.push(`<rect class="diagram-node level-2" x="${ox}" y="${y}" width="${layout.rowHeaderWidth}" height="${row.height}" stroke="none"/>`);
150
+ const maxChars = maxCharsForInnerWidth(layout.rowHeaderWidth - GRID_CELL_PAD_X * 2, CHAR_W_PRIMARY);
151
+ const lines = wrapWords(row.name, maxChars, 2);
152
+ parts.push(centeredGridCellTextSvg(lines, 'text-primary', ox + layout.rowHeaderWidth / 2, y, row.height));
153
+ }
154
+ for (const cell of layout.cells) {
155
+ if (cell.value === undefined)
156
+ continue;
157
+ const cx = cell.x + ox + cell.width / 2;
158
+ const cy = cell.y + oy + cell.height / 2;
159
+ parts.push(`<text class="text-secondary" x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="central">${escXml(cell.value)}</text>`);
160
+ }
161
+ const gridX1 = ox;
162
+ const gridX2 = ox + tw;
163
+ const gridY1 = oy;
164
+ const gridY2 = oy + th;
165
+ const headerBottomY = oy + layout.headerHeight;
166
+ parts.push(`<line class="diagram-edge" x1="${gridX1}" y1="${headerBottomY}" x2="${gridX2}" y2="${headerBottomY}"/>`);
167
+ for (const row of layout.rows) {
168
+ const rowBottomY = oy + row.y + row.height;
169
+ parts.push(`<line class="diagram-edge" x1="${gridX1}" y1="${rowBottomY}" x2="${gridX2}" y2="${rowBottomY}"/>`);
170
+ }
171
+ const rowHeaderLineX = ox + layout.rowHeaderWidth;
172
+ parts.push(`<line class="diagram-edge" x1="${rowHeaderLineX}" y1="${gridY1}" x2="${rowHeaderLineX}" y2="${gridY2}"/>`);
173
+ for (const col of layout.columns) {
174
+ const colLineX = ox + col.x + col.width;
175
+ parts.push(`<line class="diagram-edge" x1="${colLineX}" y1="${gridY1}" x2="${colLineX}" y2="${gridY2}"/>`);
176
+ }
177
+ parts.push(`<rect class="blocks-grid-border" x="${ox}" y="${oy}" width="${tw}" height="${th}"/>`);
178
+ return parts.join('\n');
179
+ }
180
+ /** The single grid (matrix subset) SVG emitter shared by every host. */
181
+ export function renderGridLayoutSvg(layout, options = {}) {
182
+ const { topInset = 0, title = '', embedCssTheme } = options;
183
+ const w = layout.bounds.width + PAD * 2;
184
+ const h = layout.bounds.height + PAD * 2 + topInset;
185
+ const ox = PAD;
186
+ const oy = PAD + topInset;
187
+ const body = renderGridBody(layout, ox, oy);
188
+ const styleLine = embedCssTheme ? `\n<style>${generateSvgEmbedCss(embedCssTheme)}${GRID_EMBED_CSS}</style>` : '';
189
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${styleLine}
190
+ ${title}
191
+ ${body}
192
+ </svg>`;
193
+ }
194
+ /** Host-neutral grid (matrix subset) renderer (IntelliJ/UI). */
195
+ export function renderGridSvg(doc, options = {}) {
196
+ const { title = '', layoutOptions } = options;
197
+ const layout = layoutGrid(doc, layoutOptions);
198
+ if (layout.columns.length === 0 || layout.rows.length === 0) {
199
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
200
+ }
201
+ const titleSvg = title
202
+ ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Nested Blocks — ${title}`)}</text>`
203
+ : '';
204
+ return renderGridLayoutSvg(layout, { title: titleSvg, embedCssTheme: 'transitrix' });
205
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitrix/diagrams",
3
- "version": "1.8.13",
3
+ "version": "1.8.19",
4
4
  "type": "module",
5
5
  "description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
6
6
  "license": "MIT",
@@ -19,7 +19,15 @@ export function validateActivities(input: unknown): ActivityValidationResult {
19
19
 
20
20
  const raw = input as Record<string, unknown>;
21
21
 
22
- // ACT-001: notation must be "action"
22
+ // ACT-001: notation must be "action". ACT-020 is deliberately NOT ported
23
+ // here (DSM's api02/internal/importer/activities.go ValidateActivities
24
+ // still accepts the pre-2026-06-25 "activities" notation/root-key alias
25
+ // with a warning) — PR #320 ("drop deprecated notation aliases
26
+ // fgca/fga/activities/activity-card") already removed that leniency from
27
+ // this validator on purpose, with tests asserting it is "no longer
28
+ // accepted". Re-introducing it would reverse a deliberate, shipped
29
+ // decision, not fill a gap; flagged in the PR for a call rather than
30
+ // silently reversed either way.
23
31
  if (raw.notation === undefined) {
24
32
  errors.push({ code: 'ACT-001', message: 'notation field is required' });
25
33
  return { valid: false, errors, warnings };
@@ -5,13 +5,27 @@ export type {
5
5
  BlocksLayoutOptions,
6
6
  LaidOutBlock,
7
7
  BlocksLayout,
8
+ GridColumn,
9
+ GridRow,
10
+ GridHeader,
11
+ GridFile,
12
+ GridLayoutOptions,
13
+ LaidOutGridColumn,
14
+ LaidOutGridRow,
15
+ LaidOutGridCell,
16
+ GridLayout,
8
17
  } from './types.js';
9
18
 
10
- export { validateNestedBlocks, isWellFormedBlock } from './validate.js';
19
+ export { validateNestedBlocks, validateGrid, validateBlocks, isWellFormedBlock } from './validate.js';
11
20
  export type {
12
21
  ValidationError as BlocksValidationError,
13
22
  ValidationWarning as BlocksValidationWarning,
14
23
  ValidationResult as BlocksValidationResult,
24
+ GridRule,
25
+ ValidateGridOptions,
26
+ ValidateBlocksOptions,
15
27
  } from './validate.js';
16
28
 
17
- export { layoutNestedBlocks, iterateBlocks } from './layout.js';
29
+ export { layoutNestedBlocks, layoutGrid, iterateBlocks } from './layout.js';
30
+
31
+ export { GRID_TEMPLATE_RULES } from './templates/index.js';
@@ -3,7 +3,13 @@ import type {
3
3
  BlocksFile,
4
4
  BlocksLayout,
5
5
  BlocksLayoutOptions,
6
+ GridFile,
7
+ GridLayout,
8
+ GridLayoutOptions,
6
9
  LaidOutBlock,
10
+ LaidOutGridCell,
11
+ LaidOutGridColumn,
12
+ LaidOutGridRow,
7
13
  } from './types.js';
8
14
  import { ENTITY_NODE_SIZE } from '../node-size-presets.js';
9
15
 
@@ -16,6 +22,13 @@ const DEFAULTS: Required<BlocksLayoutOptions> = {
16
22
  topLevelGap: 24,
17
23
  };
18
24
 
25
+ const GRID_DEFAULTS: Required<GridLayoutOptions> = {
26
+ columnWidth: 130,
27
+ rowHeight: 44,
28
+ rowHeaderWidth: 200,
29
+ headerHeight: 56,
30
+ };
31
+
19
32
  interface MeasuredBlock {
20
33
  width: number;
21
34
  height: number;
@@ -185,3 +198,60 @@ export function* iterateBlocks(layout: BlocksLayout): Iterable<LaidOutBlock> {
185
198
  }
186
199
  for (const b of layout.blocks) yield* walk(b);
187
200
  }
201
+
202
+ /**
203
+ * Matrix subset (08-blocks.md §4a): lay out a `grid:` root document as a
204
+ * single-layer rectangular table — a row-header column, a column-header row,
205
+ * and one cell per `(row, column)` pair. Assumes the document is already
206
+ * well-formed ({@link validateGrid}); callers validate first.
207
+ */
208
+ export function layoutGrid(file: GridFile, options?: GridLayoutOptions): GridLayout {
209
+ const opts = { ...GRID_DEFAULTS, ...(options ?? {}) };
210
+ const grid = file?.grid;
211
+ const rawColumns = Array.isArray(grid?.columns) ? grid.columns : [];
212
+ const rawRows = Array.isArray(grid?.rows) ? grid.rows : [];
213
+
214
+ const columns: LaidOutGridColumn[] = rawColumns.map((c, i) => ({
215
+ id: c.id,
216
+ name: c.name,
217
+ x: opts.rowHeaderWidth + i * opts.columnWidth,
218
+ width: opts.columnWidth,
219
+ }));
220
+
221
+ const rows: LaidOutGridRow[] = rawRows.map((r, i) => ({
222
+ id: r.id,
223
+ name: r.name,
224
+ y: opts.headerHeight + i * opts.rowHeight,
225
+ height: opts.rowHeight,
226
+ }));
227
+
228
+ const cells: LaidOutGridCell[] = [];
229
+ for (let ri = 0; ri < rawRows.length; ri++) {
230
+ const row = rawRows[ri];
231
+ for (let ci = 0; ci < rawColumns.length; ci++) {
232
+ const col = rawColumns[ci];
233
+ const raw = row.assign?.[col.id];
234
+ cells.push({
235
+ rowId: row.id,
236
+ colId: col.id,
237
+ x: columns[ci].x,
238
+ y: rows[ri].y,
239
+ width: opts.columnWidth,
240
+ height: opts.rowHeight,
241
+ value: raw === undefined ? undefined : String(raw),
242
+ });
243
+ }
244
+ }
245
+
246
+ const width = opts.rowHeaderWidth + rawColumns.length * opts.columnWidth;
247
+ const height = opts.headerHeight + rawRows.length * opts.rowHeight;
248
+
249
+ return {
250
+ bounds: { x: 0, y: 0, width, height },
251
+ rowHeaderWidth: opts.rowHeaderWidth,
252
+ headerHeight: opts.headerHeight,
253
+ columns,
254
+ rows,
255
+ cells,
256
+ };
257
+ }
@@ -0,0 +1,13 @@
1
+ import type { GridRule } from '../validate.js';
2
+ import { RACI_GRID_RULES } from './raci.js';
3
+
4
+ /**
5
+ * Closed registry of built-in grid templates (§6a): each entry is a template
6
+ * name (as passed to `--template <name>`) mapped to its `GridRule`s. Adding a
7
+ * new template means adding one rule module + one entry here — the same
8
+ * shape as adding a new notation validator, not a plugin/config-loading
9
+ * mechanism (deliberately out of scope — see the design note on `RACI-001`).
10
+ */
11
+ export const GRID_TEMPLATE_RULES: Record<string, GridRule[]> = {
12
+ raci: RACI_GRID_RULES,
13
+ };
@@ -0,0 +1,28 @@
1
+ import type { GridRule } from '../validate.js';
2
+
3
+ /**
4
+ * RACI-as-matrix template (methodology `templates/raci/`), built on the
5
+ * `blocks` matrix subset (08-blocks.md §4a). The base grid validator does not
6
+ * fix a cell-value vocabulary, so this invariant — exactly one Accountable
7
+ * owner per row — lives here, not in `validateGrid` (see §6a).
8
+ */
9
+ export const raciExactlyOneAccountableRule: GridRule = {
10
+ ruleId: 'RACI-001',
11
+ severity: 'error',
12
+ check(grid) {
13
+ const findings: Array<{ message: string; path?: string }> = [];
14
+ grid.rows.forEach((row, i) => {
15
+ const accountableCount = Object.values(row.assign ?? {}).filter((v) => v === 'A').length;
16
+ if (accountableCount !== 1) {
17
+ findings.push({
18
+ message:
19
+ `Row "${row.name}" (${row.id}) must have exactly one column assigned "A" (Accountable); found ${accountableCount}`,
20
+ path: `grid.rows[${i}]`,
21
+ });
22
+ }
23
+ });
24
+ return findings;
25
+ },
26
+ };
27
+
28
+ export const RACI_GRID_RULES: GridRule[] = [raciExactlyOneAccountableRule];
@@ -21,6 +21,73 @@ export interface BlocksFile {
21
21
  nested_blocks: NestedBlocksHeader;
22
22
  }
23
23
 
24
+ export interface GridColumn {
25
+ id: string;
26
+ name: string;
27
+ }
28
+
29
+ export interface GridRow {
30
+ id: string;
31
+ name: string;
32
+ assign?: Record<string, unknown>;
33
+ }
34
+
35
+ export interface GridHeader {
36
+ columns: GridColumn[];
37
+ rows: GridRow[];
38
+ }
39
+
40
+ export interface GridFile {
41
+ notation: string;
42
+ spec_version?: string;
43
+ grid: GridHeader;
44
+ }
45
+
46
+ export interface GridLayoutOptions {
47
+ /** Width of each data column (px). */
48
+ columnWidth?: number;
49
+ /** Height of each data row (px). */
50
+ rowHeight?: number;
51
+ /** Width of the row-header column carrying each row's name (px). */
52
+ rowHeaderWidth?: number;
53
+ /** Height of the column-header row carrying each column's name (px). */
54
+ headerHeight?: number;
55
+ }
56
+
57
+ export interface LaidOutGridColumn {
58
+ id: string;
59
+ name: string;
60
+ x: number;
61
+ width: number;
62
+ }
63
+
64
+ export interface LaidOutGridRow {
65
+ id: string;
66
+ name: string;
67
+ y: number;
68
+ height: number;
69
+ }
70
+
71
+ export interface LaidOutGridCell {
72
+ rowId: string;
73
+ colId: string;
74
+ x: number;
75
+ y: number;
76
+ width: number;
77
+ height: number;
78
+ /** The cell's `assign` value, stringified; omitted (blank) when unassigned. */
79
+ value?: string;
80
+ }
81
+
82
+ export interface GridLayout {
83
+ bounds: LayoutBounds;
84
+ rowHeaderWidth: number;
85
+ headerHeight: number;
86
+ columns: LaidOutGridColumn[];
87
+ rows: LaidOutGridRow[];
88
+ cells: LaidOutGridCell[];
89
+ }
90
+
24
91
  export interface BlocksLayoutOptions {
25
92
  /** Width of a leaf block (a block with no children). */
26
93
  leafWidth?: number;