@transitrix/diagrams 1.6.0 → 1.8.1

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 (131) hide show
  1. package/dist/activities/layout.js +7 -7
  2. package/dist/activities/types.d.ts +4 -0
  3. package/dist/capability-map/dsm-schema.d.ts +1 -1
  4. package/dist/capability-map/dsm-schema.js +1 -1
  5. package/dist/capability-map/layout-tree.d.ts +5 -1
  6. package/dist/capability-map/layout-tree.js +11 -9
  7. package/dist/capability-map/render-capability-tree.d.ts +2 -9
  8. package/dist/capability-map/render-capability-tree.js +16 -28
  9. package/dist/codex/__tests__/example.test.d.ts +1 -0
  10. package/dist/codex/__tests__/example.test.js +33 -0
  11. package/dist/codex/__tests__/validate.test.d.ts +1 -0
  12. package/dist/codex/__tests__/validate.test.js +68 -0
  13. package/dist/codex/index.d.ts +4 -0
  14. package/dist/codex/index.js +2 -0
  15. package/dist/codex/types.d.ts +7 -0
  16. package/dist/codex/types.js +8 -0
  17. package/dist/codex/validate.d.ts +11 -0
  18. package/dist/codex/validate.js +103 -0
  19. package/dist/compliance/__tests__/canon-catalog.test.d.ts +1 -0
  20. package/dist/compliance/__tests__/canon-catalog.test.js +60 -0
  21. package/dist/compliance/__tests__/view-resolution.test.d.ts +1 -0
  22. package/dist/compliance/__tests__/view-resolution.test.js +59 -0
  23. package/dist/compliance/canon-catalog.d.ts +19 -0
  24. package/dist/compliance/canon-catalog.js +59 -0
  25. package/dist/compliance/coverage-metric.d.ts +12 -0
  26. package/dist/compliance/coverage-metric.js +35 -0
  27. package/dist/compliance/impact.d.ts +7 -1
  28. package/dist/compliance/impact.js +53 -0
  29. package/dist/compliance/index.d.ts +6 -4
  30. package/dist/compliance/index.js +3 -2
  31. package/dist/constraint/__tests__/example.test.d.ts +1 -0
  32. package/dist/constraint/__tests__/example.test.js +14 -0
  33. package/dist/constraint/__tests__/validate.test.d.ts +1 -0
  34. package/dist/constraint/__tests__/validate.test.js +58 -0
  35. package/dist/constraint/index.d.ts +4 -0
  36. package/dist/constraint/index.js +2 -0
  37. package/dist/constraint/types.d.ts +4 -0
  38. package/dist/constraint/types.js +2 -0
  39. package/dist/constraint/validate.d.ts +7 -0
  40. package/dist/constraint/validate.js +115 -0
  41. package/dist/fgca/__tests__/parse-canonical.test.js +31 -0
  42. package/dist/fgca/parse-canonical.d.ts +1 -1
  43. package/dist/fgca/parse-canonical.js +18 -4
  44. package/dist/fgca/preview-layout.d.ts +8 -4
  45. package/dist/fgca/preview-layout.js +13 -13
  46. package/dist/goals/GoalNode.d.ts +2 -1
  47. package/dist/goals/GoalNode.js +7 -2
  48. package/dist/goals/GoalTreeView.d.ts +3 -1
  49. package/dist/goals/GoalTreeView.js +31 -14
  50. package/dist/goals/__tests__/GoalTreeView.test.js +41 -0
  51. package/dist/goals/__tests__/mutations.test.js +39 -0
  52. package/dist/goals/__tests__/parse-canonical.test.js +2 -0
  53. package/dist/goals/layout.js +2 -1
  54. package/dist/goals/mutations.js +6 -6
  55. package/dist/goals/parse-canonical.d.ts +5 -0
  56. package/dist/goals/parse-canonical.js +5 -0
  57. package/dist/goals/types.d.ts +2 -0
  58. package/dist/node-size-presets.d.ts +50 -0
  59. package/dist/node-size-presets.js +103 -0
  60. package/dist/process-blueprint/layout.js +4 -47
  61. package/dist/repo-validate/types.js +2 -3
  62. package/dist/webview/__tests__/entity-text-layout.test.d.ts +1 -0
  63. package/dist/webview/__tests__/entity-text-layout.test.js +43 -0
  64. package/dist/webview/__tests__/entry.test.js +3 -3
  65. package/dist/webview/__tests__/render-blocks.test.js +1 -7
  66. package/dist/webview/__tests__/render-goals.test.js +17 -3
  67. package/dist/webview/entity-text-layout.d.ts +95 -0
  68. package/dist/webview/entity-text-layout.js +279 -0
  69. package/dist/webview/entry.js +7 -3
  70. package/dist/webview/notation-style.d.ts +1 -1
  71. package/dist/webview/notation-style.js +1 -1
  72. package/dist/webview/render-activities.d.ts +3 -1
  73. package/dist/webview/render-activities.js +29 -32
  74. package/dist/webview/render-blocks.d.ts +4 -1
  75. package/dist/webview/render-blocks.js +27 -108
  76. package/dist/webview/render-fgca.d.ts +15 -8
  77. package/dist/webview/render-fgca.js +41 -44
  78. package/dist/webview/render-goals.d.ts +2 -0
  79. package/dist/webview/render-goals.js +23 -37
  80. package/dist/webview/render-process-blueprint.d.ts +4 -10
  81. package/dist/webview/render-process-blueprint.js +18 -64
  82. package/package.json +1 -1
  83. package/src/activities/layout.ts +12 -7
  84. package/src/activities/types.ts +4 -0
  85. package/src/capability-map/dsm-schema.ts +1 -1
  86. package/src/capability-map/index.ts +1 -1
  87. package/src/capability-map/layout-tree.ts +16 -8
  88. package/src/capability-map/render-capability-tree.ts +18 -30
  89. package/src/codex/__tests__/example.test.ts +40 -0
  90. package/src/codex/__tests__/validate.test.ts +84 -0
  91. package/src/codex/index.ts +4 -0
  92. package/src/codex/types.ts +13 -0
  93. package/src/codex/validate.ts +125 -0
  94. package/src/compliance/__tests__/canon-catalog.test.ts +68 -0
  95. package/src/compliance/__tests__/view-resolution.test.ts +80 -0
  96. package/src/compliance/canon-catalog.ts +76 -0
  97. package/src/compliance/coverage-metric.ts +50 -0
  98. package/src/compliance/impact.ts +67 -1
  99. package/src/compliance/index.ts +10 -1
  100. package/src/constraint/__tests__/example.test.ts +19 -0
  101. package/src/constraint/__tests__/validate.test.ts +68 -0
  102. package/src/constraint/index.ts +4 -0
  103. package/src/constraint/types.ts +5 -0
  104. package/src/constraint/validate.ts +131 -0
  105. package/src/fgca/__tests__/parse-canonical.test.ts +34 -0
  106. package/src/fgca/parse-canonical.ts +17 -4
  107. package/src/fgca/preview-layout.ts +21 -15
  108. package/src/goals/GoalNode.tsx +8 -1
  109. package/src/goals/GoalTreeView.tsx +61 -27
  110. package/src/goals/__tests__/GoalTreeView.test.tsx +48 -0
  111. package/src/goals/__tests__/mutations.test.ts +42 -0
  112. package/src/goals/__tests__/parse-canonical.test.ts +2 -0
  113. package/src/goals/layout.ts +2 -1
  114. package/src/goals/mutations.ts +6 -6
  115. package/src/goals/parse-canonical.ts +6 -0
  116. package/src/goals/types.ts +2 -0
  117. package/src/node-size-presets.ts +157 -0
  118. package/src/process-blueprint/layout.ts +4 -48
  119. package/src/repo-validate/types.ts +2 -3
  120. package/src/webview/__tests__/entity-text-layout.test.ts +52 -0
  121. package/src/webview/__tests__/entry.test.ts +3 -3
  122. package/src/webview/__tests__/render-blocks.test.ts +1 -9
  123. package/src/webview/__tests__/render-goals.test.ts +17 -3
  124. package/src/webview/entity-text-layout.ts +355 -0
  125. package/src/webview/entry.ts +8 -4
  126. package/src/webview/notation-style.ts +1 -1
  127. package/src/webview/render-activities.ts +38 -27
  128. package/src/webview/render-blocks.ts +35 -118
  129. package/src/webview/render-fgca.ts +59 -44
  130. package/src/webview/render-goals.ts +24 -35
  131. package/src/webview/render-process-blueprint.ts +30 -65
@@ -0,0 +1,59 @@
1
+ // CanonCatalog builder — repo-scope cross-reference resolution (#518 Phase C3).
2
+ //
3
+ // Scans admitted YAML documents and builds the `{ typeOf }` catalogue that
4
+ // `validateRequirement` / `validateAssertion` accept for REQ-002 and
5
+ // ASSERT-002..005 resolution.
6
+ import { typeOfId } from '../typed-id.js';
7
+ import { emptyCanon, ingestComplianceDoc } from './classify.js';
8
+ function strId(doc) {
9
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc))
10
+ return undefined;
11
+ const id = doc.id;
12
+ return typeof id === 'string' && id.trim() !== '' ? id.trim() : undefined;
13
+ }
14
+ /** Admit one parsed document into the id → TYPE map. */
15
+ export function admitDocumentToCatalog(map, doc) {
16
+ const id = strId(doc);
17
+ if (!id)
18
+ return undefined;
19
+ const fromPrefix = typeOfId(id);
20
+ if (fromPrefix) {
21
+ map.set(id, fromPrefix);
22
+ return id;
23
+ }
24
+ if (doc !== null && typeof doc === 'object' && !Array.isArray(doc)) {
25
+ const d = doc;
26
+ if (d.zone === 'codex') {
27
+ const explicit = typeof d.type === 'string' ? d.type.trim().toUpperCase() : undefined;
28
+ if (explicit) {
29
+ map.set(id, explicit);
30
+ return id;
31
+ }
32
+ }
33
+ }
34
+ return undefined;
35
+ }
36
+ /** Build a read-only `CanonCatalog` from a populated id → TYPE map. */
37
+ export function catalogFromMap(map) {
38
+ return { typeOf: (id) => map.get(id) };
39
+ }
40
+ /** Ingest compliance projections and admit cross-reference targets from a batch
41
+ * of already-parsed YAML documents (no IO). */
42
+ export function buildComplianceScan(docs) {
43
+ const complianceCanon = emptyCanon();
44
+ const catalogMap = new Map();
45
+ const pathById = new Map();
46
+ for (const { path, data } of docs) {
47
+ const ingested = ingestComplianceDoc(complianceCanon, data);
48
+ if (ingested)
49
+ pathById.set(ingested, path);
50
+ const admitted = admitDocumentToCatalog(catalogMap, data);
51
+ if (admitted && !pathById.has(admitted))
52
+ pathById.set(admitted, path);
53
+ }
54
+ return {
55
+ complianceCanon,
56
+ catalog: catalogFromMap(catalogMap),
57
+ pathById,
58
+ };
59
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ComplianceCanon } from './classify.js';
2
+ import type { CanonCatalog } from '../typed-id.js';
2
3
  /** Declarative filter resolved at build time from the canon's codex catalogue. */
3
4
  export interface CoverageMetricRegimesFilter {
4
5
  jurisdiction?: string[];
@@ -73,6 +74,17 @@ export interface CoverageMatrix {
73
74
  rows: CoverageRow[];
74
75
  thresholds: CoverageMetricThresholds;
75
76
  }
77
+ /** Repo-scope cross-reference finding for a coverage-metric view (#518 C3). */
78
+ export interface CoverageViewFinding {
79
+ code: string;
80
+ severity: 'error' | 'warning';
81
+ message: string;
82
+ }
83
+ /**
84
+ * Repo-scope cross-reference checks for a parsed coverage-metric view config.
85
+ * Surfaces unresolved product and regime ids before `buildCoverageMatrix` runs.
86
+ */
87
+ export declare function collectCoverageViewResolutionFindings(config: CoverageMetricConfig, catalog: CanonCatalog, complianceCanon: ComplianceCanon): CoverageViewFinding[];
76
88
  /**
77
89
  * Parse and validate a raw (YAML-parsed) document into a `CoverageMetricConfig`.
78
90
  *
@@ -5,6 +5,41 @@
5
5
  // across the scoped products, and what is the RAG status against the configured
6
6
  // thresholds.
7
7
  import { buildComplianceIndex } from './reverse-index.js';
8
+ /**
9
+ * Repo-scope cross-reference checks for a parsed coverage-metric view config.
10
+ * Surfaces unresolved product and regime ids before `buildCoverageMatrix` runs.
11
+ */
12
+ export function collectCoverageViewResolutionFindings(config, catalog, complianceCanon) {
13
+ const findings = [];
14
+ const codexIds = new Set(complianceCanon.codex.map((c) => c.id));
15
+ for (const id of config.subjects?.products ?? []) {
16
+ const t = catalog.typeOf(id);
17
+ if (t === undefined) {
18
+ findings.push({
19
+ code: 'COVMET-REF',
20
+ severity: 'error',
21
+ message: `subjects.products: "${id}" does not resolve to an admitted artefact.`,
22
+ });
23
+ }
24
+ else if (t !== 'PRODUCT') {
25
+ findings.push({
26
+ code: 'COVMET-REF',
27
+ severity: 'error',
28
+ message: `subjects.products: "${id}" resolves to ${t}, not PRODUCT.`,
29
+ });
30
+ }
31
+ }
32
+ for (const id of config.regimes?.include ?? []) {
33
+ if (!codexIds.has(id)) {
34
+ findings.push({
35
+ code: 'COVMET-REF',
36
+ severity: 'error',
37
+ message: `regimes.include: "${id}" does not resolve to an admitted codex artefact.`,
38
+ });
39
+ }
40
+ }
41
+ return findings;
42
+ }
8
43
  // ── Parser ───────────────────────────────────────────────────────────────────
9
44
  function parseRegimes(raw) {
10
45
  const warnings = [];
@@ -1,5 +1,6 @@
1
1
  import type { AssertionStatus } from '../assertion/types.js';
2
2
  import type { ComplianceCanon } from './classify.js';
3
+ import type { CanonCatalog } from '../typed-id.js';
3
4
  import type { IndexAssertion, IndexRequirement, ObjectDetailInput, DeadlineStatus } from './types.js';
4
5
  /** Filter selecting REQUIREMENTs by codex source (jurisdiction / regime keys
5
6
  * are accepted for forward compatibility but not yet honoured — the canon
@@ -57,12 +58,17 @@ export interface ImpactColumn {
57
58
  * Used to enforce the subject type label invariant (§5.2, COMPIMP-010). */
58
59
  subjectType: 'product' | 'process' | 'capability';
59
60
  }
60
- /** A structural validation finding emitted by the matrix builder. */
61
61
  export interface ImpactFinding {
62
62
  code: string;
63
63
  severity: 'error' | 'warning';
64
64
  message: string;
65
65
  }
66
+ /**
67
+ * Repo-scope cross-reference checks for a parsed compliance-impact view config
68
+ * (#518 Phase C3). Surfaces unresolved subject, obligation, and regime ids before
69
+ * `buildImpactMatrix` runs.
70
+ */
71
+ export declare function collectImpactViewResolutionFindings(config: ImpactViewConfig, catalog: CanonCatalog, complianceCanon: ComplianceCanon): ImpactFinding[];
66
72
  /**
67
73
  * Grouping options for matrix columns.
68
74
  *
@@ -7,6 +7,59 @@
7
7
  // (`product` columns × `obligation` rows). Stage / task grouping is planned
8
8
  // for CV-3a and intentionally out of scope here.
9
9
  import { buildComplianceIndex } from './reverse-index.js';
10
+ /**
11
+ * Repo-scope cross-reference checks for a parsed compliance-impact view config
12
+ * (#518 Phase C3). Surfaces unresolved subject, obligation, and regime ids before
13
+ * `buildImpactMatrix` runs.
14
+ */
15
+ export function collectImpactViewResolutionFindings(config, catalog, complianceCanon) {
16
+ const findings = [];
17
+ const codexIds = new Set(complianceCanon.codex.map((c) => c.id));
18
+ const subjectChecks = [
19
+ { ids: config.subjects.products, expectedType: 'PRODUCT', path: 'subjects.products' },
20
+ { ids: config.subjects.processes, expectedType: 'PROCESS', path: 'subjects.processes' },
21
+ { ids: config.subjects.capabilities, expectedType: 'CAPABILITY', path: 'subjects.capabilities' },
22
+ ];
23
+ for (const { ids, expectedType, path } of subjectChecks) {
24
+ for (const id of ids ?? []) {
25
+ const t = catalog.typeOf(id);
26
+ if (t === undefined) {
27
+ findings.push({
28
+ code: 'COMPIMP-REF',
29
+ severity: 'error',
30
+ message: `${path}: "${id}" does not resolve to an admitted artefact.`,
31
+ });
32
+ }
33
+ else if (t !== expectedType) {
34
+ findings.push({
35
+ code: 'COMPIMP-REF',
36
+ severity: 'error',
37
+ message: `${path}: "${id}" resolves to ${t}, not ${expectedType}.`,
38
+ });
39
+ }
40
+ }
41
+ }
42
+ for (const id of config.obligations.include ?? []) {
43
+ const t = catalog.typeOf(id);
44
+ if (t === undefined || t !== 'REQUIREMENT') {
45
+ findings.push({
46
+ code: 'COMPIMP-REF',
47
+ severity: 'error',
48
+ message: `obligations.include: "${id}" does not resolve to an admitted REQUIREMENT.`,
49
+ });
50
+ }
51
+ }
52
+ for (const id of config.obligations.filter?.derived_from_codex ?? []) {
53
+ if (!codexIds.has(id)) {
54
+ findings.push({
55
+ code: 'COMPIMP-REF',
56
+ severity: 'error',
57
+ message: `obligations.filter.derived_from_codex: "${id}" does not resolve to an admitted codex artefact.`,
58
+ });
59
+ }
60
+ }
61
+ return findings;
62
+ }
10
63
  // ── Pinned defaults ─────────────────────────────────────────────────────────
11
64
  /**
12
65
  * Explicit defaults for every optional field in ImpactViewConfig.
@@ -5,13 +5,15 @@ export { buildGapReport } from './gap-report.js';
5
5
  export type { GapReport, GapReportOptions } from './gap-report.js';
6
6
  export { emptyCanon, ingestComplianceDoc } from './classify.js';
7
7
  export type { ComplianceCanon, ComplianceProduct, ComplianceCodexDoc } from './classify.js';
8
+ export { admitDocumentToCatalog, buildComplianceScan, catalogFromMap, } from './canon-catalog.js';
9
+ export type { ComplianceScanResult, ScannedYamlDoc } from './canon-catalog.js';
8
10
  export { renderComplianceMarkdown } from './markdown.js';
9
11
  export type { ReportScope, MarkdownOptions } from './markdown.js';
10
12
  export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
11
13
  export type { HtmlOptions, ImpactMatrixHtmlOptions } from './html.js';
12
- export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
13
- export type { ImpactCell, ImpactColumn, ImpactEmptyCellLabels, ImpactGrouping, ImpactMatrix, ImpactObligationFilter, ImpactStatusDisplay, ImpactSubjects, ImpactViewConfig, ParseImpactViewConfigResult, } from './impact.js';
14
+ export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, collectImpactViewResolutionFindings, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
15
+ export type { ImpactCell, ImpactColumn, ImpactEmptyCellLabels, ImpactFinding, ImpactGrouping, ImpactMatrix, ImpactObligationFilter, ImpactStatusDisplay, ImpactSubjects, ImpactViewConfig, ParseImpactViewConfigResult, } from './impact.js';
14
16
  export { assertionToScoringElement, complianceConfidenceHeader, requirementToScoringElement, scoreComplianceView, } from './confidence.js';
15
- export { parseCoverageMetricConfig, buildCoverageMatrix, } from './coverage-metric.js';
16
- export type { CoverageMetricConfig, CoverageMetricRegimes, CoverageMetricRegimesFilter, CoverageMetricSubjects, CoverageMetricThresholds, CoverageMatrix, CoverageRow, ParseCoverageMetricResult, RagStatus, } from './coverage-metric.js';
17
+ export { parseCoverageMetricConfig, buildCoverageMatrix, collectCoverageViewResolutionFindings, } from './coverage-metric.js';
18
+ export type { CoverageMetricConfig, CoverageMetricRegimes, CoverageMetricRegimesFilter, CoverageMetricSubjects, CoverageMetricThresholds, CoverageMatrix, CoverageRow, CoverageViewFinding, ParseCoverageMetricResult, RagStatus, } from './coverage-metric.js';
17
19
  export type { ComplianceIndex, ComplianceIndexInput, DeadlineStatus, IndexRequirement, IndexAssertion, LawTree, LawTreeRequirement, ProductView, ProductRequirementStatus, ObjectDetailDef, ObjectDetailInput, RequirementTrace, TraceAssertionRow, TraceElementCatalog, TraceElementRef, TraceSourceRef, } from './types.js';
@@ -3,8 +3,9 @@ export { buildLawTree, buildProductView } from './views.js';
3
3
  export { buildRequirementTrace, buildTraceElementCatalog } from './trace.js';
4
4
  export { buildGapReport } from './gap-report.js';
5
5
  export { emptyCanon, ingestComplianceDoc } from './classify.js';
6
+ export { admitDocumentToCatalog, buildComplianceScan, catalogFromMap, } from './canon-catalog.js';
6
7
  export { renderComplianceMarkdown } from './markdown.js';
7
8
  export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
8
- export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
9
+ export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, collectImpactViewResolutionFindings, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
9
10
  export { assertionToScoringElement, complianceConfidenceHeader, requirementToScoringElement, scoreComplianceView, } from './confidence.js';
10
- export { parseCoverageMetricConfig, buildCoverageMatrix, } from './coverage-metric.js';
11
+ export { parseCoverageMetricConfig, buildCoverageMatrix, collectCoverageViewResolutionFindings, } from './coverage-metric.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import path from 'node:path';
5
+ import yaml from 'js-yaml';
6
+ import { validateConstraint } from '../validate.js';
7
+ const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../tests/fixtures/notation-corpus/constraint');
8
+ describe('constraint worked example (notation-corpus)', () => {
9
+ it('validates CONSTRAINT-GDPR-RESIDENCY-1.yaml without error', () => {
10
+ const data = yaml.load(readFileSync(path.join(dir, 'CONSTRAINT-GDPR-RESIDENCY-1.yaml'), 'utf-8'));
11
+ const v = validateConstraint(data);
12
+ expect(v.valid, JSON.stringify(v.errors)).toBe(true);
13
+ });
14
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validateConstraint } from '../validate.js';
3
+ function valid() {
4
+ return {
5
+ notation: 'constraint',
6
+ id: 'CONSTRAINT-GDPR-RESIDENCY-1',
7
+ name: 'EU customer personal data must be stored within EU/EEA jurisdictions',
8
+ statement: 'Personal data of EU customers MUST NOT be persisted outside EU / EEA jurisdictions.',
9
+ status: 'active',
10
+ zone: 'canon',
11
+ admitted_at: '2026-05-29',
12
+ admitted_by: 'v.korobeinikov',
13
+ gate_checks: { uniqueness: 'pass', consistency: 'pass', completeness: 'pass' },
14
+ valid_from: '2018-05-25',
15
+ valid_to: null,
16
+ severity: 'mandatory',
17
+ owner_role: 'ROLE-DPO-1',
18
+ applies_to: ['APPLICATION-CRM-1'],
19
+ };
20
+ }
21
+ const codes = (input, opts) => validateConstraint(input, opts).errors.map((e) => e.code);
22
+ describe('validateConstraint — positive', () => {
23
+ it('accepts a well-formed constraint', () => {
24
+ const r = validateConstraint(valid());
25
+ expect(r.valid, JSON.stringify(r.errors)).toBe(true);
26
+ });
27
+ });
28
+ describe('validateConstraint — CONST-001', () => {
29
+ it('flags missing required fields and wrong notation', () => {
30
+ const r = valid();
31
+ delete r.statement;
32
+ expect(codes(r)).toContain('CONST-001');
33
+ expect(codes({ ...valid(), notation: 'requirement' })).toContain('CONST-001');
34
+ expect(codes({ ...valid(), id: 'CONSTRAINT-001' })).toContain('CONST-001');
35
+ });
36
+ });
37
+ describe('validateConstraint — CONST-002/003', () => {
38
+ it('flags invalid status and severity', () => {
39
+ expect(codes({ ...valid(), status: 'enabled' })).toContain('CONST-002');
40
+ expect(codes({ ...valid(), severity: 'critical' })).toContain('CONST-003');
41
+ });
42
+ });
43
+ describe('validateConstraint — CONST-004/005 (catalog)', () => {
44
+ it('flags unresolved applies_to and owner_role with catalog', () => {
45
+ const catalog = { typeOf: () => undefined };
46
+ expect(codes(valid(), { catalog })).toContain('CONST-004');
47
+ expect(codes(valid(), { catalog })).toContain('CONST-005');
48
+ });
49
+ it('does not flag CONST-004 without catalog for well-formed applies_to', () => {
50
+ expect(codes(valid())).not.toContain('CONST-004');
51
+ });
52
+ it('resolves applies_to when catalogue admits the target', () => {
53
+ const catalog = {
54
+ typeOf: (id) => (id === 'APPLICATION-CRM-1' ? 'APPLICATION' : id === 'ROLE-DPO-1' ? 'ROLE' : undefined),
55
+ };
56
+ expect(validateConstraint(valid(), { catalog }).valid).toBe(true);
57
+ });
58
+ });
@@ -0,0 +1,4 @@
1
+ export { CONSTRAINT_STATUSES, CONSTRAINT_SEVERITIES } from './types.js';
2
+ export type { ConstraintStatus, ConstraintSeverity } from './types.js';
3
+ export { validateConstraint } from './validate.js';
4
+ export type { ConstraintValidateOptions } from './validate.js';
@@ -0,0 +1,2 @@
1
+ export { CONSTRAINT_STATUSES, CONSTRAINT_SEVERITIES } from './types.js';
2
+ export { validateConstraint } from './validate.js';
@@ -0,0 +1,4 @@
1
+ export declare const CONSTRAINT_STATUSES: readonly ["active", "proposed", "deprecated", "retired"];
2
+ export type ConstraintStatus = (typeof CONSTRAINT_STATUSES)[number];
3
+ export declare const CONSTRAINT_SEVERITIES: readonly ["mandatory", "recommended", "advisory"];
4
+ export type ConstraintSeverity = (typeof CONSTRAINT_SEVERITIES)[number];
@@ -0,0 +1,2 @@
1
+ export const CONSTRAINT_STATUSES = ['active', 'proposed', 'deprecated', 'retired'];
2
+ export const CONSTRAINT_SEVERITIES = ['mandatory', 'recommended', 'advisory'];
@@ -0,0 +1,7 @@
1
+ import type { ValidationResult } from '../validation-types.js';
2
+ import { type CanonCatalog } from '../typed-id.js';
3
+ export interface ConstraintValidateOptions {
4
+ /** When provided, CONST-004/005 enforce artefact existence for cross-refs. */
5
+ catalog?: CanonCatalog;
6
+ }
7
+ export declare function validateConstraint(input: unknown, options?: ConstraintValidateOptions): ValidationResult;
@@ -0,0 +1,115 @@
1
+ // CONSTRAINT validator — methodology ELEMENT_PRIMITIVES.md §7.13 (sibling to
2
+ // 15-requirement.md). Implements CONST-001..005 for the compliance suite (#518 C4).
3
+ import { typeOfId, isCanonicalIdOfType } from '../typed-id.js';
4
+ import { CONSTRAINT_STATUSES, CONSTRAINT_SEVERITIES } from './types.js';
5
+ const REQUIRED_FIELDS = [
6
+ 'notation',
7
+ 'name',
8
+ 'statement',
9
+ 'status',
10
+ 'zone',
11
+ 'admitted_at',
12
+ 'admitted_by',
13
+ 'gate_checks',
14
+ 'valid_from',
15
+ ];
16
+ export function validateConstraint(input, options = {}) {
17
+ const errors = [];
18
+ const warnings = [];
19
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
20
+ errors.push({ code: 'CONST-001', message: 'Constraint must be a YAML mapping.' });
21
+ return { valid: false, errors, warnings };
22
+ }
23
+ const c = input;
24
+ if (!isCanonicalIdOfType(c.id, 'CONSTRAINT')) {
25
+ errors.push({
26
+ code: 'CONST-001',
27
+ message: `id "${String(c.id)}" must match CONSTRAINT-[<middle>-]<INTEGER>.`,
28
+ path: 'id',
29
+ });
30
+ }
31
+ if (c.notation !== 'constraint') {
32
+ errors.push({ code: 'CONST-001', message: 'notation must be the fixed value "constraint".', path: 'notation' });
33
+ }
34
+ for (const f of REQUIRED_FIELDS) {
35
+ if (f === 'notation')
36
+ continue;
37
+ if (f === 'gate_checks') {
38
+ if (c.gate_checks === null || typeof c.gate_checks !== 'object') {
39
+ errors.push({ code: 'CONST-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
40
+ }
41
+ continue;
42
+ }
43
+ const v = c[f];
44
+ if (typeof v !== 'string' || v.trim() === '') {
45
+ errors.push({ code: 'CONST-001', message: `${f} is required.`, path: f });
46
+ }
47
+ }
48
+ if (c.zone !== 'canon') {
49
+ errors.push({ code: 'CONST-001', message: 'zone must be "canon" for a CONSTRAINT.', path: 'zone' });
50
+ }
51
+ if (!('valid_to' in c) || !(typeof c.valid_to === 'string' || c.valid_to === null)) {
52
+ errors.push({ code: 'CONST-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
53
+ }
54
+ const status = typeof c.status === 'string' ? c.status.trim() : '';
55
+ if (status && !CONSTRAINT_STATUSES.includes(status)) {
56
+ errors.push({
57
+ code: 'CONST-002',
58
+ message: `status must be one of ${CONSTRAINT_STATUSES.join(', ')}.`,
59
+ path: 'status',
60
+ });
61
+ }
62
+ if (c.severity !== undefined) {
63
+ const severity = typeof c.severity === 'string' ? c.severity.trim() : '';
64
+ if (!severity || !CONSTRAINT_SEVERITIES.includes(severity)) {
65
+ errors.push({
66
+ code: 'CONST-003',
67
+ message: `severity must be one of ${CONSTRAINT_SEVERITIES.join(', ')}.`,
68
+ path: 'severity',
69
+ });
70
+ }
71
+ }
72
+ if (c.applies_to !== undefined) {
73
+ if (!Array.isArray(c.applies_to)) {
74
+ errors.push({ code: 'CONST-001', message: 'applies_to must be a list of typed IDs.', path: 'applies_to' });
75
+ }
76
+ else {
77
+ c.applies_to.forEach((ref, i) => {
78
+ const type = typeOfId(ref);
79
+ if (!type) {
80
+ errors.push({
81
+ code: 'CONST-004',
82
+ message: `applies_to[${i}] "${String(ref)}" is not a resolvable typed ID.`,
83
+ path: `applies_to[${i}]`,
84
+ });
85
+ return;
86
+ }
87
+ if (options.catalog && options.catalog.typeOf(ref) === undefined) {
88
+ errors.push({
89
+ code: 'CONST-004',
90
+ message: `applies_to[${i}] "${String(ref)}" does not resolve to an admitted artefact.`,
91
+ path: `applies_to[${i}]`,
92
+ });
93
+ }
94
+ });
95
+ }
96
+ }
97
+ if (c.owner_role !== undefined) {
98
+ const role = c.owner_role;
99
+ if (typeof role !== 'string' || !isCanonicalIdOfType(role, 'ROLE')) {
100
+ errors.push({
101
+ code: 'CONST-005',
102
+ message: `owner_role "${String(role)}" must be a typed ROLE id.`,
103
+ path: 'owner_role',
104
+ });
105
+ }
106
+ else if (options.catalog && options.catalog.typeOf(role) === undefined) {
107
+ errors.push({
108
+ code: 'CONST-005',
109
+ message: `owner_role "${role}" does not resolve to an admitted artefact.`,
110
+ path: 'owner_role',
111
+ });
112
+ }
113
+ }
114
+ return { valid: errors.length === 0, errors, warnings };
115
+ }
@@ -32,6 +32,29 @@ describe('parseCanonicalFGCA', () => {
32
32
  expect(r.parsed?.changes).toHaveLength(1);
33
33
  expect(r.parsed?.activities).toHaveLength(1);
34
34
  });
35
+ it('accepts ACTION-* ids (methodology 1.0) alongside legacy ACTIVITY-*', () => {
36
+ const r = parseCanonicalFGCA({
37
+ ...VALID,
38
+ actions: [
39
+ { id: 'ACTION-CRM-EU-1', name: 'CRM rollout', changes: ['CHANGE-1'] },
40
+ { id: 'ACTIVITY-2', name: 'Legacy workstream', changes: ['CHANGE-1'] },
41
+ ],
42
+ });
43
+ expect(r.valid, JSON.stringify(r.errors)).toBe(true);
44
+ expect(r.parsed?.activities.map((a) => a.id)).toEqual(['ACTION-CRM-EU-1', 'ACTIVITY-2']);
45
+ });
46
+ it('defaults changes to [] when view_config.layers.changes is off (DGA-in-dgca)', () => {
47
+ const r = parseCanonicalFGCA({
48
+ ...VALID,
49
+ changes: undefined,
50
+ view_config: { layers: { changes: 'off' } },
51
+ actions: [
52
+ { id: 'ACTION-DISCOVERY-1', name: 'Gap assessment', goals: ['GOAL-1'] },
53
+ ],
54
+ });
55
+ expect(r.valid, JSON.stringify(r.errors)).toBe(true);
56
+ expect(r.parsed?.changes).toEqual([]);
57
+ });
35
58
  it('FGCA-001: rejects non-object input', () => {
36
59
  expect(parseCanonicalFGCA(null).errors[0].code).toBe('FGCA-001');
37
60
  expect(parseCanonicalFGCA('string').errors[0].code).toBe('FGCA-001');
@@ -178,6 +201,14 @@ describe('parseCanonicalFGA', () => {
178
201
  expect(r.parsed?.goals).toHaveLength(1);
179
202
  expect(r.parsed?.activities).toHaveLength(1);
180
203
  });
204
+ it('accepts ACTION-* ids in FGA mode', () => {
205
+ const r = parseCanonicalFGA({
206
+ ...VALID_FGA,
207
+ actions: [{ id: 'ACTION-GDPR-DSR-WORKFLOW-1', name: 'DSR workflow', goals: ['GOAL-1'] }],
208
+ });
209
+ expect(r.valid, JSON.stringify(r.errors)).toBe(true);
210
+ expect(r.parsed?.activities[0].id).toBe('ACTION-GDPR-DSR-WORKFLOW-1');
211
+ });
181
212
  it('populates activity.goal_id from activity.goals[] (the FGA edge-driving field)', () => {
182
213
  // The "FGA nodes render, no edges" bug: activities had no resolvable
183
214
  // goal_id, so the renderer drew no goal → activity edges.
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * - Flat top-level arrays, no `fgca:` wrapper.
8
8
  * - Typed string IDs per `IDS_AND_REFERENCES.md` (`FACTOR-1`,
9
- * `GOAL-RET-1`, `CHANGE-1`, `ACTIVITY-ONBOARD-1`).
9
+ * `GOAL-RET-1`, `CHANGE-1`, `ACTION-ONBOARD-1` (legacy `ACTIVITY-*` accepted).
10
10
  * - Plural canonical-direction cross-references: `goal.factors`,
11
11
  * `change.goals`, `activity.changes`, `activity.goals`.
12
12
  * - Per-notation validation codes `FGCA-001 … FGCA-015`.
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * - Flat top-level arrays, no `fgca:` wrapper.
8
8
  * - Typed string IDs per `IDS_AND_REFERENCES.md` (`FACTOR-1`,
9
- * `GOAL-RET-1`, `CHANGE-1`, `ACTIVITY-ONBOARD-1`).
9
+ * `GOAL-RET-1`, `CHANGE-1`, `ACTION-ONBOARD-1` (legacy `ACTIVITY-*` accepted).
10
10
  * - Plural canonical-direction cross-references: `goal.factors`,
11
11
  * `change.goals`, `activity.changes`, `activity.goals`.
12
12
  * - Per-notation validation codes `FGCA-001 … FGCA-015`.
@@ -31,7 +31,8 @@ const DRIVER_ID_RE = /^DRIVER(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
31
31
  const FACTOR_OR_DRIVER_ID_RE = /^(FACTOR|DRIVER)(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
32
32
  const GOAL_ID_RE = /^GOAL(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
33
33
  const CHANGE_ID_RE = /^CHANGE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
34
- const ACTIVITY_ID_RE = /^ACTIVITY(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
34
+ // ACTION is canonical since methodology 1.0; ACTIVITY accepted for legacy docs.
35
+ const ACTION_OR_ACTIVITY_ID_RE = /^(ACTION|ACTIVITY)(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
35
36
  const FGCA_DOC_ID_RE = /^(FGCA|DGCA)(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
36
37
  const CONSTRAINT_ID_RE = /^CONSTRAINT(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
37
38
  function isNonEmptyString(v) {
@@ -42,6 +43,16 @@ function asObjectArray(v) {
42
43
  return null;
43
44
  return v.map((el) => (el && typeof el === 'object' ? el : null));
44
45
  }
46
+ /** DGA mode (`view_config.layers.changes: off`) may omit the changes layer. */
47
+ function isChangesLayerOff(raw) {
48
+ const vc = raw['view_config'];
49
+ if (!vc || typeof vc !== 'object')
50
+ return false;
51
+ const layers = vc['layers'];
52
+ if (!layers || typeof layers !== 'object')
53
+ return false;
54
+ return layers['changes'] === 'off';
55
+ }
45
56
  /**
46
57
  * Validate canonical FGCA YAML and, on success, return an internal-form
47
58
  * `FGCADoc` ready for `buildFGCALayout`.
@@ -87,7 +98,10 @@ export function parseCanonicalFGCA(input) {
87
98
  }
88
99
  const factorsRaw = asObjectArray(raw['factors']);
89
100
  const goalsRaw = asObjectArray(raw['goals']);
90
- const changesRaw = asObjectArray(raw['changes']);
101
+ let changesRaw = asObjectArray(raw['changes']);
102
+ if (changesRaw === null && raw['changes'] === undefined && isChangesLayerOff(raw)) {
103
+ changesRaw = [];
104
+ }
91
105
  const activitiesRaw = asObjectArray(raw['actions']);
92
106
  if (activitiesRaw === null && 'activities' in raw) {
93
107
  errors.push({ code: 'FGCA-004', message: '"activities" key is not accepted — rename to "actions"', path: 'actions' });
@@ -145,7 +159,7 @@ export function parseCanonicalFGCA(input) {
145
159
  factors.forEach((el, i) => checkLayerElement(el, `factors[${i}]`, FACTOR_OR_DRIVER_ID_RE, factorIds));
146
160
  goals.forEach((el, i) => checkLayerElement(el, `goals[${i}]`, GOAL_ID_RE, goalIds));
147
161
  changes.forEach((el, i) => checkLayerElement(el, `changes[${i}]`, CHANGE_ID_RE, changeIds));
148
- activities.forEach((el, i) => checkLayerElement(el, `activities[${i}]`, ACTIVITY_ID_RE, activityIds));
162
+ activities.forEach((el, i) => checkLayerElement(el, `activities[${i}]`, ACTION_OR_ACTIVITY_ID_RE, activityIds));
149
163
  if (errors.length > 0)
150
164
  return { valid: false, errors, warnings };
151
165
  // Cross-refs.
@@ -23,7 +23,7 @@ export interface FGCAPreviewActivity {
23
23
  name: string;
24
24
  goal_id?: number | string | null;
25
25
  }
26
- /** Structural input the preview layout needs — a subset of the parsed FGCA/FGA doc. */
26
+ /** Structural input the preview layout needs — a subset of the parsed DGCA/DGA doc. */
27
27
  export interface FGCAPreviewDoc {
28
28
  factors: FGCAPreviewFactor[];
29
29
  goals: FGCAPreviewGoal[];
@@ -31,7 +31,7 @@ export interface FGCAPreviewDoc {
31
31
  activities: FGCAPreviewActivity[];
32
32
  }
33
33
  export interface FGCAPreviewLayoutOptions {
34
- /** FGA hides the Changes column and links Goal → Activity directly. */
34
+ /** DGA hides the Changes column and links Goal → Activity directly. */
35
35
  hideChanges?: boolean;
36
36
  /** Horizontal gap (px) between columns. Default matches the historical hardcoded value. */
37
37
  colGap?: number;
@@ -39,11 +39,15 @@ export interface FGCAPreviewLayoutOptions {
39
39
  rowGap?: number;
40
40
  /** Trim to a level cap or a single root goal (vkgeorgia/strategy#77). Defaults to 'all'. */
41
41
  scope?: Scope;
42
+ /** Entity node width (px). Default {@link FGCA_NODE_W}. */
43
+ nodeWidth?: number;
44
+ /** Entity node height (px). Default {@link FGCA_NODE_H}. */
45
+ nodeHeight?: number;
42
46
  }
43
47
  /**
44
- * Trims an FGCA/FGA doc to a scope (vkgeorgia/strategy#77).
48
+ * Trims a DGCA/DGA doc to a scope.
45
49
  *
46
- * FGCA/FGA goals are flat (no parent_id), so:
50
+ * DGCA/DGA goals are flat (no parent_id), so:
47
51
  * - 'level' → goals with `(level ?? 0) <= maxLevel`.
48
52
  * - 'root' → the single goal whose id matches `rootGoalId` (empty when absent).
49
53
  *