@transitrix/diagrams 1.14.0-rc.0 → 1.15.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 (58) 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/coverage-metric.js +3 -3
  10. package/dist/compliance/impact.js +4 -4
  11. package/dist/fgca/parse-canonical.d.ts +0 -25
  12. package/dist/fgca/parse-canonical.js +16 -26
  13. package/dist/fgca/projection-origin.d.ts +2 -0
  14. package/dist/fgca/projection-origin.js +2 -0
  15. package/dist/fgca/resolver.d.ts +0 -8
  16. package/dist/fgca/resolver.js +2 -0
  17. package/dist/products/validate.js +30 -25
  18. package/dist/repo-validate/check-strategy-chain.d.ts +8 -2
  19. package/dist/repo-validate/check-strategy-chain.js +64 -64
  20. package/dist/repo-validate/types.d.ts +2 -0
  21. package/dist/repo-validate/validate-repo.js +2 -0
  22. package/dist/risk/index.d.ts +1 -1
  23. package/dist/risk/types.d.ts +8 -1
  24. package/dist/risk/validate.js +19 -4
  25. package/dist/scenarios/validate.js +4 -0
  26. package/dist/schema-finding.d.ts +3 -0
  27. package/dist/schema-finding.js +6 -0
  28. package/dist/standalone/history.d.ts +3 -0
  29. package/dist/standalone/history.js +67 -0
  30. package/dist/standalone/validate.d.ts +9 -0
  31. package/dist/standalone/validate.js +146 -0
  32. package/dist/webview/entry.js +7 -2
  33. package/dist/webview/render-goals.js +2 -1
  34. package/package.json +1 -1
  35. package/src/activities/action-fields.ts +31 -0
  36. package/src/activities/validate.ts +37 -34
  37. package/src/applications/validate.ts +27 -26
  38. package/src/capability-map/validate.ts +33 -29
  39. package/src/codex/validate.ts +3 -3
  40. package/src/compliance/coverage-metric.ts +3 -3
  41. package/src/compliance/impact.ts +4 -4
  42. package/src/fgca/parse-canonical.ts +14 -1
  43. package/src/fgca/projection-origin.ts +2 -0
  44. package/src/fgca/resolver.ts +2 -0
  45. package/src/products/validate.ts +31 -25
  46. package/src/repo-validate/check-strategy-chain.ts +69 -64
  47. package/src/repo-validate/types.ts +2 -0
  48. package/src/repo-validate/validate-repo.ts +2 -0
  49. package/src/risk/index.ts +1 -1
  50. package/src/risk/types.ts +9 -1
  51. package/src/risk/validate.ts +19 -5
  52. package/src/scenarios/validate.ts +5 -0
  53. package/src/schema-finding.ts +8 -0
  54. package/src/standalone/history.ts +45 -0
  55. package/src/standalone/validate.ts +93 -0
  56. package/src/webview/entry.ts +6 -2
  57. package/src/webview/render-goals.ts +2 -1
  58. package/src/webview/styles.css +8 -0
@@ -1,3 +1,4 @@
1
+ import { schemaFinding } from '../schema-finding.js';
1
2
  import type { ApplicationsCatalogueFile, ApplicationType, ApplicationStatus, IntegrationDirection } from './types.js';
2
3
  import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
3
4
 
@@ -27,23 +28,23 @@ export function validateApplicationsCatalogue(input: unknown): ValidationResult
27
28
  const errors: ValidationError[] = [];
28
29
  const warnings: ValidationWarning[] = [];
29
30
 
30
- if (!input || typeof input !== 'object') {
31
- return { valid: false, errors: [{ code: 'APP-001', message: 'Input must be an object' }], warnings };
31
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
32
+ return { valid: false, errors: [schemaFinding('applications inline', '$', 'mapping', input)], warnings };
32
33
  }
33
34
 
34
35
  const raw = input as Record<string, unknown>;
35
36
 
36
37
  // APP-001: notation header
37
38
  if (!('notation' in raw)) {
38
- errors.push({ code: 'APP-001', message: 'Missing required field: notation' });
39
+ errors.push({ code: 'HDR-001', message: 'notation field is missing' });
39
40
  } else if (raw['notation'] !== 'applications') {
40
- errors.push({ code: 'APP-001', message: `notation must be "applications", got "${raw['notation']}"` });
41
+ errors.push({ code: 'HDR-002', message: 'notation must be applications' });
41
42
  }
42
43
 
43
44
  if (errors.length > 0) return { valid: false, errors, warnings };
44
45
 
45
46
  // APP-002: catalogue header fields
46
- if (!raw['applications_catalogue'] || typeof raw['applications_catalogue'] !== 'object') {
47
+ if (!raw['applications_catalogue'] || typeof raw['applications_catalogue'] !== 'object' || Array.isArray(raw['applications_catalogue'])) {
47
48
  errors.push({ code: 'APP-002', message: 'Missing required field: applications_catalogue' });
48
49
  return { valid: false, errors, warnings };
49
50
  }
@@ -62,9 +63,9 @@ export function validateApplicationsCatalogue(input: unknown): ValidationResult
62
63
 
63
64
  if (errors.length > 0) return { valid: false, errors, warnings };
64
65
 
65
- // APP-007: updated_at format
66
+ // Schema: updated_at format
66
67
  if (!DATE_RE.test(cat['updated_at'] as string)) {
67
- errors.push({ code: 'APP-007', message: `applications_catalogue.updated_at must be YYYY-MM-DD, got "${cat['updated_at']}"` });
68
+ errors.push(schemaFinding('applications inline', 'applications_catalogue.updated_at', 'YYYY-MM-DD string', cat['updated_at']));
68
69
  }
69
70
 
70
71
  const applications = cat['applications'];
@@ -73,55 +74,55 @@ export function validateApplicationsCatalogue(input: unknown): ValidationResult
73
74
  return { valid: false, errors, warnings };
74
75
  }
75
76
 
76
- // APP-008: unique app_id
77
+ // Schema: unique app_id
77
78
  const seenIds = new Set<string>();
78
79
 
79
80
  for (let i = 0; i < applications.length; i++) {
80
81
  const rawApp = applications[i];
81
82
  const idx = `applications[${i}]`;
82
83
 
83
- if (!rawApp || typeof rawApp !== 'object') {
84
- errors.push({ code: 'APP-003', message: `${idx} must be an object` });
84
+ if (!rawApp || typeof rawApp !== 'object' || Array.isArray(rawApp)) {
85
+ errors.push(schemaFinding('applications inline', `${idx}`, 'mapping', rawApp));
85
86
  continue;
86
87
  }
87
88
  const a = rawApp as Record<string, unknown>;
88
89
 
89
- // APP-003: required per-application fields
90
+ // Schema: required per-application fields
90
91
  if (!a['app_id'] || typeof a['app_id'] !== 'string' || !(a['app_id'] as string).trim()) {
91
- errors.push({ code: 'APP-003', message: `${idx}: app_id is required` });
92
+ errors.push(schemaFinding('applications inline', `${idx}.app_id`, 'nonempty string', a['app_id']));
92
93
  } else {
93
94
  const aid = a['app_id'] as string;
94
95
  if (seenIds.has(aid)) {
95
- errors.push({ code: 'APP-008', message: `Duplicate app_id: "${aid}"` });
96
+ errors.push(schemaFinding('applications inline', `${idx}.app_id`, 'unique identifier', a['app_id']));
96
97
  }
97
98
  seenIds.add(aid);
98
99
  }
99
100
 
100
101
  if (!a['name'] || typeof a['name'] !== 'string' || !(a['name'] as string).trim()) {
101
- errors.push({ code: 'APP-003', message: `${idx}: name is required` });
102
+ errors.push(schemaFinding('applications inline', `${idx}.name`, 'nonempty string', a['name']));
102
103
  }
103
104
  if (!a['type']) {
104
- errors.push({ code: 'APP-003', message: `${idx}: type is required` });
105
+ errors.push(schemaFinding('applications inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, a['type']));
105
106
  }
106
107
  if (!a['status']) {
107
- errors.push({ code: 'APP-003', message: `${idx}: status is required` });
108
+ errors.push(schemaFinding('applications inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, a['status']));
108
109
  }
109
110
 
110
- // APP-004: type enum
111
+ // Schema: type enum
111
112
  if (a['type'] && !VALID_TYPES.has(a['type'] as ApplicationType)) {
112
- errors.push({ code: 'APP-004', message: `${idx}: type "${a['type']}" must be one of: application, integration, platform, data_store` });
113
+ errors.push(schemaFinding('applications inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, a['type']));
113
114
  }
114
115
 
115
- // APP-005: status enum (4 values)
116
+ // Schema: status enum (4 values)
116
117
  if (a['status'] && !VALID_STATUSES.has(a['status'] as ApplicationStatus)) {
117
- errors.push({ code: 'APP-005', message: `${idx}: status "${a['status']}" must be one of: Draft, Active, Deprecated, Decommissioning` });
118
+ errors.push(schemaFinding('applications inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, a['status']));
118
119
  }
119
120
 
120
- // APP-006: maturity range
121
+ // Schema: maturity range
121
122
  if (a['maturity'] !== undefined) {
122
123
  const m = a['maturity'];
123
124
  if (typeof m !== 'number' || !Number.isInteger(m) || m < 1 || m > 5) {
124
- errors.push({ code: 'APP-006', message: `${idx}: maturity must be an integer 1–5, got "${m}"` });
125
+ errors.push(schemaFinding('applications inline', `${idx}.maturity`, 'integer in 1–5', a['maturity']));
125
126
  }
126
127
  }
127
128
 
@@ -137,18 +138,18 @@ export function validateApplicationsCatalogue(input: unknown): ValidationResult
137
138
  }
138
139
  }
139
140
 
140
- // APP-009: integrations[].direction enum
141
+ // Schema: integrations[].direction enum
141
142
  if (Array.isArray(a['integrations'])) {
142
143
  const integrations = a['integrations'] as unknown[];
143
144
  for (let j = 0; j < integrations.length; j++) {
144
145
  const rawIntg = integrations[j];
145
- if (!rawIntg || typeof rawIntg !== 'object') {
146
- errors.push({ code: 'APP-009', message: `${idx}.integrations[${j}] must be an object` });
146
+ if (!rawIntg || typeof rawIntg !== 'object' || Array.isArray(rawIntg)) {
147
+ errors.push(schemaFinding('applications inline', `${idx}.integrations[${j}]`, 'mapping', rawIntg));
147
148
  continue;
148
149
  }
149
150
  const intg = rawIntg as Record<string, unknown>;
150
151
  if (intg['direction'] !== undefined && !VALID_DIRECTIONS.has(intg['direction'] as IntegrationDirection)) {
151
- errors.push({ code: 'APP-009', message: `${idx}.integrations[${j}]: direction "${intg['direction']}" must be one of: inbound, outbound, bidirectional` });
152
+ errors.push(schemaFinding('applications inline', `${idx}.integrations[${j}].direction`, 'inbound | outbound | bidirectional', intg['direction']));
152
153
  }
153
154
 
154
155
  // INT-001 — when interface_semantics: true, four fields become required.
@@ -1,3 +1,5 @@
1
+ import type { ResolvedCapabilityAttributes } from './resolve-maturity.js';
2
+ import { schemaFinding } from '../schema-finding.js';
1
3
  import type { CapabilityType } from './types.js';
2
4
  import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
3
5
 
@@ -11,31 +13,25 @@ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
11
13
  // canonical prefix do not break; migration is tracked separately.
12
14
  const CAP_ID_RE = /^(CAPABILITY-)?(V|H)\d+(\.\d+)*$/;
13
15
 
14
- export function validateCapabilityMap(input: unknown): ValidationResult {
16
+ export function validateCapabilityMap(input: unknown, options: { resolvedAttributes?: ReadonlyMap<string, ResolvedCapabilityAttributes> } = {}): ValidationResult {
15
17
  const errors: ValidationError[] = [];
16
18
  const warnings: ValidationWarning[] = [];
17
19
 
18
- if (!input || typeof input !== 'object') {
19
- return { valid: false, errors: [{ code: 'CMAP-001', message: 'Input must be an object' }], warnings };
20
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
21
+ return { valid: false, errors: [schemaFinding('capability-map inline', '$', 'mapping', input)], warnings };
20
22
  }
21
23
  const raw = input as Record<string, unknown>;
22
24
 
23
- // HDR-001/002 — DSM's own rule codes for the same header check CMAP-001
24
- // already makes (api02/internal/importer/capabilities.go ValidateCapMap),
25
- // added alongside it (not instead of it) so DSM can map a finding straight
26
- // back onto its own import-log taxonomy while CMAP-001 stays the richer,
27
- // pre-existing Studio-native code for this notation's own validator.
25
+ // Shared header rules have precedence over generic schema findings.
28
26
  if (!('notation' in raw)) {
29
- errors.push({ code: 'CMAP-001', message: 'Missing required field: notation' });
30
27
  errors.push({ code: 'HDR-001', message: 'notation field is missing' });
31
28
  } else if (raw['notation'] !== 'capability-map') {
32
- errors.push({ code: 'CMAP-001', message: `notation must be "capability-map", got "${raw['notation']}"` });
33
29
  errors.push({ code: 'HDR-002', message: `notation must be "capability-map", got "${raw['notation']}"` });
34
30
  }
35
31
  if (errors.length > 0) return { valid: false, errors, warnings };
36
32
 
37
33
  const map = raw['capability_map'];
38
- if (!map || typeof map !== 'object') {
34
+ if (!map || typeof map !== 'object' || Array.isArray(map)) {
39
35
  errors.push({ code: 'CMAP-002', message: 'Missing required field: capability_map' });
40
36
  return { valid: false, errors, warnings };
41
37
  }
@@ -51,7 +47,7 @@ export function validateCapabilityMap(input: unknown): ValidationResult {
51
47
  if (errors.length > 0) return { valid: false, errors, warnings };
52
48
 
53
49
  if (!DATE_RE.test(m['assessment_date'] as string))
54
- errors.push({ code: 'CMAP-007', message: `capability_map.assessment_date must be YYYY-MM-DD, got "${m['assessment_date']}"` });
50
+ errors.push(schemaFinding('capability-map inline', 'capability_map.assessment_date', 'YYYY-MM-DD string', m['assessment_date']));
55
51
 
56
52
  const caps = m['capabilities'];
57
53
  if (!Array.isArray(caps)) {
@@ -60,7 +56,7 @@ export function validateCapabilityMap(input: unknown): ValidationResult {
60
56
  }
61
57
 
62
58
  const seenIds = new Set<string>();
63
- validateCapabilityTree(caps, 'capabilities', errors, seenIds);
59
+ validateCapabilityTree(caps, 'capabilities', errors, seenIds, warnings, options.resolvedAttributes);
64
60
 
65
61
  return { valid: errors.length === 0, errors, warnings };
66
62
  }
@@ -70,57 +66,65 @@ function validateCapabilityTree(
70
66
  pathPrefix: string,
71
67
  errors: ValidationError[],
72
68
  seenIds: Set<string>,
69
+ warnings: ValidationWarning[],
70
+ resolved?: ReadonlyMap<string, ResolvedCapabilityAttributes>,
73
71
  ): void {
74
72
  for (let i = 0; i < nodes.length; i++) {
75
73
  const rawNode = nodes[i];
76
74
  const nodePath = `${pathPrefix}[${i}]`;
77
75
 
78
- if (!rawNode || typeof rawNode !== 'object') {
79
- errors.push({ code: 'CMAP-003', message: `${nodePath} must be an object` });
76
+ if (!rawNode || typeof rawNode !== 'object' || Array.isArray(rawNode)) {
77
+ errors.push(schemaFinding('capability-map inline', `${nodePath}`, 'mapping', rawNode));
80
78
  continue;
81
79
  }
82
80
  const node = rawNode as Record<string, unknown>;
83
81
 
84
82
  if (!node['id'] || typeof node['id'] !== 'string' || !(node['id'] as string).trim()) {
85
- errors.push({ code: 'CMAP-003', message: `${nodePath}: id is required` });
83
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.id`, 'nonempty string', node['id']));
86
84
  } else {
87
85
  const id = node['id'] as string;
88
86
  if (seenIds.has(id)) {
89
- errors.push({ code: 'CMAP-008', message: `Duplicate capability id: "${id}"` });
87
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.id`, 'unique identifier', node['id']));
90
88
  }
91
89
  seenIds.add(id);
92
90
  if (!CAP_ID_RE.test(id)) {
93
- errors.push({ code: 'CMAP-009', message: `${nodePath}: id "${id}" must match the canonical pattern CAPABILITY-V[n] or CAPABILITY-H[n] with optional .n segments (e.g. CAPABILITY-V1, CAPABILITY-V1.2, CAPABILITY-H1.3); the bare V[n] / H[n] form is also accepted for legacy compatibility` });
91
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.id`, 'CAPABILITY-V[n] / CAPABILITY-H[n] (legacy V[n] / H[n]) with optional .n segments', node['id']));
94
92
  }
95
93
  }
96
94
 
97
95
  if (!node['name'] || typeof node['name'] !== 'string' || !(node['name'] as string).trim())
98
- errors.push({ code: 'CMAP-003', message: `${nodePath}: name is required` });
96
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.name`, 'nonempty string', node['name']));
99
97
 
100
- if (node['current_maturity'] === undefined) {
101
- errors.push({ code: 'CMAP-003', message: `${nodePath}: current_maturity is required` });
98
+ for (const field of ['current_maturity', 'target_maturity', 'target_date', 'owner_role']) {
99
+ if (node[field] !== undefined) errors.push({code: 'VERSIONED-004', path: `${nodePath}.${field}`,
100
+ message: `${nodePath}.${field} belongs in ${String(node['id'])}.history.yaml, not inline.`});
101
+ }
102
+ const currentMaturity = node['current_maturity'] ?? resolved?.get(String(node['id']))?.current_maturity;
103
+ if (currentMaturity === undefined && resolved === undefined) {
104
+ warnings.push({code: 'NOTATION-SKIP-001', path: `${nodePath}.current_maturity`,
105
+ message: `${nodePath}.current_maturity cannot be checked without the capability sidecar catalogue; this file remains unvalidated.`});
102
106
  } else {
103
- const cm = node['current_maturity'];
107
+ const cm = currentMaturity;
104
108
  if (typeof cm !== 'number' || !Number.isInteger(cm) || cm < 1 || cm > 5)
105
- errors.push({ code: 'CMAP-005', message: `${nodePath}: current_maturity must be an integer 1–5, got "${cm}"` });
109
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.current_maturity`, 'integer in 1–5 from the sidecar at the assessment date', currentMaturity));
106
110
  }
107
111
 
108
112
  if (node['target_maturity'] !== undefined) {
109
113
  const tm = node['target_maturity'];
110
114
  if (typeof tm !== 'number' || !Number.isInteger(tm) || tm < 1 || tm > 5)
111
- errors.push({ code: 'CMAP-006', message: `${nodePath}: target_maturity must be an integer 1–5, got "${tm}"` });
115
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.target_maturity`, 'integer in 1–5', node['target_maturity']));
112
116
  }
113
117
 
114
118
  if (node['type'] !== undefined && !VALID_TYPES.has(node['type'] as CapabilityType))
115
- errors.push({ code: 'CMAP-004', message: `${nodePath}: type "${node['type']}" must be one of: domain, supporting` });
119
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.type`, `one of ${[...VALID_TYPES].join(", ")}`, node['type']));
116
120
 
117
121
  if (node['target_date'] !== undefined) {
118
122
  if (typeof node['target_date'] !== 'string' || !DATE_RE.test(node['target_date'] as string))
119
- errors.push({ code: 'CMAP-007', message: `${nodePath}: target_date must be YYYY-MM-DD, got "${node['target_date']}"` });
123
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.target_date`, 'YYYY-MM-DD string', node['target_date']));
120
124
  }
121
125
 
122
126
  if (node['applications'] !== undefined && !Array.isArray(node['applications']))
123
- errors.push({ code: 'CMAP-003', message: `${nodePath}: applications must be an array` });
127
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.applications`, 'array', node['applications']));
124
128
 
125
129
  // LIFECYCLE-001/004 — DSM's own rule codes (capabilities.go
126
130
  // ValidateCapMap/validateCapMapNodes) for `valid_from`/`valid_to`, the
@@ -157,9 +161,9 @@ function validateCapabilityTree(
157
161
 
158
162
  if (node['children'] !== undefined) {
159
163
  if (!Array.isArray(node['children'])) {
160
- errors.push({ code: 'CMAP-003', message: `${nodePath}: children must be an array` });
164
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.children`, 'array', node['children']));
161
165
  } else {
162
- validateCapabilityTree(node['children'] as unknown[], `${nodePath}.children`, errors, seenIds);
166
+ validateCapabilityTree(node['children'] as unknown[], `${nodePath}.children`, errors, seenIds, warnings, resolved);
163
167
  }
164
168
  }
165
169
  }
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // CODEX-001 — shape, zone, id grammar, admission envelope.
4
4
  // CODEX-002 — `type` field matches the id TYPE prefix when present.
5
- // CODEX-003 — external artefact frontmatter (jurisdiction, effective_date).
5
+ // CODEX-002 — external artefact frontmatter (jurisdiction, effective_date).
6
6
  // CODEX-004 — internal artefact frontmatter (issuing_authority, effective_date for
7
7
  // POLICY/INTERNAL_STANDARD; statement, rationale for PRINCIPLE — §4.1).
8
8
  // CODEX-005 — jurisdiction must match the parent folder under codex/external/
@@ -92,7 +92,7 @@ export function validateCodex(input: unknown, options: CodexValidateOptions = {}
92
92
  if (artefactType && (EXTERNAL_CODEX_TYPES as readonly string[]).includes(artefactType)) {
93
93
  const jurisdiction = typeof c.jurisdiction === 'string' ? c.jurisdiction.trim() : '';
94
94
  if (!jurisdiction) {
95
- errors.push({ code: 'CODEX-003', message: 'jurisdiction is required for external codex artefacts.', path: 'jurisdiction' });
95
+ errors.push({ code: 'CODEX-002', message: 'jurisdiction is required for external codex artefacts.', path: 'jurisdiction' });
96
96
  } else if (options.folderJurisdiction && jurisdiction.toLowerCase() !== options.folderJurisdiction.toLowerCase()) {
97
97
  errors.push({
98
98
  code: 'CODEX-005',
@@ -101,7 +101,7 @@ export function validateCodex(input: unknown, options: CodexValidateOptions = {}
101
101
  });
102
102
  }
103
103
  if (typeof c.effective_date !== 'string' || c.effective_date.trim() === '') {
104
- errors.push({ code: 'CODEX-003', message: 'effective_date is required for external codex artefacts.', path: 'effective_date' });
104
+ errors.push({ code: 'CODEX-002', message: 'effective_date is required for external codex artefacts.', path: 'effective_date' });
105
105
  }
106
106
  }
107
107
 
@@ -117,13 +117,13 @@ export function collectCoverageViewResolutionFindings(
117
117
  const t = catalog.typeOf(id);
118
118
  if (t === undefined) {
119
119
  findings.push({
120
- code: 'COVMET-REF',
120
+ code: 'COVMET-003',
121
121
  severity: 'error',
122
122
  message: `subjects.products: "${id}" does not resolve to an admitted artefact.`,
123
123
  });
124
124
  } else if (t !== 'PRODUCT') {
125
125
  findings.push({
126
- code: 'COVMET-REF',
126
+ code: 'COVMET-003',
127
127
  severity: 'error',
128
128
  message: `subjects.products: "${id}" resolves to ${t}, not PRODUCT.`,
129
129
  });
@@ -133,7 +133,7 @@ export function collectCoverageViewResolutionFindings(
133
133
  for (const id of config.regimes?.include ?? []) {
134
134
  if (!codexIds.has(id)) {
135
135
  findings.push({
136
- code: 'COVMET-REF',
136
+ code: 'COVMET-003',
137
137
  severity: 'error',
138
138
  message: `regimes.include: "${id}" does not resolve to an admitted codex artefact.`,
139
139
  });
@@ -111,13 +111,13 @@ export function collectImpactViewResolutionFindings(
111
111
  const t = catalog.typeOf(id);
112
112
  if (t === undefined) {
113
113
  findings.push({
114
- code: 'COMPIMP-REF',
114
+ code: 'COMPIMP-003',
115
115
  severity: 'error',
116
116
  message: `${path}: "${id}" does not resolve to an admitted artefact.`,
117
117
  });
118
118
  } else if (t !== expectedType) {
119
119
  findings.push({
120
- code: 'COMPIMP-REF',
120
+ code: 'COMPIMP-003',
121
121
  severity: 'error',
122
122
  message: `${path}: "${id}" resolves to ${t}, not ${expectedType}.`,
123
123
  });
@@ -129,7 +129,7 @@ export function collectImpactViewResolutionFindings(
129
129
  const t = catalog.typeOf(id);
130
130
  if (t === undefined || t !== 'REQUIREMENT') {
131
131
  findings.push({
132
- code: 'COMPIMP-REF',
132
+ code: 'COMPIMP-003',
133
133
  severity: 'error',
134
134
  message: `obligations.include: "${id}" does not resolve to an admitted REQUIREMENT.`,
135
135
  });
@@ -139,7 +139,7 @@ export function collectImpactViewResolutionFindings(
139
139
  for (const id of config.obligations.filter?.derived_from_codex ?? []) {
140
140
  if (!codexIds.has(id)) {
141
141
  findings.push({
142
- code: 'COMPIMP-REF',
142
+ code: 'COMPIMP-003',
143
143
  severity: 'error',
144
144
  message: `obligations.filter.derived_from_codex: "${id}" does not resolve to an admitted codex artefact.`,
145
145
  });
@@ -1,3 +1,4 @@
1
+ import { RESOLVED_DGCA } from './projection-origin.js';
1
2
  /**
2
3
  * Canonical-form FGCA parser + validator.
3
4
  *
@@ -167,6 +168,15 @@ export function parseCanonicalFGCA(
167
168
 
168
169
  if (errors.length > 0) return { valid: false, errors, warnings };
169
170
 
171
+ if (!(input as { [RESOLVED_DGCA]?: boolean })[RESOLVED_DGCA]) {
172
+ for (const [field, values] of [['factors', factorsRaw], ['goals', goalsRaw], ['actions', activitiesRaw], ['changes', changesRaw]] as const) {
173
+ if (field === 'changes' && isDgcaChangesLayerOff(raw)) continue;
174
+ if (values?.length === 0) errors.push({ code: 'DGCA-004', path: field,
175
+ message: `Inline DGCA ${field} must be nonempty.` });
176
+ }
177
+ if (errors.length > 0) return { valid: false, errors, warnings };
178
+ }
179
+
170
180
  // From here we know all four arrays exist.
171
181
  const factors = factorsRaw!;
172
182
  const goals = goalsRaw!;
@@ -435,7 +445,10 @@ export function parseCanonicalFGA(
435
445
  // changes. Per-layer / per-ref checks all reuse the FGCA implementation;
436
446
  // codes are remapped on the way out. FGCA-009 / 010 / 014 (changes-related)
437
447
  // are unreachable here because `changes` is empty.
438
- const synth = { ...raw, notation: 'dgca', id: 'DGCA-FROM-DGA-1', changes: [] };
448
+ const vc = raw.view_config && typeof raw.view_config === 'object' ? raw.view_config as Record<string, unknown> : {};
449
+ const layers = vc.layers && typeof vc.layers === 'object' ? vc.layers as Record<string, unknown> : {};
450
+ const synth = { ...raw, notation: 'dgca', id: 'DGCA-FROM-DGA-1', changes: [],
451
+ view_config: { ...vc, layers: { ...layers, changes: 'off' } } };
439
452
  const r = parseCanonicalFGCA(synth, outOfScopeGoalIds, outOfScopeFactorIds, progressData);
440
453
  const remap: Record<string, string> = {
441
454
  'FGCA-001': 'FGA-001',
@@ -0,0 +1,2 @@
1
+ /** Marks an in-memory projection result without admitting a YAML override. */
2
+ export const RESOLVED_DGCA = Symbol.for('@transitrix/diagrams/resolved-dgca');
@@ -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).
@@ -182,6 +183,7 @@ export function resolveFGCA(
182
183
  }
183
184
 
184
185
  return {
186
+ [RESOLVED_DGCA]: true,
185
187
  notation: 'dgca',
186
188
  id: viewDoc['id'],
187
189
  name: viewDoc['name'],
@@ -1,3 +1,4 @@
1
+ import { schemaFinding } from '../schema-finding.js';
1
2
  import type { ProductsCatalogueFile, ProductType, ProductStatus } from './types.js';
2
3
  import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
3
4
 
@@ -11,8 +12,8 @@ export function validateProductsCatalogue(input: unknown): ValidationResult {
11
12
  const errors: ValidationError[] = [];
12
13
  const warnings: ValidationWarning[] = [];
13
14
 
14
- if (!input || typeof input !== 'object') {
15
- return { valid: false, errors: [{ code: 'PROD-001', message: 'Input must be an object' }], warnings };
15
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
16
+ return { valid: false, errors: [schemaFinding('products inline', '$', 'mapping', input)], warnings };
16
17
  }
17
18
 
18
19
  const raw = input as Record<string, unknown>;
@@ -26,84 +27,89 @@ export function validateProductsCatalogue(input: unknown): ValidationResult {
26
27
 
27
28
  if (errors.length > 0) return { valid: false, errors, warnings };
28
29
 
30
+ if (('view_config' in raw || 'view' in raw) && !('products_catalogue' in raw)) {
31
+ return { valid: true, errors, warnings: [{ code: 'NOTATION-SKIP-001',
32
+ message: 'products projection form is unsupported and remains unvalidated.' }] };
33
+ }
34
+
29
35
  // PROD-002: catalogue header fields
30
36
  const cat = (raw['products_catalogue'] ?? {}) as Record<string, unknown>;
31
- if (!raw['products_catalogue'] || typeof raw['products_catalogue'] !== 'object') {
32
- errors.push({ code: 'PROD-002', message: 'Missing required field: products_catalogue' });
37
+ if (!raw['products_catalogue'] || typeof raw['products_catalogue'] !== 'object' || Array.isArray(raw['products_catalogue'])) {
38
+ errors.push(schemaFinding('products inline', 'products_catalogue', 'mapping', raw['products_catalogue']));
33
39
  return { valid: false, errors, warnings };
34
40
  }
35
41
  if (!cat['id'] || typeof cat['id'] !== 'string' || !(cat['id'] as string).trim()) {
36
- errors.push({ code: 'PROD-002', message: 'products_catalogue.id is required' });
42
+ errors.push(schemaFinding('products inline', 'products_catalogue.id', 'nonempty string', cat['id']));
37
43
  }
38
44
  if (!cat['name'] || typeof cat['name'] !== 'string' || !(cat['name'] as string).trim()) {
39
- errors.push({ code: 'PROD-002', message: 'products_catalogue.name is required' });
45
+ errors.push(schemaFinding('products inline', 'products_catalogue.name', 'nonempty string', cat['name']));
40
46
  }
41
47
  if (!cat['updated_at'] || typeof cat['updated_at'] !== 'string') {
42
- errors.push({ code: 'PROD-002', message: 'products_catalogue.updated_at is required' });
48
+ errors.push(schemaFinding('products inline', 'products_catalogue.updated_at', 'YYYY-MM-DD string', cat['updated_at']));
43
49
  }
44
50
 
45
51
  if (errors.length > 0) return { valid: false, errors, warnings };
46
52
 
47
- // PROD-007: updated_at format
53
+ // Schema: updated_at format
48
54
  if (!DATE_RE.test(cat['updated_at'] as string)) {
49
- errors.push({ code: 'PROD-007', message: `products_catalogue.updated_at must be YYYY-MM-DD, got "${cat['updated_at']}"` });
55
+ errors.push(schemaFinding('products inline', 'products_catalogue.updated_at', 'YYYY-MM-DD string', cat['updated_at']));
50
56
  }
51
57
 
52
58
  const products = cat['products'];
53
59
  if (!Array.isArray(products)) {
54
- errors.push({ code: 'PROD-002', message: 'products_catalogue.products must be an array' });
60
+ errors.push(schemaFinding('products inline', 'products_catalogue.products', 'array', cat['products']));
55
61
  return { valid: false, errors, warnings };
56
62
  }
57
63
 
58
- // PROD-008: unique product_id
64
+ // Schema: unique product_id
59
65
  const seenIds = new Set<string>();
60
66
 
61
67
  for (let i = 0; i < products.length; i++) {
62
68
  const rawProduct = products[i];
63
69
  const idx = `products[${i}]`;
64
70
 
65
- if (!rawProduct || typeof rawProduct !== 'object') {
66
- errors.push({ code: 'PROD-003', message: `${idx} must be an object` });
71
+ if (!rawProduct || typeof rawProduct !== 'object' || Array.isArray(rawProduct)) {
72
+ errors.push(schemaFinding('products inline', `${idx}`, 'mapping', rawProduct));
67
73
  continue;
68
74
  }
69
75
  const p = rawProduct as Record<string, unknown>;
70
76
 
71
- // PROD-003: required per-product fields
77
+ // Schema: required per-product fields
72
78
  if (!p['product_id'] || typeof p['product_id'] !== 'string' || !(p['product_id'] as string).trim()) {
73
- errors.push({ code: 'PROD-003', message: `${idx}: product_id is required` });
79
+ errors.push(schemaFinding('products inline', `${idx}.product_id`, 'nonempty string', p['product_id']));
74
80
  } else {
75
81
  const pid = p['product_id'] as string;
76
82
  if (seenIds.has(pid)) {
77
- errors.push({ code: 'PROD-008', message: `Duplicate product_id: "${pid}"` });
83
+ errors.push(schemaFinding('products inline', `${idx}.product_id`, 'unique identifier', p['product_id']));
78
84
  }
79
85
  seenIds.add(pid);
80
86
  }
81
87
 
82
88
  if (!p['name'] || typeof p['name'] !== 'string' || !(p['name'] as string).trim()) {
83
- errors.push({ code: 'PROD-003', message: `${idx}: name is required` });
89
+ errors.push(schemaFinding('products inline', `${idx}.name`, 'nonempty string', p['name']));
84
90
  }
85
91
  if (!p['type']) {
86
- errors.push({ code: 'PROD-003', message: `${idx}: type is required` });
92
+ errors.push(schemaFinding('products inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, p['type']));
87
93
  }
88
94
  if (!p['status']) {
89
- errors.push({ code: 'PROD-003', message: `${idx}: status is required` });
95
+ errors.push(schemaFinding('products inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, p['status']));
90
96
  }
91
97
 
92
- // PROD-004: type enum
98
+ // Schema: type enum
93
99
  if (p['type'] && !VALID_TYPES.has(p['type'] as ProductType)) {
94
- errors.push({ code: 'PROD-004', message: `${idx}: type "${p['type']}" must be one of: digital_product, service, platform, bundle` });
100
+ errors.push(schemaFinding('products inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, p['type']));
95
101
  }
96
102
 
97
- // PROD-005: status enum
103
+ // Schema: status enum
98
104
  if (p['status'] && !VALID_STATUSES.has(p['status'] as ProductStatus)) {
99
- errors.push({ code: 'PROD-005', message: `${idx}: status "${p['status']}" must be one of: Draft, Active, Deprecated` });
105
+ errors.push(schemaFinding('products inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, p['status']));
100
106
  }
101
107
 
102
- // PROD-006: maturity range
108
+ // Schema: maturity range
103
109
  if (p['maturity'] !== undefined) {
104
110
  const m = p['maturity'];
105
111
  if (typeof m !== 'number' || !Number.isInteger(m) || m < 1 || m > 5) {
106
- errors.push({ code: 'PROD-006', message: `${idx}: maturity must be an integer 1–5, got "${m}"` });
112
+ errors.push(schemaFinding('products inline', `${idx}.maturity`, 'integer in 1–5', p['maturity']));
107
113
  }
108
114
  }
109
115
  }