@transitrix/diagrams 1.8.11 → 1.8.18
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/validate.js +9 -1
- package/dist/blocks/index.d.ts +5 -4
- package/dist/blocks/index.js +3 -2
- package/dist/blocks/layout.d.ts +8 -1
- package/dist/blocks/layout.js +57 -0
- package/dist/blocks/templates/index.d.ts +9 -0
- package/dist/blocks/templates/index.js +11 -0
- package/dist/blocks/templates/raci.d.ts +9 -0
- package/dist/blocks/templates/raci.js +24 -0
- package/dist/blocks/types.d.ts +58 -0
- package/dist/blocks/validate.d.ts +31 -1
- package/dist/blocks/validate.js +127 -0
- package/dist/capability-map/validate.js +41 -0
- package/dist/fgca/parse-canonical.js +8 -0
- package/dist/repo-validate/check-element-hygiene.d.ts +8 -0
- package/dist/repo-validate/check-element-hygiene.js +211 -0
- package/dist/repo-validate/check-strategy-chain.d.ts +8 -0
- package/dist/repo-validate/check-strategy-chain.js +491 -0
- package/dist/repo-validate/index.d.ts +2 -1
- package/dist/repo-validate/index.js +1 -0
- package/dist/repo-validate/resolve-model.d.ts +5 -0
- package/dist/repo-validate/resolve-model.js +71 -0
- package/dist/repo-validate/types.d.ts +70 -1
- package/dist/repo-validate/types.js +22 -3
- package/dist/repo-validate/validate-repo.d.ts +14 -3
- package/dist/repo-validate/validate-repo.js +21 -8
- package/dist/webview/render-blocks.d.ts +23 -1
- package/dist/webview/render-blocks.js +100 -2
- package/package.json +1 -1
- package/src/activities/validate.ts +9 -1
- package/src/blocks/index.ts +16 -2
- package/src/blocks/layout.ts +70 -0
- package/src/blocks/templates/index.ts +13 -0
- package/src/blocks/templates/raci.ts +28 -0
- package/src/blocks/types.ts +67 -0
- package/src/blocks/validate.ts +157 -1
- package/src/capability-map/validate.ts +40 -0
- package/src/fgca/parse-canonical.ts +8 -0
- package/src/repo-validate/check-element-hygiene.ts +236 -0
- package/src/repo-validate/check-strategy-chain.ts +528 -0
- package/src/repo-validate/index.ts +2 -1
- package/src/repo-validate/resolve-model.ts +79 -0
- package/src/repo-validate/types.ts +95 -4
- package/src/repo-validate/validate-repo.ts +21 -8
- package/src/webview/render-blocks.ts +159 -2
|
@@ -7,7 +7,26 @@
|
|
|
7
7
|
// runtime (methodology ADR "Validation Two-Axis Model"; Studio decision
|
|
8
8
|
// "Validation Runtime Convergence").
|
|
9
9
|
//
|
|
10
|
-
// The finding shape
|
|
11
|
-
// richer `target` / `category` reporting taxonomy
|
|
12
|
-
// ADR until a real consumer
|
|
10
|
+
// The finding shape was originally frozen minimal — `{ scope, id, message }` —
|
|
11
|
+
// with the richer `target` / `category` reporting taxonomy explicitly DEFERRED
|
|
12
|
+
// per the ADR until a real consumer needed it. This un-freezes one field,
|
|
13
|
+
// `ruleId`: DSM (migrating off its own Go `Validate*` functions) needs a
|
|
14
|
+
// stable code per finding to map onto its existing import-log rule taxonomy
|
|
15
|
+
// (`api02/internal/importer/{goals,activities,fgca}.go`). The richer
|
|
16
|
+
// `target`/`category` taxonomy stays deferred — only `ruleId` is added.
|
|
17
|
+
// `ruleId` is optional: only checks with a stable, documented code set it
|
|
18
|
+
// (`docs/validation.md` § repo-scope rule list); the original structural
|
|
19
|
+
// checks (syntax/uniqueness/atomicity/referential-integrity/policy) stay
|
|
20
|
+
// uncoded rather than being retrofitted with invented codes.
|
|
21
|
+
//
|
|
22
|
+
// A second field, `severity`, is un-frozen for the same reason: DSM's
|
|
23
|
+
// warn-severity strategy-chain rules (GOALS-009/011, ACT-005, FGCA-012..014
|
|
24
|
+
// — see check-strategy-chain.ts) are non-fatal by
|
|
25
|
+
// DSM's own taxonomy ("import anyway, record the warning"); without a
|
|
26
|
+
// severity field, porting them here would silently make them blocking,
|
|
27
|
+
// since every existing repo-scope finding is implicitly blocking. `severity`
|
|
28
|
+
// is optional and defaults to `'error'` when omitted — every finding from
|
|
29
|
+
// before this field existed (syntax/uniqueness/atomicity/referential-
|
|
30
|
+
// integrity/policy, plus the error-tier strategy-chain rules and
|
|
31
|
+
// TSVC-003/INT-002) stays blocking with no code change at the call site.
|
|
13
32
|
export {};
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import type { RepoFinding, RepoModelInput } from './types.js';
|
|
1
|
+
import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
|
|
2
|
+
export declare function isRecord(v: unknown): v is Record<string, unknown>;
|
|
3
|
+
/** Read a string `id` from a parsed doc, or `null` if absent/non-string.
|
|
4
|
+
* Mirrors lint.py, which only treats a doc as an element/relation when it has
|
|
5
|
+
* a top-level `id` (sidecars such as versioned-attribute files have no `id`
|
|
6
|
+
* and are ignored). Exported for reuse by `resolve-model.ts`, which needs the
|
|
7
|
+
* same id/endpoint reading rules to stay in lockstep with this validator. */
|
|
8
|
+
export declare function docId(doc: RepoDoc): string | null;
|
|
9
|
+
/** Resolve a relation endpoint, which may be a bare id string or a `{ id }`
|
|
10
|
+
* mapping (lint.py accepts both). Returns the id string or `null`. */
|
|
11
|
+
export declare function endpointId(value: unknown): string | null;
|
|
2
12
|
/**
|
|
3
13
|
* Run all repo-scope checks over a loaded canon model and return the findings.
|
|
4
14
|
* Pure: no IO, deterministic order (syntax, uniqueness, atomicity, referential,
|
|
5
|
-
* semantics, policy). Reaches parity with `lint.py` on the
|
|
6
|
-
* (zero findings on a clean tree
|
|
15
|
+
* semantics, policy, strategy-chain). Reaches parity with `lint.py` on the
|
|
16
|
+
* `acme_corp` fixture (zero findings on a clean tree from the original
|
|
17
|
+
* lint.py-ported phases).
|
|
7
18
|
*/
|
|
8
19
|
export declare function validateRepoModel(input: RepoModelInput): RepoFinding[];
|
|
@@ -23,19 +23,23 @@
|
|
|
23
23
|
// 6. Policy (phase 6) — element marked Active/Production with no
|
|
24
24
|
// owner.
|
|
25
25
|
//
|
|
26
|
-
// Findings stay `{ scope, id, message }`
|
|
27
|
-
//
|
|
26
|
+
// Findings stay `{ scope, id, message }` plus the two un-frozen optional
|
|
27
|
+
// fields `ruleId` and `severity` — the richer target/category taxonomy stays
|
|
28
|
+
// deferred per the ADR.
|
|
29
|
+
import { checkStrategyChainSemantics } from './check-strategy-chain.js';
|
|
30
|
+
import { checkElementHygiene } from './check-element-hygiene.js';
|
|
28
31
|
const PScope = 'repo';
|
|
29
32
|
/** Statuses that, per lint.py's policy check, require an owner. */
|
|
30
33
|
const OWNER_REQUIRED_STATUSES = new Set(['Active', 'Production']);
|
|
31
|
-
function isRecord(v) {
|
|
34
|
+
export function isRecord(v) {
|
|
32
35
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
33
36
|
}
|
|
34
37
|
/** Read a string `id` from a parsed doc, or `null` if absent/non-string.
|
|
35
38
|
* Mirrors lint.py, which only treats a doc as an element/relation when it has
|
|
36
39
|
* a top-level `id` (sidecars such as versioned-attribute files have no `id`
|
|
37
|
-
* and are ignored).
|
|
38
|
-
|
|
40
|
+
* and are ignored). Exported for reuse by `resolve-model.ts`, which needs the
|
|
41
|
+
* same id/endpoint reading rules to stay in lockstep with this validator. */
|
|
42
|
+
export function docId(doc) {
|
|
39
43
|
if (!doc.data)
|
|
40
44
|
return null;
|
|
41
45
|
const id = doc.data['id'];
|
|
@@ -43,7 +47,7 @@ function docId(doc) {
|
|
|
43
47
|
}
|
|
44
48
|
/** Resolve a relation endpoint, which may be a bare id string or a `{ id }`
|
|
45
49
|
* mapping (lint.py accepts both). Returns the id string or `null`. */
|
|
46
|
-
function endpointId(value) {
|
|
50
|
+
export function endpointId(value) {
|
|
47
51
|
if (typeof value === 'string')
|
|
48
52
|
return value.length > 0 ? value : null;
|
|
49
53
|
if (isRecord(value)) {
|
|
@@ -329,6 +333,7 @@ function checkLayerSemantics(input, findings) {
|
|
|
329
333
|
findings.push({
|
|
330
334
|
scope: PScope,
|
|
331
335
|
id: svcId,
|
|
336
|
+
ruleId: 'TSVC-003',
|
|
332
337
|
message: `TSVC-003: TECHNOLOGY_SERVICE '${svcId}' node '${nodeRef}' ` +
|
|
333
338
|
`must resolve to a NODE element; got notation='${resolved['notation']}'.`,
|
|
334
339
|
});
|
|
@@ -353,6 +358,7 @@ function checkLayerSemantics(input, findings) {
|
|
|
353
358
|
findings.push({
|
|
354
359
|
scope: PScope,
|
|
355
360
|
id: intId,
|
|
361
|
+
ruleId: 'INT-002',
|
|
356
362
|
message: `INT-002: INTEGRATION '${intId}' interface_semantics endpoint '${endpointId}' ` +
|
|
357
363
|
`(${field}) must resolve to an APPLICATION element; got notation='${resolved['notation']}'.`,
|
|
358
364
|
});
|
|
@@ -383,8 +389,9 @@ function checkPolicy(input, findings) {
|
|
|
383
389
|
/**
|
|
384
390
|
* Run all repo-scope checks over a loaded canon model and return the findings.
|
|
385
391
|
* Pure: no IO, deterministic order (syntax, uniqueness, atomicity, referential,
|
|
386
|
-
* semantics, policy). Reaches parity with `lint.py` on the
|
|
387
|
-
* (zero findings on a clean tree
|
|
392
|
+
* semantics, policy, strategy-chain). Reaches parity with `lint.py` on the
|
|
393
|
+
* `acme_corp` fixture (zero findings on a clean tree from the original
|
|
394
|
+
* lint.py-ported phases).
|
|
388
395
|
*/
|
|
389
396
|
export function validateRepoModel(input) {
|
|
390
397
|
const findings = [];
|
|
@@ -399,5 +406,11 @@ export function validateRepoModel(input) {
|
|
|
399
406
|
checkReferentialIntegrity(input, findings);
|
|
400
407
|
checkLayerSemantics(input, findings);
|
|
401
408
|
checkPolicy(input, findings);
|
|
409
|
+
// Phase 7 — strategy-chain semantic rules ported from DSM's Go Validate*
|
|
410
|
+
// functions (GOALS-010, ACT-006..009, FGCA-008..011).
|
|
411
|
+
checkStrategyChainSemantics(input, findings);
|
|
412
|
+
// Phase 8 — standalone-element envelope rules ported from DSM's Go
|
|
413
|
+
// Validate*Element functions (GOAL-ELEM-001..003, ACTION-001/002/005).
|
|
414
|
+
checkElementHygiene(input, findings);
|
|
402
415
|
return findings;
|
|
403
416
|
}
|
|
@@ -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
|
@@ -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 };
|
package/src/blocks/index.ts
CHANGED
|
@@ -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';
|
package/src/blocks/layout.ts
CHANGED
|
@@ -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];
|
package/src/blocks/types.ts
CHANGED
|
@@ -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;
|