@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
@@ -7,7 +7,15 @@ export function validateActivities(input) {
7
7
  return { valid: false, errors: [{ code: 'ACT-001', message: 'Input must be an object' }], warnings };
8
8
  }
9
9
  const raw = input;
10
- // ACT-001: notation must be "action"
10
+ // ACT-001: notation must be "action". ACT-020 is deliberately NOT ported
11
+ // here (DSM's api02/internal/importer/activities.go ValidateActivities
12
+ // still accepts the pre-2026-06-25 "activities" notation/root-key alias
13
+ // with a warning) — PR #320 ("drop deprecated notation aliases
14
+ // fgca/fga/activities/activity-card") already removed that leniency from
15
+ // this validator on purpose, with tests asserting it is "no longer
16
+ // accepted". Re-introducing it would reverse a deliberate, shipped
17
+ // decision, not fill a gap; flagged in the PR for a call rather than
18
+ // silently reversed either way.
11
19
  if (raw.notation === undefined) {
12
20
  errors.push({ code: 'ACT-001', message: 'notation field is required' });
13
21
  return { valid: false, errors, warnings };
@@ -1,4 +1,5 @@
1
- export type { Block, NestedBlocksHeader, BlocksFile, BlocksLayoutOptions, LaidOutBlock, BlocksLayout, } from './types.js';
2
- export { validateNestedBlocks, isWellFormedBlock } from './validate.js';
3
- export type { ValidationError as BlocksValidationError, ValidationWarning as BlocksValidationWarning, ValidationResult as BlocksValidationResult, } from './validate.js';
4
- export { layoutNestedBlocks, iterateBlocks } from './layout.js';
1
+ export type { Block, NestedBlocksHeader, BlocksFile, BlocksLayoutOptions, LaidOutBlock, BlocksLayout, GridColumn, GridRow, GridHeader, GridFile, GridLayoutOptions, LaidOutGridColumn, LaidOutGridRow, LaidOutGridCell, GridLayout, } from './types.js';
2
+ export { validateNestedBlocks, validateGrid, validateBlocks, isWellFormedBlock } from './validate.js';
3
+ export type { ValidationError as BlocksValidationError, ValidationWarning as BlocksValidationWarning, ValidationResult as BlocksValidationResult, GridRule, ValidateGridOptions, ValidateBlocksOptions, } from './validate.js';
4
+ export { layoutNestedBlocks, layoutGrid, iterateBlocks } from './layout.js';
5
+ export { GRID_TEMPLATE_RULES } from './templates/index.js';
@@ -1,2 +1,3 @@
1
- export { validateNestedBlocks, isWellFormedBlock } from './validate.js';
2
- export { layoutNestedBlocks, iterateBlocks } from './layout.js';
1
+ export { validateNestedBlocks, validateGrid, validateBlocks, isWellFormedBlock } from './validate.js';
2
+ export { layoutNestedBlocks, layoutGrid, iterateBlocks } from './layout.js';
3
+ export { GRID_TEMPLATE_RULES } from './templates/index.js';
@@ -1,4 +1,11 @@
1
- import type { BlocksFile, BlocksLayout, BlocksLayoutOptions, LaidOutBlock } from './types.js';
1
+ import type { BlocksFile, BlocksLayout, BlocksLayoutOptions, GridFile, GridLayout, GridLayoutOptions, LaidOutBlock } from './types.js';
2
2
  export declare function layoutNestedBlocks(file: BlocksFile, options?: BlocksLayoutOptions): BlocksLayout;
3
3
  /** Iterate every laid-out block (including nested children) in pre-order. */
4
4
  export declare function iterateBlocks(layout: BlocksLayout): Iterable<LaidOutBlock>;
5
+ /**
6
+ * Matrix subset (08-blocks.md §4a): lay out a `grid:` root document as a
7
+ * single-layer rectangular table — a row-header column, a column-header row,
8
+ * and one cell per `(row, column)` pair. Assumes the document is already
9
+ * well-formed ({@link validateGrid}); callers validate first.
10
+ */
11
+ export declare function layoutGrid(file: GridFile, options?: GridLayoutOptions): GridLayout;
@@ -7,6 +7,12 @@ const DEFAULTS = {
7
7
  childGap: 12,
8
8
  topLevelGap: 24,
9
9
  };
10
+ const GRID_DEFAULTS = {
11
+ columnWidth: 130,
12
+ rowHeight: 44,
13
+ rowHeaderWidth: 200,
14
+ headerHeight: 56,
15
+ };
10
16
  function resolveOptions(options) {
11
17
  return { ...DEFAULTS, ...(options ?? {}) };
12
18
  }
@@ -144,3 +150,54 @@ export function* iterateBlocks(layout) {
144
150
  for (const b of layout.blocks)
145
151
  yield* walk(b);
146
152
  }
153
+ /**
154
+ * Matrix subset (08-blocks.md §4a): lay out a `grid:` root document as a
155
+ * single-layer rectangular table — a row-header column, a column-header row,
156
+ * and one cell per `(row, column)` pair. Assumes the document is already
157
+ * well-formed ({@link validateGrid}); callers validate first.
158
+ */
159
+ export function layoutGrid(file, options) {
160
+ const opts = { ...GRID_DEFAULTS, ...(options ?? {}) };
161
+ const grid = file?.grid;
162
+ const rawColumns = Array.isArray(grid?.columns) ? grid.columns : [];
163
+ const rawRows = Array.isArray(grid?.rows) ? grid.rows : [];
164
+ const columns = rawColumns.map((c, i) => ({
165
+ id: c.id,
166
+ name: c.name,
167
+ x: opts.rowHeaderWidth + i * opts.columnWidth,
168
+ width: opts.columnWidth,
169
+ }));
170
+ const rows = rawRows.map((r, i) => ({
171
+ id: r.id,
172
+ name: r.name,
173
+ y: opts.headerHeight + i * opts.rowHeight,
174
+ height: opts.rowHeight,
175
+ }));
176
+ const cells = [];
177
+ for (let ri = 0; ri < rawRows.length; ri++) {
178
+ const row = rawRows[ri];
179
+ for (let ci = 0; ci < rawColumns.length; ci++) {
180
+ const col = rawColumns[ci];
181
+ const raw = row.assign?.[col.id];
182
+ cells.push({
183
+ rowId: row.id,
184
+ colId: col.id,
185
+ x: columns[ci].x,
186
+ y: rows[ri].y,
187
+ width: opts.columnWidth,
188
+ height: opts.rowHeight,
189
+ value: raw === undefined ? undefined : String(raw),
190
+ });
191
+ }
192
+ }
193
+ const width = opts.rowHeaderWidth + rawColumns.length * opts.columnWidth;
194
+ const height = opts.headerHeight + rawRows.length * opts.rowHeight;
195
+ return {
196
+ bounds: { x: 0, y: 0, width, height },
197
+ rowHeaderWidth: opts.rowHeaderWidth,
198
+ headerHeight: opts.headerHeight,
199
+ columns,
200
+ rows,
201
+ cells,
202
+ };
203
+ }
@@ -0,0 +1,9 @@
1
+ import type { GridRule } from '../validate.js';
2
+ /**
3
+ * Closed registry of built-in grid templates (§6a): each entry is a template
4
+ * name (as passed to `--template <name>`) mapped to its `GridRule`s. Adding a
5
+ * new template means adding one rule module + one entry here — the same
6
+ * shape as adding a new notation validator, not a plugin/config-loading
7
+ * mechanism (deliberately out of scope — see the design note on `RACI-001`).
8
+ */
9
+ export declare const GRID_TEMPLATE_RULES: Record<string, GridRule[]>;
@@ -0,0 +1,11 @@
1
+ import { RACI_GRID_RULES } from './raci.js';
2
+ /**
3
+ * Closed registry of built-in grid templates (§6a): each entry is a template
4
+ * name (as passed to `--template <name>`) mapped to its `GridRule`s. Adding a
5
+ * new template means adding one rule module + one entry here — the same
6
+ * shape as adding a new notation validator, not a plugin/config-loading
7
+ * mechanism (deliberately out of scope — see the design note on `RACI-001`).
8
+ */
9
+ export const GRID_TEMPLATE_RULES = {
10
+ raci: RACI_GRID_RULES,
11
+ };
@@ -0,0 +1,9 @@
1
+ import type { GridRule } from '../validate.js';
2
+ /**
3
+ * RACI-as-matrix template (methodology `templates/raci/`), built on the
4
+ * `blocks` matrix subset (08-blocks.md §4a). The base grid validator does not
5
+ * fix a cell-value vocabulary, so this invariant — exactly one Accountable
6
+ * owner per row — lives here, not in `validateGrid` (see §6a).
7
+ */
8
+ export declare const raciExactlyOneAccountableRule: GridRule;
9
+ export declare const RACI_GRID_RULES: GridRule[];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * RACI-as-matrix template (methodology `templates/raci/`), built on the
3
+ * `blocks` matrix subset (08-blocks.md §4a). The base grid validator does not
4
+ * fix a cell-value vocabulary, so this invariant — exactly one Accountable
5
+ * owner per row — lives here, not in `validateGrid` (see §6a).
6
+ */
7
+ export const raciExactlyOneAccountableRule = {
8
+ ruleId: 'RACI-001',
9
+ severity: 'error',
10
+ check(grid) {
11
+ const findings = [];
12
+ grid.rows.forEach((row, i) => {
13
+ const accountableCount = Object.values(row.assign ?? {}).filter((v) => v === 'A').length;
14
+ if (accountableCount !== 1) {
15
+ findings.push({
16
+ message: `Row "${row.name}" (${row.id}) must have exactly one column assigned "A" (Accountable); found ${accountableCount}`,
17
+ path: `grid.rows[${i}]`,
18
+ });
19
+ }
20
+ });
21
+ return findings;
22
+ },
23
+ };
24
+ export const RACI_GRID_RULES = [raciExactlyOneAccountableRule];
@@ -18,6 +18,64 @@ export interface BlocksFile {
18
18
  spec_version?: string;
19
19
  nested_blocks: NestedBlocksHeader;
20
20
  }
21
+ export interface GridColumn {
22
+ id: string;
23
+ name: string;
24
+ }
25
+ export interface GridRow {
26
+ id: string;
27
+ name: string;
28
+ assign?: Record<string, unknown>;
29
+ }
30
+ export interface GridHeader {
31
+ columns: GridColumn[];
32
+ rows: GridRow[];
33
+ }
34
+ export interface GridFile {
35
+ notation: string;
36
+ spec_version?: string;
37
+ grid: GridHeader;
38
+ }
39
+ export interface GridLayoutOptions {
40
+ /** Width of each data column (px). */
41
+ columnWidth?: number;
42
+ /** Height of each data row (px). */
43
+ rowHeight?: number;
44
+ /** Width of the row-header column carrying each row's name (px). */
45
+ rowHeaderWidth?: number;
46
+ /** Height of the column-header row carrying each column's name (px). */
47
+ headerHeight?: number;
48
+ }
49
+ export interface LaidOutGridColumn {
50
+ id: string;
51
+ name: string;
52
+ x: number;
53
+ width: number;
54
+ }
55
+ export interface LaidOutGridRow {
56
+ id: string;
57
+ name: string;
58
+ y: number;
59
+ height: number;
60
+ }
61
+ export interface LaidOutGridCell {
62
+ rowId: string;
63
+ colId: string;
64
+ x: number;
65
+ y: number;
66
+ width: number;
67
+ height: number;
68
+ /** The cell's `assign` value, stringified; omitted (blank) when unassigned. */
69
+ value?: string;
70
+ }
71
+ export interface GridLayout {
72
+ bounds: LayoutBounds;
73
+ rowHeaderWidth: number;
74
+ headerHeight: number;
75
+ columns: LaidOutGridColumn[];
76
+ rows: LaidOutGridRow[];
77
+ cells: LaidOutGridCell[];
78
+ }
21
79
  export interface BlocksLayoutOptions {
22
80
  /** Width of a leaf block (a block with no children). */
23
81
  leafWidth?: number;
@@ -1,7 +1,37 @@
1
- import type { Block } from './types.js';
1
+ import type { Block, GridHeader } from './types.js';
2
2
  import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
3
3
  export type { ValidationError, ValidationWarning, ValidationResult };
4
+ /**
5
+ * A template-level check over an already well-formed grid document (matrix
6
+ * subset, §4a). Templates built on the `grid:` root (e.g. RACI) supply their
7
+ * own `GridRule`s for vocabulary-specific invariants — the base validator
8
+ * intentionally does not fix what `assign` values mean (08-blocks.md §6a), so
9
+ * those invariants never live inside `validateGrid` itself.
10
+ */
11
+ export interface GridRule {
12
+ /** Template-namespaced rule code, e.g. "RACI-001" — never a BL-0xx code. */
13
+ ruleId: string;
14
+ severity: 'error' | 'warning';
15
+ check(grid: GridHeader): Array<{
16
+ message: string;
17
+ path?: string;
18
+ }>;
19
+ }
20
+ export interface ValidateGridOptions {
21
+ /** Extra template-level rules to run once the base grid is well-formed. */
22
+ rules?: GridRule[];
23
+ }
4
24
  export declare function validateNestedBlocks(input: unknown): ValidationResult;
25
+ /** Matrix subset (§4a): validate a `grid:` root document — BL-021..BL-025. */
26
+ export declare function validateGrid(input: unknown, options?: ValidateGridOptions): ValidationResult;
27
+ export interface ValidateBlocksOptions extends ValidateGridOptions {
28
+ }
29
+ /**
30
+ * Root-form dispatcher (§4/§4a): a document declares exactly one of
31
+ * `nested_blocks` (tree form) or `grid` (matrix subset) — never both, never
32
+ * neither (BL-020). Delegates to whichever form is present.
33
+ */
34
+ export declare function validateBlocks(input: unknown, options?: ValidateBlocksOptions): ValidationResult;
5
35
  /**
6
36
  * Convenience: validate a single `Block` subtree. Used by tests and by
7
37
  * downstream consumers that need to check a sub-document.
@@ -163,6 +163,133 @@ export function validateNestedBlocks(input) {
163
163
  }
164
164
  return { valid: errors.length === 0, errors, warnings };
165
165
  }
166
+ /** Matrix subset (§4a): validate a `grid:` root document — BL-021..BL-025. */
167
+ export function validateGrid(input, options = {}) {
168
+ const errors = [];
169
+ const warnings = [];
170
+ if (!input || typeof input !== 'object') {
171
+ return { valid: false, errors: [{ code: 'BL-001', message: 'Input must be an object' }], warnings };
172
+ }
173
+ const raw = input;
174
+ if ('notation' in raw && raw['notation'] !== 'blocks') {
175
+ errors.push({
176
+ code: 'BL-001',
177
+ message: `notation must be "blocks", got "${String(raw['notation'])}"`,
178
+ });
179
+ }
180
+ if (!('grid' in raw) || !raw['grid'] || typeof raw['grid'] !== 'object') {
181
+ errors.push({ code: 'BL-020', message: 'Missing required root key: grid' });
182
+ return { valid: false, errors, warnings };
183
+ }
184
+ const grid = raw['grid'];
185
+ const rawColumns = grid['columns'];
186
+ const rawRows = grid['rows'];
187
+ if (!Array.isArray(rawColumns) || rawColumns.length === 0) {
188
+ errors.push({ code: 'BL-021', message: 'grid.columns must be a non-empty array' });
189
+ }
190
+ if (!Array.isArray(rawRows) || rawRows.length === 0) {
191
+ errors.push({ code: 'BL-022', message: 'grid.rows must be a non-empty array' });
192
+ }
193
+ if (errors.length > 0)
194
+ return { valid: false, errors, warnings };
195
+ const columnIds = new Set();
196
+ const columns = [];
197
+ rawColumns.forEach((entry, i) => {
198
+ const path = `grid.columns[${i}]`;
199
+ const c = entry;
200
+ if (!entry || typeof entry !== 'object' || !isNonEmptyString(c['id']) || !isNonEmptyString(c['name'])) {
201
+ errors.push({ code: 'BL-023', message: `${path} must have non-empty id and name`, path });
202
+ return;
203
+ }
204
+ const id = c['id'].trim();
205
+ if (columnIds.has(id)) {
206
+ errors.push({ code: 'BL-024', message: `Duplicate column id: "${id}"`, path });
207
+ }
208
+ else {
209
+ columnIds.add(id);
210
+ }
211
+ columns.push({ id, name: c['name'] });
212
+ });
213
+ const rowIds = new Set();
214
+ const rows = [];
215
+ rawRows.forEach((entry, i) => {
216
+ const path = `grid.rows[${i}]`;
217
+ const r = entry;
218
+ if (!entry || typeof entry !== 'object' || !isNonEmptyString(r['id']) || !isNonEmptyString(r['name'])) {
219
+ errors.push({ code: 'BL-023', message: `${path} must have non-empty id and name`, path });
220
+ return;
221
+ }
222
+ const id = r['id'].trim();
223
+ if (rowIds.has(id)) {
224
+ errors.push({ code: 'BL-024', message: `Duplicate row id: "${id}"`, path });
225
+ }
226
+ else {
227
+ rowIds.add(id);
228
+ }
229
+ const rawAssign = r['assign'];
230
+ let assign;
231
+ if (rawAssign !== undefined) {
232
+ if (!rawAssign || typeof rawAssign !== 'object' || Array.isArray(rawAssign)) {
233
+ errors.push({ code: 'BL-023', message: `${path}.assign must be a map when present`, path });
234
+ }
235
+ else {
236
+ assign = rawAssign;
237
+ for (const colId of Object.keys(assign)) {
238
+ if (!columnIds.has(colId)) {
239
+ errors.push({
240
+ code: 'BL-025',
241
+ message: `${path}.assign references unknown column id "${colId}"`,
242
+ path,
243
+ });
244
+ }
245
+ }
246
+ }
247
+ }
248
+ rows.push({ id, name: r['name'], assign });
249
+ });
250
+ if (errors.length > 0)
251
+ return { valid: false, errors, warnings };
252
+ const grid_ = { columns, rows };
253
+ for (const rule of options.rules ?? []) {
254
+ for (const finding of rule.check(grid_)) {
255
+ const entry = { code: rule.ruleId, message: finding.message, path: finding.path };
256
+ if (rule.severity === 'error')
257
+ errors.push(entry);
258
+ else
259
+ warnings.push(entry);
260
+ }
261
+ }
262
+ return { valid: errors.length === 0, errors, warnings };
263
+ }
264
+ /**
265
+ * Root-form dispatcher (§4/§4a): a document declares exactly one of
266
+ * `nested_blocks` (tree form) or `grid` (matrix subset) — never both, never
267
+ * neither (BL-020). Delegates to whichever form is present.
268
+ */
269
+ export function validateBlocks(input, options = {}) {
270
+ if (!input || typeof input !== 'object') {
271
+ return { valid: false, errors: [{ code: 'BL-001', message: 'Input must be an object' }], warnings: [] };
272
+ }
273
+ const raw = input;
274
+ const hasNested = raw['nested_blocks'] !== undefined && raw['nested_blocks'] !== null;
275
+ const hasGrid = raw['grid'] !== undefined && raw['grid'] !== null;
276
+ if (hasNested && hasGrid) {
277
+ return {
278
+ valid: false,
279
+ errors: [{ code: 'BL-020', message: 'Document root must declare exactly one of nested_blocks or grid, not both' }],
280
+ warnings: [],
281
+ };
282
+ }
283
+ if (hasGrid)
284
+ return validateGrid(input, options);
285
+ if (hasNested)
286
+ return validateNestedBlocks(input);
287
+ return {
288
+ valid: false,
289
+ errors: [{ code: 'BL-020', message: 'Document root must declare exactly one of nested_blocks or grid' }],
290
+ warnings: [],
291
+ };
292
+ }
166
293
  /**
167
294
  * Convenience: validate a single `Block` subtree. Used by tests and by
168
295
  * downstream consumers that need to check a sub-document.
@@ -13,11 +13,18 @@ export function validateCapabilityMap(input) {
13
13
  return { valid: false, errors: [{ code: 'CMAP-001', message: 'Input must be an object' }], warnings };
14
14
  }
15
15
  const raw = input;
16
+ // HDR-001/002 — DSM's own rule codes for the same header check CMAP-001
17
+ // already makes (api02/internal/importer/capabilities.go ValidateCapMap),
18
+ // added alongside it (not instead of it) so DSM can map a finding straight
19
+ // back onto its own import-log taxonomy while CMAP-001 stays the richer,
20
+ // pre-existing Studio-native code for this notation's own validator.
16
21
  if (!('notation' in raw)) {
17
22
  errors.push({ code: 'CMAP-001', message: 'Missing required field: notation' });
23
+ errors.push({ code: 'HDR-001', message: 'notation field is missing' });
18
24
  }
19
25
  else if (raw['notation'] !== 'capability-map') {
20
26
  errors.push({ code: 'CMAP-001', message: `notation must be "capability-map", got "${raw['notation']}"` });
27
+ errors.push({ code: 'HDR-002', message: `notation must be "capability-map", got "${raw['notation']}"` });
21
28
  }
22
29
  if (errors.length > 0)
23
30
  return { valid: false, errors, warnings };
@@ -91,6 +98,40 @@ function validateCapabilityTree(nodes, pathPrefix, errors, seenIds) {
91
98
  }
92
99
  if (node['applications'] !== undefined && !Array.isArray(node['applications']))
93
100
  errors.push({ code: 'CMAP-003', message: `${nodePath}: applications must be an array` });
101
+ // LIFECYCLE-001/004 — DSM's own rule codes (capabilities.go
102
+ // ValidateCapMap/validateCapMapNodes) for `valid_from`/`valid_to`, the
103
+ // CONTRACT.md §7 primitive-lifecycle fields. Net new: this notation's own
104
+ // validator has no equivalent check today — `CapabilityNode` (types.ts)
105
+ // does not even declare these fields, since Studio's own schema tracks
106
+ // lifecycle via `target_date` instead. The fields are still read directly
107
+ // off the raw untyped node here because real capability-map documents
108
+ // (e.g. organizations/acme_corp) author them inline per §7 regardless.
109
+ {
110
+ const id = typeof node['id'] === 'string' ? node['id'] : nodePath;
111
+ const validFrom = node['valid_from'];
112
+ const validTo = node['valid_to'];
113
+ let fromDate;
114
+ let toDate;
115
+ if (validFrom !== undefined && validFrom !== null) {
116
+ if (typeof validFrom !== 'string' || !DATE_RE.test(validFrom)) {
117
+ errors.push({ code: 'LIFECYCLE-001', message: `capability ${id} valid_from "${String(validFrom)}" is not a valid YYYY-MM-DD date (must be quoted)` });
118
+ }
119
+ else {
120
+ fromDate = validFrom;
121
+ }
122
+ }
123
+ if (validTo !== undefined && validTo !== null) {
124
+ if (typeof validTo !== 'string' || !DATE_RE.test(validTo)) {
125
+ errors.push({ code: 'LIFECYCLE-001', message: `capability ${id} valid_to "${String(validTo)}" is not a valid YYYY-MM-DD date (must be quoted)` });
126
+ }
127
+ else {
128
+ toDate = validTo;
129
+ }
130
+ }
131
+ if (fromDate && toDate && toDate < fromDate) {
132
+ errors.push({ code: 'LIFECYCLE-004', message: `capability ${id} valid_to ${toDate} is before valid_from ${fromDate}` });
133
+ }
134
+ }
94
135
  if (node['children'] !== undefined) {
95
136
  if (!Array.isArray(node['children'])) {
96
137
  errors.push({ code: 'CMAP-003', message: `${nodePath}: children must be an array` });
@@ -20,3 +20,18 @@ export declare const DEFAULT_EDGE_CURVATURE = 1;
20
20
  * - >1 → progressively stronger arc at that end.
21
21
  */
22
22
  export declare function horizontalCubicEdgePath(sx: number, sy: number, tx: number, ty: number, curvature?: number, entryCurvature?: number): string;
23
+ /**
24
+ * Builds the SVG `d` for a "skip" edge that must arc over an intermediate
25
+ * node sitting on the direct (sx,sy)→(tx,ty) line — e.g. a Network/PSND view
26
+ * where a multi-predecessor activity's nearer predecessor shares a row with
27
+ * one of the columns in between, so the direct line would be drawn straight
28
+ * through that node. Both control points target `bowY` instead of each
29
+ * endpoint's own Y, producing a hump that departs/arrives at an angle rather
30
+ * than the flat horizontal tangent of `horizontalCubicEdgePath`.
31
+ *
32
+ * A cubic's actual apex only reaches part-way from an endpoint Y to the
33
+ * control-point Y (75% at the midpoint for a fully symmetric sy=ty curve),
34
+ * so callers must pick `bowY` well past the obstacle for the apex to clear
35
+ * it — see `render-activities.ts`'s `blockingNodes`/`bowY` for the margin.
36
+ */
37
+ export declare function bowedCubicEdgePath(sx: number, sy: number, tx: number, ty: number, bowY: number, curvature?: number): string;
package/dist/edge-path.js CHANGED
@@ -47,3 +47,22 @@ export function horizontalCubicEdgePath(sx, sy, tx, ty, curvature = DEFAULT_EDGE
47
47
  const entryHandle = baseHandle * (entryCurvature ?? curvature);
48
48
  return `M${sx},${sy} C${sx + exitHandle},${sy} ${tx - entryHandle},${ty} ${tx},${ty}`;
49
49
  }
50
+ /**
51
+ * Builds the SVG `d` for a "skip" edge that must arc over an intermediate
52
+ * node sitting on the direct (sx,sy)→(tx,ty) line — e.g. a Network/PSND view
53
+ * where a multi-predecessor activity's nearer predecessor shares a row with
54
+ * one of the columns in between, so the direct line would be drawn straight
55
+ * through that node. Both control points target `bowY` instead of each
56
+ * endpoint's own Y, producing a hump that departs/arrives at an angle rather
57
+ * than the flat horizontal tangent of `horizontalCubicEdgePath`.
58
+ *
59
+ * A cubic's actual apex only reaches part-way from an endpoint Y to the
60
+ * control-point Y (75% at the midpoint for a fully symmetric sy=ty curve),
61
+ * so callers must pick `bowY` well past the obstacle for the apex to clear
62
+ * it — see `render-activities.ts`'s `blockingNodes`/`bowY` for the margin.
63
+ */
64
+ export function bowedCubicEdgePath(sx, sy, tx, ty, bowY, curvature = DEFAULT_EDGE_CURVATURE) {
65
+ const dx = tx - sx;
66
+ const handle = Math.max(EDGE_MIN_HANDLE, Math.min(Math.abs(dx) * DX_FACTOR, MAX_HANDLE)) * curvature;
67
+ return `M${sx},${sy} C${sx + handle},${bowY} ${tx - handle},${bowY} ${tx},${ty}`;
68
+ }
@@ -104,6 +104,14 @@ export function parseCanonicalFGCA(input) {
104
104
  }
105
105
  const activitiesRaw = asObjectArray(raw['actions']);
106
106
  if (activitiesRaw === null && 'activities' in raw) {
107
+ // DSM's Go ValidateFGCA (fgca.go) still accepts this as the deprecated
108
+ // "activities" key and promotes it with a DGCA-DEPR warning, not an
109
+ // error. Deliberately NOT matched here — PR #320 ("drop deprecated
110
+ // notation aliases fgca/fga/activities/activity-card") already removed
111
+ // that leniency from this validator on purpose. Re-introducing
112
+ // DGCA-DEPR would reverse a deliberate, shipped decision, not fill a
113
+ // gap; see docs/validation.md's "Standalone-element envelope rules"
114
+ // section for the full note.
107
115
  errors.push({ code: 'FGCA-004', message: '"activities" key is not accepted — rename to "actions"', path: 'actions' });
108
116
  }
109
117
  if (factorsRaw === null)
@@ -0,0 +1,8 @@
1
+ import type { RepoFinding, RepoModelInput } from './types.js';
2
+ /**
3
+ * Run the standalone-element envelope checks (GOAL-ELEM-001..003,
4
+ * ACTION-001/002/005) over the loaded element set and append findings.
5
+ * Called from `validateRepoModel` alongside `checkStrategyChainSemantics`.
6
+ * Pure, deterministic order.
7
+ */
8
+ export declare function checkElementHygiene(input: RepoModelInput, findings: RepoFinding[]): void;