@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.
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/fgca/parse-canonical.js +8 -0
  15. package/dist/repo-validate/check-element-hygiene.d.ts +8 -0
  16. package/dist/repo-validate/check-element-hygiene.js +211 -0
  17. package/dist/repo-validate/check-strategy-chain.d.ts +8 -0
  18. package/dist/repo-validate/check-strategy-chain.js +491 -0
  19. package/dist/repo-validate/index.d.ts +2 -1
  20. package/dist/repo-validate/index.js +1 -0
  21. package/dist/repo-validate/resolve-model.d.ts +5 -0
  22. package/dist/repo-validate/resolve-model.js +71 -0
  23. package/dist/repo-validate/types.d.ts +70 -1
  24. package/dist/repo-validate/types.js +22 -3
  25. package/dist/repo-validate/validate-repo.d.ts +14 -3
  26. package/dist/repo-validate/validate-repo.js +21 -8
  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/fgca/parse-canonical.ts +8 -0
  39. package/src/repo-validate/check-element-hygiene.ts +236 -0
  40. package/src/repo-validate/check-strategy-chain.ts +528 -0
  41. package/src/repo-validate/index.ts +2 -1
  42. package/src/repo-validate/resolve-model.ts +79 -0
  43. package/src/repo-validate/types.ts +95 -4
  44. package/src/repo-validate/validate-repo.ts +21 -8
  45. package/src/webview/render-blocks.ts +159 -2
@@ -7,11 +7,30 @@
7
7
  // runtime (methodology ADR "Validation Two-Axis Model"; Studio decision
8
8
  // "Validation Runtime Convergence").
9
9
  //
10
- // The finding shape is deliberately minimal — `{ scope, id, message }`. The
11
- // richer `target` / `category` reporting taxonomy is explicitly DEFERRED per the
12
- // ADR until a real consumer needs it; do not add it here.
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
 
14
- /** A single repo-scope validation finding. Shape is frozen at `{ scope, id, message }`. */
33
+ /** A single repo-scope validation finding. */
15
34
  export interface RepoFinding {
16
35
  /** Always `'repo'` — the execution axis this finding came from. */
17
36
  scope: 'repo';
@@ -22,6 +41,19 @@ export interface RepoFinding {
22
41
  id: string;
23
42
  /** Human-readable description of the violation. */
24
43
  message: string;
44
+ /**
45
+ * Stable rule code (e.g. `GOALS-010`, `ACT-006`, `FGCA-008`), for a consumer
46
+ * that maps findings onto its own rule taxonomy (`docs/validation.md`).
47
+ * Omitted for checks that don't yet have one.
48
+ */
49
+ ruleId?: string;
50
+ /**
51
+ * `'error'` (blocking) or `'warning'` (advisory). Omitted is equivalent to
52
+ * `'error'` — every check that predates this field never sets it and must
53
+ * stay blocking. Only the warn-severity strategy-chain rules set this to
54
+ * `'warning'` today (`docs/validation.md` § repo-scope rule list).
55
+ */
56
+ severity?: 'error' | 'warning';
25
57
  }
26
58
 
27
59
  /** A parsed canon document fed to the repo validator. IO (the filesystem walk)
@@ -47,3 +79,62 @@ export interface RepoModelInput {
47
79
  /** Documents found under `canon/relations/**`. */
48
80
  relations: RepoDoc[];
49
81
  }
82
+
83
+ /** One resolved canon element, projected from a `RepoDoc` for a non-JS
84
+ * consumer (DSM's Go importer). The top-level fields below are a stable,
85
+ * minimal identity projection — kept for backward compatibility with
86
+ * consumers that only need `id`/`name`/`notation`/`type`/`layer`/`sourceFile`
87
+ * (mirrors DSM's `methodology_element` table). `data` carries the complete
88
+ * parsed element alongside it, so a consumer never needs an engine schema
89
+ * change to read a canon-authored field the engine already parsed (a goal's
90
+ * `level`/`parent`/`description`/`link`/`tags`, an action's scheduling and
91
+ * ownership fields, etc.) — the engine owns each notation's field set
92
+ * (ELEMENT_PRIMITIVES.md), so its output stays notation-shaped rather than
93
+ * growing a bespoke field list per consumer. This is the faithful-projection
94
+ * fallback: a typed per-notation shape may replace `data` later, but the
95
+ * contract must not be curated down to what one consumer currently needs. */
96
+ export interface ResolvedElementRecord {
97
+ /** Canonical id, e.g. `DRIVER-COMP-1`. */
98
+ id: string;
99
+ /** `name` field; `''` when absent (should not happen for an admitted element). */
100
+ name: string;
101
+ /** The element TYPE's short name — the doc's `notation` field, e.g. `driver`, `goal`, `capability`. */
102
+ notation: string;
103
+ /** Subtype from the TYPE's controlled vocabulary (ELEMENT_PRIMITIVES.md §3), e.g. `internal`/`external` for DRIVER. Omitted when the doc has none. */
104
+ type?: string;
105
+ /** `motivation` / `business` / `application` / `technology` / `implementation`. Read from the doc's `layer` field when present, else derived from the `canon/elements/<NN>_<layer>/…` folder. */
106
+ layer?: string;
107
+ /** Source file path, relative to the scanned root. */
108
+ sourceFile: string;
109
+ /** The full parsed element document — every field its author supplied,
110
+ * unfiltered (including the admission/lifecycle envelope). The same
111
+ * `RepoDoc.data` the repo-scope validator already consumes, so this
112
+ * projection and the validation pass never see a different parse. */
113
+ data: Record<string, unknown>;
114
+ }
115
+
116
+ /** One resolved canon relation, projected from a `RepoDoc`. Only
117
+ * emitted when both endpoints resolve to a non-empty id — an endpoint that
118
+ * fails to resolve is already surfaced as a referential-integrity finding by
119
+ * `validateRepoModel`; this projection does not duplicate that as a
120
+ * half-resolved record. */
121
+ export interface ResolvedRelationRecord {
122
+ /** Canonical id, e.g. `REL-EMP-PERSON-OPS-1`. `''` when the relation doc has no `id`. */
123
+ id: string;
124
+ /** The relation's `type` field, e.g. `employment`, `realizes`. Omitted when the doc has none. */
125
+ kind?: string;
126
+ /** Resolved `from` (or legacy `source`) endpoint id. */
127
+ source: string;
128
+ /** Resolved `to` (or legacy `target`) endpoint id. */
129
+ target: string;
130
+ /** Source file path, relative to the scanned root. */
131
+ sourceFile: string;
132
+ }
133
+
134
+ /** The resolved element/relation records for a repo-scope run —
135
+ * the shape `transitrix validate --scope=repo --json --include-model` adds
136
+ * alongside the validation findings. */
137
+ export interface ResolvedRepoModel {
138
+ elements: ResolvedElementRecord[];
139
+ relations: ResolvedRelationRecord[];
140
+ }
@@ -23,9 +23,12 @@
23
23
  // 6. Policy (phase 6) — element marked Active/Production with no
24
24
  // owner.
25
25
  //
26
- // Findings stay `{ scope, id, message }` — no severity, no target/category
27
- // taxonomy (deferred per the ADR).
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.
28
29
 
30
+ import { checkStrategyChainSemantics } from './check-strategy-chain.js';
31
+ import { checkElementHygiene } from './check-element-hygiene.js';
29
32
  import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
30
33
 
31
34
  const PScope: RepoFinding['scope'] = 'repo';
@@ -33,15 +36,16 @@ const PScope: RepoFinding['scope'] = 'repo';
33
36
  /** Statuses that, per lint.py's policy check, require an owner. */
34
37
  const OWNER_REQUIRED_STATUSES = new Set(['Active', 'Production']);
35
38
 
36
- function isRecord(v: unknown): v is Record<string, unknown> {
39
+ export function isRecord(v: unknown): v is Record<string, unknown> {
37
40
  return typeof v === 'object' && v !== null && !Array.isArray(v);
38
41
  }
39
42
 
40
43
  /** Read a string `id` from a parsed doc, or `null` if absent/non-string.
41
44
  * Mirrors lint.py, which only treats a doc as an element/relation when it has
42
45
  * a top-level `id` (sidecars such as versioned-attribute files have no `id`
43
- * and are ignored). */
44
- function docId(doc: RepoDoc): string | null {
46
+ * and are ignored). Exported for reuse by `resolve-model.ts`, which needs the
47
+ * same id/endpoint reading rules to stay in lockstep with this validator. */
48
+ export function docId(doc: RepoDoc): string | null {
45
49
  if (!doc.data) return null;
46
50
  const id = doc.data['id'];
47
51
  return typeof id === 'string' && id.length > 0 ? id : null;
@@ -49,7 +53,7 @@ function docId(doc: RepoDoc): string | null {
49
53
 
50
54
  /** Resolve a relation endpoint, which may be a bare id string or a `{ id }`
51
55
  * mapping (lint.py accepts both). Returns the id string or `null`. */
52
- function endpointId(value: unknown): string | null {
56
+ export function endpointId(value: unknown): string | null {
53
57
  if (typeof value === 'string') return value.length > 0 ? value : null;
54
58
  if (isRecord(value)) {
55
59
  const id = value['id'];
@@ -345,6 +349,7 @@ function checkLayerSemantics(input: RepoModelInput, findings: RepoFinding[]): vo
345
349
  findings.push({
346
350
  scope: PScope,
347
351
  id: svcId,
352
+ ruleId: 'TSVC-003',
348
353
  message:
349
354
  `TSVC-003: TECHNOLOGY_SERVICE '${svcId}' node '${nodeRef}' ` +
350
355
  `must resolve to a NODE element; got notation='${resolved['notation']}'.`,
@@ -368,6 +373,7 @@ function checkLayerSemantics(input: RepoModelInput, findings: RepoFinding[]): vo
368
373
  findings.push({
369
374
  scope: PScope,
370
375
  id: intId,
376
+ ruleId: 'INT-002',
371
377
  message:
372
378
  `INT-002: INTEGRATION '${intId}' interface_semantics endpoint '${endpointId}' ` +
373
379
  `(${field}) must resolve to an APPLICATION element; got notation='${resolved['notation']}'.`,
@@ -399,8 +405,9 @@ function checkPolicy(input: RepoModelInput, findings: RepoFinding[]): void {
399
405
  /**
400
406
  * Run all repo-scope checks over a loaded canon model and return the findings.
401
407
  * Pure: no IO, deterministic order (syntax, uniqueness, atomicity, referential,
402
- * semantics, policy). Reaches parity with `lint.py` on the `acme_corp` fixture
403
- * (zero findings on a clean tree).
408
+ * semantics, policy, strategy-chain). Reaches parity with `lint.py` on the
409
+ * `acme_corp` fixture (zero findings on a clean tree from the original
410
+ * lint.py-ported phases).
404
411
  */
405
412
  export function validateRepoModel(input: RepoModelInput): RepoFinding[] {
406
413
  const findings: RepoFinding[] = [];
@@ -415,5 +422,11 @@ export function validateRepoModel(input: RepoModelInput): RepoFinding[] {
415
422
  checkReferentialIntegrity(input, findings);
416
423
  checkLayerSemantics(input, findings);
417
424
  checkPolicy(input, findings);
425
+ // Phase 7 — strategy-chain semantic rules ported from DSM's Go Validate*
426
+ // functions (GOALS-010, ACT-006..009, FGCA-008..011).
427
+ checkStrategyChainSemantics(input, findings);
428
+ // Phase 8 — standalone-element envelope rules ported from DSM's Go
429
+ // Validate*Element functions (GOAL-ELEM-001..003, ACTION-001/002/005).
430
+ checkElementHygiene(input, findings);
418
431
  return findings;
419
432
  }
@@ -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
+ }