@transitrix/diagrams 1.13.3 → 1.14.0

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 (67) hide show
  1. package/dist/activities/action-fields.d.ts +6 -0
  2. package/dist/activities/action-fields.js +34 -0
  3. package/dist/activities/validate.d.ts +3 -1
  4. package/dist/activities/validate.js +36 -34
  5. package/dist/applications/validate.js +27 -26
  6. package/dist/capability-map/validate.d.ts +4 -1
  7. package/dist/capability-map/validate.js +32 -30
  8. package/dist/codex/validate.js +3 -3
  9. package/dist/compliance/canon-catalog.js +1 -1
  10. package/dist/compliance/classify.d.ts +4 -1
  11. package/dist/compliance/classify.js +21 -1
  12. package/dist/compliance/coverage-metric.js +3 -3
  13. package/dist/compliance/impact.js +4 -4
  14. package/dist/compliance/index.d.ts +3 -0
  15. package/dist/compliance/index.js +1 -0
  16. package/dist/compliance/requirement-chain.d.ts +111 -0
  17. package/dist/compliance/requirement-chain.js +501 -0
  18. package/dist/compliance/reverse-index.js +4 -0
  19. package/dist/compliance/types.d.ts +20 -0
  20. package/dist/fgca/parse-canonical.d.ts +0 -25
  21. package/dist/fgca/parse-canonical.js +16 -26
  22. package/dist/fgca/projection-origin.d.ts +2 -0
  23. package/dist/fgca/projection-origin.js +2 -0
  24. package/dist/fgca/resolver.d.ts +0 -8
  25. package/dist/fgca/resolver.js +2 -0
  26. package/dist/products/validate.js +30 -25
  27. package/dist/repo-validate/check-strategy-chain.d.ts +8 -2
  28. package/dist/repo-validate/check-strategy-chain.js +64 -64
  29. package/dist/repo-validate/types.d.ts +2 -0
  30. package/dist/repo-validate/validate-repo.js +2 -0
  31. package/dist/risk/index.d.ts +1 -1
  32. package/dist/risk/types.d.ts +8 -1
  33. package/dist/risk/validate.js +19 -4
  34. package/dist/scenarios/validate.js +4 -0
  35. package/dist/schema-finding.d.ts +3 -0
  36. package/dist/schema-finding.js +6 -0
  37. package/dist/webview/entry.js +7 -2
  38. package/dist/webview/render-goals.js +2 -1
  39. package/package.json +1 -1
  40. package/src/activities/action-fields.ts +31 -0
  41. package/src/activities/validate.ts +37 -34
  42. package/src/applications/validate.ts +27 -26
  43. package/src/capability-map/validate.ts +33 -29
  44. package/src/codex/validate.ts +3 -3
  45. package/src/compliance/canon-catalog.ts +1 -1
  46. package/src/compliance/classify.ts +24 -1
  47. package/src/compliance/coverage-metric.ts +3 -3
  48. package/src/compliance/impact.ts +4 -4
  49. package/src/compliance/index.ts +3 -0
  50. package/src/compliance/requirement-chain.ts +430 -0
  51. package/src/compliance/reverse-index.ts +3 -0
  52. package/src/compliance/types.ts +21 -0
  53. package/src/fgca/parse-canonical.ts +14 -1
  54. package/src/fgca/projection-origin.ts +2 -0
  55. package/src/fgca/resolver.ts +2 -0
  56. package/src/products/validate.ts +31 -25
  57. package/src/repo-validate/check-strategy-chain.ts +69 -64
  58. package/src/repo-validate/types.ts +2 -0
  59. package/src/repo-validate/validate-repo.ts +2 -0
  60. package/src/risk/index.ts +1 -1
  61. package/src/risk/types.ts +9 -1
  62. package/src/risk/validate.ts +19 -5
  63. package/src/scenarios/validate.ts +5 -0
  64. package/src/schema-finding.ts +8 -0
  65. package/src/webview/entry.ts +6 -2
  66. package/src/webview/render-goals.ts +2 -1
  67. package/src/webview/styles.css +8 -0
@@ -1,11 +1,3 @@
1
- /**
2
- * FGCA canon resolver — assembles a flat canonical FGCA document from a
3
- * view_config projection and a canon element/relation store (VP-3).
4
- *
5
- * Output shape matches the flat form that `parseCanonicalFGCA` expects:
6
- * top-level `factors[]`, `goals[]`, `changes[]`, `actions[]` with typed
7
- * string IDs. Filesystem-free — callable from unit tests without vscode.
8
- */
9
1
  export interface FGCAViewConfig {
10
2
  goals?: {
11
3
  filter?: 'all' | 'ids' | 'tags';
@@ -1,3 +1,4 @@
1
+ import { RESOLVED_DGCA } from './projection-origin.js';
1
2
  /**
2
3
  * FGCA canon resolver — assembles a flat canonical FGCA document from a
3
4
  * view_config projection and a canon element/relation store (VP-3).
@@ -137,6 +138,7 @@ export function resolveFGCA(viewDoc, sources) {
137
138
  outOfScopeFactorIds = new Set([...factorElems.keys()].filter((id) => !selectedFactorIds.has(id)));
138
139
  }
139
140
  return {
141
+ [RESOLVED_DGCA]: true,
140
142
  notation: 'dgca',
141
143
  id: viewDoc['id'],
142
144
  name: viewDoc['name'],
@@ -1,11 +1,12 @@
1
+ import { schemaFinding } from '../schema-finding.js';
1
2
  const VALID_TYPES = new Set(['digital_product', 'service', 'platform', 'bundle']);
2
3
  const VALID_STATUSES = new Set(['Draft', 'Active', 'Deprecated']);
3
4
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4
5
  export function validateProductsCatalogue(input) {
5
6
  const errors = [];
6
7
  const warnings = [];
7
- if (!input || typeof input !== 'object') {
8
- return { valid: false, errors: [{ code: 'PROD-001', message: 'Input must be an object' }], warnings };
8
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
9
+ return { valid: false, errors: [schemaFinding('products inline', '$', 'mapping', input)], warnings };
9
10
  }
10
11
  const raw = input;
11
12
  // PROD-001: notation header
@@ -17,75 +18,79 @@ export function validateProductsCatalogue(input) {
17
18
  }
18
19
  if (errors.length > 0)
19
20
  return { valid: false, errors, warnings };
21
+ if (('view_config' in raw || 'view' in raw) && !('products_catalogue' in raw)) {
22
+ return { valid: true, errors, warnings: [{ code: 'NOTATION-SKIP-001',
23
+ message: 'products projection form is unsupported and remains unvalidated.' }] };
24
+ }
20
25
  // PROD-002: catalogue header fields
21
26
  const cat = (raw['products_catalogue'] ?? {});
22
- if (!raw['products_catalogue'] || typeof raw['products_catalogue'] !== 'object') {
23
- errors.push({ code: 'PROD-002', message: 'Missing required field: products_catalogue' });
27
+ if (!raw['products_catalogue'] || typeof raw['products_catalogue'] !== 'object' || Array.isArray(raw['products_catalogue'])) {
28
+ errors.push(schemaFinding('products inline', 'products_catalogue', 'mapping', raw['products_catalogue']));
24
29
  return { valid: false, errors, warnings };
25
30
  }
26
31
  if (!cat['id'] || typeof cat['id'] !== 'string' || !cat['id'].trim()) {
27
- errors.push({ code: 'PROD-002', message: 'products_catalogue.id is required' });
32
+ errors.push(schemaFinding('products inline', 'products_catalogue.id', 'nonempty string', cat['id']));
28
33
  }
29
34
  if (!cat['name'] || typeof cat['name'] !== 'string' || !cat['name'].trim()) {
30
- errors.push({ code: 'PROD-002', message: 'products_catalogue.name is required' });
35
+ errors.push(schemaFinding('products inline', 'products_catalogue.name', 'nonempty string', cat['name']));
31
36
  }
32
37
  if (!cat['updated_at'] || typeof cat['updated_at'] !== 'string') {
33
- errors.push({ code: 'PROD-002', message: 'products_catalogue.updated_at is required' });
38
+ errors.push(schemaFinding('products inline', 'products_catalogue.updated_at', 'YYYY-MM-DD string', cat['updated_at']));
34
39
  }
35
40
  if (errors.length > 0)
36
41
  return { valid: false, errors, warnings };
37
- // PROD-007: updated_at format
42
+ // Schema: updated_at format
38
43
  if (!DATE_RE.test(cat['updated_at'])) {
39
- errors.push({ code: 'PROD-007', message: `products_catalogue.updated_at must be YYYY-MM-DD, got "${cat['updated_at']}"` });
44
+ errors.push(schemaFinding('products inline', 'products_catalogue.updated_at', 'YYYY-MM-DD string', cat['updated_at']));
40
45
  }
41
46
  const products = cat['products'];
42
47
  if (!Array.isArray(products)) {
43
- errors.push({ code: 'PROD-002', message: 'products_catalogue.products must be an array' });
48
+ errors.push(schemaFinding('products inline', 'products_catalogue.products', 'array', cat['products']));
44
49
  return { valid: false, errors, warnings };
45
50
  }
46
- // PROD-008: unique product_id
51
+ // Schema: unique product_id
47
52
  const seenIds = new Set();
48
53
  for (let i = 0; i < products.length; i++) {
49
54
  const rawProduct = products[i];
50
55
  const idx = `products[${i}]`;
51
- if (!rawProduct || typeof rawProduct !== 'object') {
52
- errors.push({ code: 'PROD-003', message: `${idx} must be an object` });
56
+ if (!rawProduct || typeof rawProduct !== 'object' || Array.isArray(rawProduct)) {
57
+ errors.push(schemaFinding('products inline', `${idx}`, 'mapping', rawProduct));
53
58
  continue;
54
59
  }
55
60
  const p = rawProduct;
56
- // PROD-003: required per-product fields
61
+ // Schema: required per-product fields
57
62
  if (!p['product_id'] || typeof p['product_id'] !== 'string' || !p['product_id'].trim()) {
58
- errors.push({ code: 'PROD-003', message: `${idx}: product_id is required` });
63
+ errors.push(schemaFinding('products inline', `${idx}.product_id`, 'nonempty string', p['product_id']));
59
64
  }
60
65
  else {
61
66
  const pid = p['product_id'];
62
67
  if (seenIds.has(pid)) {
63
- errors.push({ code: 'PROD-008', message: `Duplicate product_id: "${pid}"` });
68
+ errors.push(schemaFinding('products inline', `${idx}.product_id`, 'unique identifier', p['product_id']));
64
69
  }
65
70
  seenIds.add(pid);
66
71
  }
67
72
  if (!p['name'] || typeof p['name'] !== 'string' || !p['name'].trim()) {
68
- errors.push({ code: 'PROD-003', message: `${idx}: name is required` });
73
+ errors.push(schemaFinding('products inline', `${idx}.name`, 'nonempty string', p['name']));
69
74
  }
70
75
  if (!p['type']) {
71
- errors.push({ code: 'PROD-003', message: `${idx}: type is required` });
76
+ errors.push(schemaFinding('products inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, p['type']));
72
77
  }
73
78
  if (!p['status']) {
74
- errors.push({ code: 'PROD-003', message: `${idx}: status is required` });
79
+ errors.push(schemaFinding('products inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, p['status']));
75
80
  }
76
- // PROD-004: type enum
81
+ // Schema: type enum
77
82
  if (p['type'] && !VALID_TYPES.has(p['type'])) {
78
- errors.push({ code: 'PROD-004', message: `${idx}: type "${p['type']}" must be one of: digital_product, service, platform, bundle` });
83
+ errors.push(schemaFinding('products inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, p['type']));
79
84
  }
80
- // PROD-005: status enum
85
+ // Schema: status enum
81
86
  if (p['status'] && !VALID_STATUSES.has(p['status'])) {
82
- errors.push({ code: 'PROD-005', message: `${idx}: status "${p['status']}" must be one of: Draft, Active, Deprecated` });
87
+ errors.push(schemaFinding('products inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, p['status']));
83
88
  }
84
- // PROD-006: maturity range
89
+ // Schema: maturity range
85
90
  if (p['maturity'] !== undefined) {
86
91
  const m = p['maturity'];
87
92
  if (typeof m !== 'number' || !Number.isInteger(m) || m < 1 || m > 5) {
88
- errors.push({ code: 'PROD-006', message: `${idx}: maturity must be an integer 1–5, got "${m}"` });
93
+ errors.push(schemaFinding('products inline', `${idx}.maturity`, 'integer in 1–5', p['maturity']));
89
94
  }
90
95
  }
91
96
  }
@@ -1,8 +1,14 @@
1
1
  import type { RepoFinding, RepoModelInput } from './types.js';
2
2
  /**
3
- * Run the strategy-chain semantic checks (GOALS-009..011, ACT-005..009,
4
- * FGCA-008..014 except GOALS-008 — see the module header) over the loaded
3
+ * Run the strategy-chain semantic checks (GOALS-009..011, ACTION-007..011,
4
+ * DGCA-REPO-008..014 except GOALS-008 — see the module header) over the loaded
5
5
  * element set and append findings. Called from `validateRepoModel` after the
6
6
  * structural phases. Pure, deterministic order.
7
7
  */
8
8
  export declare function checkStrategyChainSemantics(input: RepoModelInput, findings: RepoFinding[]): void;
9
+ /** Removed FGCA orphan rules become visible coverage observations, not new aliases. */
10
+ export declare function collectStrategyCoverageObservations(input: RepoModelInput): Array<{
11
+ id: string;
12
+ kind: 'unreferenced';
13
+ message: string;
14
+ }>;
@@ -1,3 +1,4 @@
1
+ import { validateActionFields } from '../activities/action-fields.js';
1
2
  // Strategy-chain semantic checks — the error-severity subset of DSM's Go
2
3
  // `Validate*` functions (`api02/internal/importer/{goals,activities,fgca}.go`
3
4
  // in transitrix-dsm), ported onto the standalone-element repo shape
@@ -5,9 +6,8 @@
5
6
  // can drop those Go functions without regressing the checks they enforce
6
7
  // today.
7
8
  //
8
- // Rule codes are DSM's own (`GOALS-010`, `ACT-006`..`009`, `FGCA-008`..`011`)
9
- // — not invented here — so DSM can map a CLI finding straight back onto its
10
- // import-log taxonomy (`RepoFinding.ruleId`).
9
+ // ACTION identities follow the versioned element contract; schedule ACT codes
10
+ // remain separate. Stored historical findings must retain their original codes.
11
11
  //
12
12
  // Scope — DSM's full rule set is now ported except GOALS-008 (see below).
13
13
  // `RepoFinding` grew a `severity` field precisely so DSM's warn-severity
@@ -43,20 +43,20 @@
43
43
  //
44
44
  // Ported (error-severity, blocking):
45
45
  // GOALS-010 — GOAL `parent` chain contains a cycle.
46
- // ACT-006 — ACTION `predecessors` graph contains a cycle.
47
- // ACT-007 — ACTION lists itself as its own predecessor.
48
- // ACT-008 — ACTION `start_date`/`end_date` unparseable, or end before start.
49
- // ACT-009 — ACTION numeric field (`duration`/`duration_days`, `labor_cost`,
46
+ // ACTION-008 — ACTION `predecessors` graph contains a cycle.
47
+ // ACTION-009 — ACTION lists itself as its own predecessor.
48
+ // ACTION-010 — ACTION `start_date`/`end_date` unparseable, or end before start.
49
+ // ACTION-011 — ACTION numeric field (`duration`/`duration_days`, `labor_cost`,
50
50
  // `resources_cost`, `effort`, `score`) is negative.
51
- // FGCA-008 — GOAL.factors references an undefined DRIVER.
52
- // FGCA-009 — CHANGE.goals references an undefined GOAL.
53
- // FGCA-010 — ACTION.delivers_changes references an undefined CHANGE.
54
- // FGCA-011 — ACTION.goals references an undefined GOAL.
51
+ // DGCA-REPO-008 — GOAL.factors references an undefined DRIVER.
52
+ // DGCA-REPO-009 — CHANGE.goals references an undefined GOAL.
53
+ // DGCA-REPO-010 — ACTION.delivers_changes references an undefined CHANGE.
54
+ // DGCA-REPO-011 — ACTION.goals references an undefined GOAL.
55
55
  //
56
56
  // Ported (warning-severity, advisory):
57
57
  // GOALS-009 — GOAL.parent is set but does not resolve to a known GOAL (orphan).
58
58
  // GOALS-011 — GOAL has no `parent` and `level` >= 1 (backlog).
59
- // ACT-005 — ACTION.predecessors entry or ACTION.parent does not resolve
59
+ // ACTION-007 — ACTION.predecessors entry or ACTION.parent does not resolve
60
60
  // to a known ACTION (orphan).
61
61
  // FGCA-012 — a DRIVER is not referenced by any GOAL.factors (unreferenced).
62
62
  // FGCA-013 — a GOAL is not referenced by any CHANGE.goals or ACTION.goals
@@ -228,32 +228,32 @@ function checkGoalParentResolution(goals, findings) {
228
228
  }
229
229
  }
230
230
  }
231
- /** ACT-007 — an ACTION cannot list itself as its own predecessor. */
231
+ /** ACTION-009 — an ACTION cannot list itself as its own predecessor. */
232
232
  function checkActionSelfPredecessor(actions, findings) {
233
233
  for (const a of actions) {
234
234
  if (readStringArray(a.data, 'predecessors').includes(a.id)) {
235
235
  findings.push({
236
236
  scope: PScope,
237
237
  id: a.id,
238
- ruleId: 'ACT-007',
239
- message: `ACT-007: action '${a.id}' lists itself as a predecessor.`,
238
+ ruleId: 'ACTION-009',
239
+ message: `ACTION-009: action '${a.id}' lists itself as a predecessor.`,
240
240
  });
241
241
  }
242
242
  }
243
243
  }
244
- /** ACT-006 — the ACTION `predecessors` graph must not contain a cycle. */
244
+ /** ACTION-008 — the ACTION `predecessors` graph must not contain a cycle. */
245
245
  function checkActionPredecessorCycle(actions, findings) {
246
246
  const cyc = findPredecessorCycle(actions);
247
247
  if (cyc) {
248
248
  findings.push({
249
249
  scope: PScope,
250
250
  id: cyc,
251
- ruleId: 'ACT-006',
252
- message: `ACT-006: predecessor graph contains a cycle involving action '${cyc}'.`,
251
+ ruleId: 'ACTION-008',
252
+ message: `ACTION-008: predecessor graph contains a cycle involving action '${cyc}'.`,
253
253
  });
254
254
  }
255
255
  }
256
- /** ACT-005 — an ACTION `predecessors` entry or `parent` that does not resolve
256
+ /** ACTION-007 — an ACTION `predecessors` entry or `parent` that does not resolve
257
257
  * to a known ACTION is an orphan reference (warning — advisory in DSM, same
258
258
  * as the goal-parent orphan checks above). */
259
259
  function checkActionOrphanReferences(actions, findings) {
@@ -264,9 +264,9 @@ function checkActionOrphanReferences(actions, findings) {
264
264
  findings.push({
265
265
  scope: PScope,
266
266
  id: a.id,
267
- ruleId: 'ACT-005',
267
+ ruleId: 'ACTION-007',
268
268
  severity: 'warning',
269
- message: `ACT-005: action '${a.id}' predecessor '${p}' does not resolve to a known action.`,
269
+ message: `ACTION-007: action '${a.id}' predecessor '${p}' does not resolve to a known action.`,
270
270
  });
271
271
  }
272
272
  }
@@ -275,9 +275,9 @@ function checkActionOrphanReferences(actions, findings) {
275
275
  findings.push({
276
276
  scope: PScope,
277
277
  id: a.id,
278
- ruleId: 'ACT-005',
278
+ ruleId: 'ACTION-007',
279
279
  severity: 'warning',
280
- message: `ACT-005: action '${a.id}' parent '${parent}' does not resolve to a known action.`,
280
+ message: `ACTION-007: action '${a.id}' parent '${parent}' does not resolve to a known action.`,
281
281
  });
282
282
  }
283
283
  }
@@ -296,33 +296,33 @@ function parseIsoDate(value) {
296
296
  }
297
297
  return dt;
298
298
  }
299
- /** ACT-008 — `start_date`/`end_date` must be valid ISO dates, and `end_date`
299
+ /** ACTION-010 — `start_date`/`end_date` must be valid ISO dates, and `end_date`
300
300
  * must not be before `start_date` (equal is allowed — e.g. a milestone). */
301
301
  function checkActionDates(actions, findings) {
302
302
  for (const a of actions) {
303
- const startRaw = readString(a.data, 'start_date');
304
- const endRaw = readString(a.data, 'end_date');
303
+ const startRaw = a.data.start_date == null ? undefined : a.data.start_date;
304
+ const endRaw = a.data.end_date == null ? undefined : a.data.end_date;
305
305
  let start;
306
306
  let end;
307
307
  if (startRaw !== undefined) {
308
- start = parseIsoDate(startRaw);
308
+ start = typeof startRaw === 'string' ? parseIsoDate(startRaw) : undefined;
309
309
  if (!start) {
310
310
  findings.push({
311
311
  scope: PScope,
312
312
  id: a.id,
313
- ruleId: 'ACT-008',
314
- message: `ACT-008: action '${a.id}' start_date '${startRaw}' is not a valid YYYY-MM-DD date.`,
313
+ ruleId: 'ACTION-010',
314
+ message: `ACTION-010: action '${a.id}' start_date '${startRaw}' is not a valid YYYY-MM-DD date.`,
315
315
  });
316
316
  }
317
317
  }
318
318
  if (endRaw !== undefined) {
319
- end = parseIsoDate(endRaw);
319
+ end = typeof endRaw === 'string' ? parseIsoDate(endRaw) : undefined;
320
320
  if (!end) {
321
321
  findings.push({
322
322
  scope: PScope,
323
323
  id: a.id,
324
- ruleId: 'ACT-008',
325
- message: `ACT-008: action '${a.id}' end_date '${endRaw}' is not a valid YYYY-MM-DD date.`,
324
+ ruleId: 'ACTION-010',
325
+ message: `ACTION-010: action '${a.id}' end_date '${endRaw}' is not a valid YYYY-MM-DD date.`,
326
326
  });
327
327
  }
328
328
  }
@@ -330,34 +330,22 @@ function checkActionDates(actions, findings) {
330
330
  findings.push({
331
331
  scope: PScope,
332
332
  id: a.id,
333
- ruleId: 'ACT-008',
334
- message: `ACT-008: action '${a.id}' end_date '${endRaw}' is before start_date '${startRaw}'.`,
333
+ ruleId: 'ACTION-010',
334
+ message: `ACTION-010: action '${a.id}' end_date '${endRaw}' is before start_date '${startRaw}'.`,
335
335
  });
336
336
  }
337
337
  }
338
338
  }
339
- /** ACT-009 — numeric scheduling/cost fields must not be negative. `duration`
340
- * and `duration_days` are both checked — the canonical field is `duration`
341
- * (elements/24-action.md §2), but `duration_days` is an accepted alias in
342
- * this codebase's document-form validator (activities/validate.ts) and is
343
- * the field acme_corp's own ACTION elements actually use. */
344
- function checkActionNegativeNumbers(actions, findings) {
345
- const fields = ['duration', 'duration_days', 'labor_cost', 'resources_cost', 'effort', 'score'];
339
+ /** Shared ACTION type and versioned numeric checks. */
340
+ function checkActionFields(actions, findings, version) {
346
341
  for (const a of actions) {
347
- for (const field of fields) {
348
- const v = readFiniteNumber(a.data, field);
349
- if (v !== undefined && v < 0) {
350
- findings.push({
351
- scope: PScope,
352
- id: a.id,
353
- ruleId: 'ACT-009',
354
- message: `ACT-009: action '${a.id}' field '${field}' is negative (${v}).`,
355
- });
356
- }
342
+ for (const error of validateActionFields(a.data, version)) {
343
+ findings.push({ scope: PScope, id: a.id, ruleId: error.code,
344
+ message: `${a.doc.path}: canonical action '${a.id}' ${error.message}` });
357
345
  }
358
346
  }
359
347
  }
360
- /** FGCA-008..011 — inline strategy-chain cross-references must resolve within
348
+ /** DGCA-REPO-008..011 — inline strategy-chain cross-references must resolve within
361
349
  * the repo: GOAL.factors -> DRIVER, CHANGE.goals -> GOAL, ACTION.goals ->
362
350
  * GOAL, ACTION.delivers_changes -> CHANGE (ELEMENT_PRIMITIVES.md §7.1-§7.4). */
363
351
  function checkStrategyChainReferences(goals, actions, drivers, changes, findings) {
@@ -370,8 +358,8 @@ function checkStrategyChainReferences(goals, actions, drivers, changes, findings
370
358
  findings.push({
371
359
  scope: PScope,
372
360
  id: g.id,
373
- ruleId: 'FGCA-008',
374
- message: `FGCA-008: goal '${g.id}' references undefined driver '${f}'.`,
361
+ ruleId: 'DGCA-REPO-008',
362
+ message: `DGCA-REPO-008: goal '${g.id}' references undefined driver '${f}'.`,
375
363
  });
376
364
  }
377
365
  }
@@ -382,8 +370,8 @@ function checkStrategyChainReferences(goals, actions, drivers, changes, findings
382
370
  findings.push({
383
371
  scope: PScope,
384
372
  id: c.id,
385
- ruleId: 'FGCA-009',
386
- message: `FGCA-009: change '${c.id}' references undefined goal '${g}'.`,
373
+ ruleId: 'DGCA-REPO-009',
374
+ message: `DGCA-REPO-009: change '${c.id}' references undefined goal '${g}'.`,
387
375
  });
388
376
  }
389
377
  }
@@ -394,8 +382,8 @@ function checkStrategyChainReferences(goals, actions, drivers, changes, findings
394
382
  findings.push({
395
383
  scope: PScope,
396
384
  id: a.id,
397
- ruleId: 'FGCA-010',
398
- message: `FGCA-010: action '${a.id}' references undefined change '${c}'.`,
385
+ ruleId: 'DGCA-REPO-010',
386
+ message: `DGCA-REPO-010: action '${a.id}' references undefined change '${c}'.`,
399
387
  });
400
388
  }
401
389
  }
@@ -404,8 +392,8 @@ function checkStrategyChainReferences(goals, actions, drivers, changes, findings
404
392
  findings.push({
405
393
  scope: PScope,
406
394
  id: a.id,
407
- ruleId: 'FGCA-011',
408
- message: `FGCA-011: action '${a.id}' references undefined goal '${g}'.`,
395
+ ruleId: 'DGCA-REPO-011',
396
+ message: `DGCA-REPO-011: action '${a.id}' references undefined goal '${g}'.`,
409
397
  });
410
398
  }
411
399
  }
@@ -469,8 +457,8 @@ function checkStrategyChainOrphans(goals, actions, drivers, changes, findings) {
469
457
  }
470
458
  }
471
459
  /**
472
- * Run the strategy-chain semantic checks (GOALS-009..011, ACT-005..009,
473
- * FGCA-008..014 except GOALS-008 — see the module header) over the loaded
460
+ * Run the strategy-chain semantic checks (GOALS-009..011, ACTION-007..011,
461
+ * DGCA-REPO-008..014 except GOALS-008 — see the module header) over the loaded
474
462
  * element set and append findings. Called from `validateRepoModel` after the
475
463
  * structural phases. Pure, deterministic order.
476
464
  */
@@ -485,7 +473,19 @@ export function checkStrategyChainSemantics(input, findings) {
485
473
  checkActionPredecessorCycle(actions, findings);
486
474
  checkActionOrphanReferences(actions, findings);
487
475
  checkActionDates(actions, findings);
488
- checkActionNegativeNumbers(actions, findings);
476
+ checkActionFields(actions, findings, input.methodologyVersion);
489
477
  checkStrategyChainReferences(goals, actions, drivers, changes, findings);
490
- checkStrategyChainOrphans(goals, actions, drivers, changes, findings);
478
+ if (!usesCoverageObservations(input))
479
+ checkStrategyChainOrphans(goals, actions, drivers, changes, findings);
480
+ }
481
+ function usesCoverageObservations(input) {
482
+ return /^\d+\./.test(input.methodologyVersion ?? '') && Number(input.methodologyVersion.split('.')[0]) >= 5;
483
+ }
484
+ /** Removed FGCA orphan rules become visible coverage observations, not new aliases. */
485
+ export function collectStrategyCoverageObservations(input) {
486
+ if (!usesCoverageObservations(input))
487
+ return [];
488
+ const findings = [];
489
+ checkStrategyChainOrphans(collectByNotation(input.elements, isGoalNotation), collectByNotation(input.elements, isActionNotation), collectByNotation(input.elements, isDriverNotation), collectByNotation(input.elements, isChangeNotation), findings);
490
+ return findings.map(f => ({ id: f.id, kind: 'unreferenced', message: f.message.replace(/^FGCA-01[234]: /, '') }));
491
491
  }
@@ -40,6 +40,8 @@ export interface RepoDoc {
40
40
  * canon zone exactly as `lint.py` partitions them (`canon/elements/**` vs
41
41
  * `canon/relations/**`). */
42
42
  export interface RepoModelInput {
43
+ /** Catalogue manifest version; document spec_version never selects compatibility. */
44
+ methodologyVersion?: string;
43
45
  /** Documents found under `canon/elements/**`. */
44
46
  elements: RepoDoc[];
45
47
  /** Documents found under `canon/relations/**`. */
@@ -129,6 +129,7 @@ function checkReferentialIntegrity(input, findings) {
129
129
  findings.push({
130
130
  scope: PScope,
131
131
  id: relId,
132
+ ruleId: 'REL-002',
132
133
  message: `Referential integrity: relation '${relId || doc.path}' endpoint '${fromId}' (from) does not resolve to a known element.`,
133
134
  });
134
135
  }
@@ -136,6 +137,7 @@ function checkReferentialIntegrity(input, findings) {
136
137
  findings.push({
137
138
  scope: PScope,
138
139
  id: relId,
140
+ ruleId: 'REL-002',
139
141
  message: `Referential integrity: relation '${relId || doc.path}' endpoint '${toId}' (to) does not resolve to a known element.`,
140
142
  });
141
143
  }
@@ -1,4 +1,4 @@
1
- export type { Risk, RiskLevel } from './types.js';
1
+ export type { Risk, RiskLevel, RiskScale } from './types.js';
2
2
  export { RISK_LEVELS } from './types.js';
3
3
  export { validateRisk } from './validate.js';
4
4
  export type { RiskValidateOptions } from './validate.js';
@@ -1,11 +1,18 @@
1
1
  import type { GateChecks } from '../requirement/types.js';
2
2
  /** Likelihood / impact / residual severity vocabulary (§7.26). */
3
- export type RiskLevel = 'low' | 'medium' | 'high';
3
+ export type RiskLevel = 'low' | 'medium' | 'high' | number;
4
+ export interface RiskScale {
5
+ id: string;
6
+ min: number;
7
+ max: number;
8
+ direction: 'higher' | 'lower';
9
+ }
4
10
  export declare const RISK_LEVELS: readonly RiskLevel[];
5
11
  export interface Risk {
6
12
  notation: 'risk';
7
13
  id: string;
8
14
  name: string;
15
+ risk_scale?: RiskScale;
9
16
  likelihood: RiskLevel;
10
17
  impact: RiskLevel;
11
18
  residual: RiskLevel;
@@ -3,7 +3,7 @@
3
3
  // Codes:
4
4
  // RISK-001 — shape / id grammar / required envelope + per-type fields
5
5
  // (likelihood, impact, residual, owner_role, threatens).
6
- // RISK-002 — likelihood / impact / residual outside {low, medium, high}.
6
+ // RISK-002 — invalid qualitative/numeric degree or adopter scale.
7
7
  // RISK-003 — threatens is empty, or an entry does not resolve to an
8
8
  // admitted element in canon (no TYPE restriction — any core
9
9
  // element may be threatened).
@@ -50,14 +50,29 @@ export function validateRisk(input, options = {}) {
50
50
  if (!('valid_to' in r) || !(typeof r.valid_to === 'string' || r.valid_to === null)) {
51
51
  errors.push({ code: 'RISK-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
52
52
  }
53
- // likelihood / impact / residual — required (RISK-001) + enum (RISK-002).
54
- for (const f of ['likelihood', 'impact', 'residual']) {
53
+ // Authored numeric degrees use one explicitly declared adopter scale.
54
+ const fields = ['likelihood', 'impact', 'residual'];
55
+ const scale = r.risk_scale;
56
+ const finite = (v) => typeof v === 'number' && Number.isFinite(v);
57
+ const validScale = scale !== null && typeof scale === 'object' && !Array.isArray(scale)
58
+ && typeof scale.id === 'string' && scale.id.trim().length > 0
59
+ && finite(scale.min) && finite(scale.max) && scale.min < scale.max
60
+ && (scale.direction === 'higher' || scale.direction === 'lower');
61
+ if ((scale !== undefined || fields.some(f => typeof r[f] === 'number')) && !validScale) {
62
+ errors.push({ code: 'RISK-002', message: 'risk_scale requires id, finite min < max, and direction higher or lower.', path: 'risk_scale' });
63
+ }
64
+ for (const f of fields) {
55
65
  const v = r[f];
56
66
  if (v === undefined || v === null || v === '') {
57
67
  errors.push({ code: 'RISK-001', message: `${f} is required.`, path: f });
58
68
  }
69
+ else if (typeof v === 'number') {
70
+ if (!finite(v) || (validScale && (v < scale.min || v > scale.max))) {
71
+ errors.push({ code: 'RISK-002', message: `${f} must be finite and within risk_scale bounds.`, path: f });
72
+ }
73
+ }
59
74
  else if (!RISK_LEVELS.includes(v)) {
60
- errors.push({ code: 'RISK-002', message: `${f} "${String(v)}" must be one of ${RISK_LEVELS.join(', ')}.`, path: f });
75
+ errors.push({ code: 'RISK-002', message: `${f} must be low, medium, high, or a number with risk_scale.`, path: f });
61
76
  }
62
77
  }
63
78
  // threatens — required non-empty list (RISK-001), each entry resolves (RISK-003).
@@ -24,6 +24,10 @@ export function validateScenario(input) {
24
24
  }
25
25
  if (errors.length > 0)
26
26
  return { valid: false, errors, warnings };
27
+ if (('view_config' in raw || 'view' in raw || 'scenarios' in raw) && !('scenario' in raw)) {
28
+ return { valid: true, errors, warnings: [{ code: 'NOTATION-SKIP-001',
29
+ message: 'scenarios projection or scenario-set form is unsupported and remains unvalidated.' }] };
30
+ }
27
31
  const scn = raw['scenario'];
28
32
  if (!scn || typeof scn !== 'object') {
29
33
  errors.push({ code: 'SCN-002', message: 'Missing required field: scenario' });
@@ -0,0 +1,3 @@
1
+ import type { ValidationError } from './validation-types.js';
2
+ /** Shared schema identity; callers supply the owning form and exact field. */
3
+ export declare function schemaFinding(form: string, path: string, expected: string, actual: unknown): ValidationError;
@@ -0,0 +1,6 @@
1
+ /** Shared schema identity; callers supply the owning form and exact field. */
2
+ export function schemaFinding(form, path, expected, actual) {
3
+ const type = actual === null ? 'null' : Array.isArray(actual) ? 'array' : typeof actual;
4
+ return { code: 'SCHEMA_INVALID', path,
5
+ message: `${form} field '${path}': expected ${expected}; actual ${type} (${String(actual)}).` };
6
+ }
@@ -185,8 +185,13 @@ function dispatchValidate(kind, doc) {
185
185
  return renderFromValidation('process-map', validateProcessMap(doc), () => renderProcessMapHtml(doc.process_map));
186
186
  case 'scenarios':
187
187
  return renderFromValidation('scenarios', validateScenario(doc), () => renderScenarioHtml(doc.scenario));
188
- case 'capability-map':
189
- return renderFromValidation('capability-map', validateCapabilityMap(doc), () => renderCapabilityMapHtml(doc.capability_map));
188
+ case 'capability-map': {
189
+ const validation = validateCapabilityMap(doc);
190
+ const unresolved = validation.warnings.find(w => w.code === 'NOTATION-SKIP-001');
191
+ if (validation.valid && unresolved)
192
+ return errorResult('capability-map', unresolved.code, unresolved.message);
193
+ return renderFromValidation('capability-map', validation, () => renderCapabilityMapHtml(doc.capability_map));
194
+ }
190
195
  default:
191
196
  return errorResult(kind, 'NOTATION-NOT-WIRED', `Notation '${kind}' is not wired into the IntelliJ bundle yet.`);
192
197
  }
@@ -41,7 +41,8 @@ export function renderGoalsSvg(tree, options = {}) {
41
41
  const title = treeName
42
42
  ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
43
43
  : '';
44
- return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, edgeStyle, title, embedCssTheme: 'transitrix' });
44
+ // Reserve a separate header row; the theme centers text on its y coordinate.
45
+ return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, edgeStyle, title, topInset: title ? PAD : 0, embedCssTheme: 'transitrix' });
45
46
  }
46
47
  /**
47
48
  * The single goals SVG emitter shared by every host. Takes an already-computed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitrix/diagrams",
3
- "version": "1.13.3",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
5
  "description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
6
6
  "license": "MIT",
@@ -0,0 +1,31 @@
1
+ /** ACTION field types and the manifest-selected 7.0.0 numeric boundary. */
2
+ export function validateActionFields(
3
+ action: Record<string, unknown>, methodologyVersion?: string,
4
+ ): Array<{ code: string; message: string; path: string }> {
5
+ const errors: Array<{ code: string; message: string; path: string }> = [];
6
+ const version = /^(\d+)\.(\d+)\.(\d+)(-[^+]+)?(?:\+.*)?$/.exec(methodologyVersion ?? '');
7
+ const nonnegative = version !== null && (Number(version[1]) > 7 ||
8
+ (Number(version[1]) === 7 && (Number(version[2]) > 0 || Number(version[3]) > 0 || !version[4])));
9
+ const type = (value: unknown): string => value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
10
+ const schema = (path: string, expected: string, value: unknown) => errors.push({
11
+ code: 'SCHEMA_INVALID', path,
12
+ message: `ACTION field '${path}' expected ${expected}, actual ${type(value)} (${String(value)}).`,
13
+ });
14
+ for (const field of ['duration', 'duration_days', 'labor_cost', 'resources_cost', 'effort', 'score', 'sort']) {
15
+ const value = action[field];
16
+ if (value === undefined || (value === null && (field === 'duration' || field === 'duration_days'))) continue;
17
+ const integer = field === 'score' || field === 'sort';
18
+ if (typeof value !== 'number' || !Number.isFinite(value) || (integer && !Number.isInteger(value))) {
19
+ schema(field, integer ? 'finite integer' : 'finite number', value);
20
+ } else if (nonnegative && field !== 'sort' && value < 0) {
21
+ errors.push({ code: 'ACTION-011', path: field, message: `ACTION field '${field}' is negative (${value}).` });
22
+ }
23
+ }
24
+ if (action.predecessors !== undefined) {
25
+ if (!Array.isArray(action.predecessors)) schema('predecessors', 'array of strings', action.predecessors);
26
+ else action.predecessors.forEach((value, index) => {
27
+ if (typeof value !== 'string') schema(`predecessors[${index}]`, 'string', value);
28
+ });
29
+ }
30
+ return errors;
31
+ }