@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
@@ -0,0 +1,6 @@
1
+ /** ACTION field types and the manifest-selected 7.0.0 numeric boundary. */
2
+ export declare function validateActionFields(action: Record<string, unknown>, methodologyVersion?: string): Array<{
3
+ code: string;
4
+ message: string;
5
+ path: string;
6
+ }>;
@@ -0,0 +1,34 @@
1
+ /** ACTION field types and the manifest-selected 7.0.0 numeric boundary. */
2
+ export function validateActionFields(action, methodologyVersion) {
3
+ const errors = [];
4
+ const version = /^(\d+)\.(\d+)\.(\d+)(-[^+]+)?(?:\+.*)?$/.exec(methodologyVersion ?? '');
5
+ const nonnegative = version !== null && (Number(version[1]) > 7 ||
6
+ (Number(version[1]) === 7 && (Number(version[2]) > 0 || Number(version[3]) > 0 || !version[4])));
7
+ const type = (value) => value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
8
+ const schema = (path, expected, value) => errors.push({
9
+ code: 'SCHEMA_INVALID', path,
10
+ message: `ACTION field '${path}' expected ${expected}, actual ${type(value)} (${String(value)}).`,
11
+ });
12
+ for (const field of ['duration', 'duration_days', 'labor_cost', 'resources_cost', 'effort', 'score', 'sort']) {
13
+ const value = action[field];
14
+ if (value === undefined || (value === null && (field === 'duration' || field === 'duration_days')))
15
+ continue;
16
+ const integer = field === 'score' || field === 'sort';
17
+ if (typeof value !== 'number' || !Number.isFinite(value) || (integer && !Number.isInteger(value))) {
18
+ schema(field, integer ? 'finite integer' : 'finite number', value);
19
+ }
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))
26
+ schema('predecessors', 'array of strings', action.predecessors);
27
+ else
28
+ action.predecessors.forEach((value, index) => {
29
+ if (typeof value !== 'string')
30
+ schema(`predecessors[${index}]`, 'string', value);
31
+ });
32
+ }
33
+ return errors;
34
+ }
@@ -1,2 +1,4 @@
1
1
  import type { ActivityValidationResult } from './types.js';
2
- export declare function validateActivities(input: unknown): ActivityValidationResult;
2
+ export declare function validateActivities(input: unknown, options?: {
3
+ methodologyVersion?: string;
4
+ }): ActivityValidationResult;
@@ -1,5 +1,10 @@
1
+ import { validateActionFields } from './action-fields.js';
1
2
  import { isObject, str, reachesRootViaParent } from '../canon-resolver-utils.js';
2
3
  import { ACT_021_OMITTED_KEY } from './resolver.js';
4
+ function validDate(value) {
5
+ const date = new Date(value);
6
+ return DATE_RE.test(value) && Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
7
+ }
3
8
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4
9
  const VALID_WEEKDAYS = new Set(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);
5
10
  function act021Message(actionId, rootId) {
@@ -54,7 +59,7 @@ function collectAct021FromAuthored(raw, actions) {
54
59
  }
55
60
  return out;
56
61
  }
57
- export function validateActivities(input) {
62
+ export function validateActivities(input, options = {}) {
58
63
  const errors = [];
59
64
  const warnings = [];
60
65
  if (!input || typeof input !== 'object') {
@@ -92,7 +97,7 @@ export function validateActivities(input) {
92
97
  if (fromProjection.length === 0) {
93
98
  warnings.push(...collectAct021FromAuthored(raw, rawArray));
94
99
  }
95
- // ACT-014 / ACT-015: project.calendar validation (run before per-activity loop;
100
+ // ACT-007 / ACT-008: project.calendar validation (run before per-activity loop;
96
101
  // surfaces clearly even when activities also have issues).
97
102
  if (raw.project && typeof raw.project === 'object') {
98
103
  const project = raw.project;
@@ -101,7 +106,7 @@ export function validateActivities(input) {
101
106
  const calendar = cal;
102
107
  if (calendar.working_days !== undefined) {
103
108
  if (!Array.isArray(calendar.working_days)) {
104
- errors.push({ code: 'ACT-014', message: 'project.calendar.working_days must be an array of weekday names', path: 'project.calendar.working_days' });
109
+ errors.push({ code: 'ACT-007', message: 'project.calendar.working_days must be an array of weekday names', path: 'project.calendar.working_days' });
105
110
  }
106
111
  else {
107
112
  const seen = new Set();
@@ -110,14 +115,14 @@ export function validateActivities(input) {
110
115
  const day = typeof raw === 'string' ? raw.toLowerCase() : '';
111
116
  if (!VALID_WEEKDAYS.has(day)) {
112
117
  errors.push({
113
- code: 'ACT-014',
118
+ code: 'ACT-007',
114
119
  message: `project.calendar.working_days[${i}] "${String(raw)}" must be one of: mon, tue, wed, thu, fri, sat, sun`,
115
120
  path: `project.calendar.working_days[${i}]`,
116
121
  });
117
122
  }
118
123
  else if (seen.has(day)) {
119
124
  errors.push({
120
- code: 'ACT-014',
125
+ code: 'ACT-007',
121
126
  message: `project.calendar.working_days has duplicate entry "${day}"`,
122
127
  path: `project.calendar.working_days[${i}]`,
123
128
  });
@@ -130,14 +135,14 @@ export function validateActivities(input) {
130
135
  }
131
136
  if (calendar.holidays !== undefined) {
132
137
  if (!Array.isArray(calendar.holidays)) {
133
- errors.push({ code: 'ACT-015', message: 'project.calendar.holidays must be an array of ISO 8601 dates', path: 'project.calendar.holidays' });
138
+ errors.push({ code: 'ACT-008', message: 'project.calendar.holidays must be an array of ISO 8601 dates', path: 'project.calendar.holidays' });
134
139
  }
135
140
  else {
136
141
  for (let i = 0; i < calendar.holidays.length; i++) {
137
142
  const h = calendar.holidays[i];
138
- if (typeof h !== 'string' || !DATE_RE.test(h)) {
143
+ if (typeof h !== 'string' || !validDate(h)) {
139
144
  errors.push({
140
- code: 'ACT-015',
145
+ code: 'ACT-008',
141
146
  message: `project.calendar.holidays[${i}] "${String(h)}" must be an ISO 8601 date (YYYY-MM-DD)`,
142
147
  path: `project.calendar.holidays[${i}]`,
143
148
  });
@@ -188,15 +193,8 @@ export function validateActivities(input) {
188
193
  else {
189
194
  idSet.add(id);
190
195
  }
191
- // ACT-009: non-negative numeric fields
192
- const numericFields = ['duration', 'duration_days', 'labor_cost', 'resources_cost', 'effort', 'score', 'sort'];
193
- for (const field of numericFields) {
194
- const val = act[field];
195
- if (val !== undefined && val !== null) {
196
- if (typeof val !== 'number' || val < 0) {
197
- errors.push({ code: 'ACT-009', message: `Activity "${id}" field "${field}" must be a non-negative number, got ${String(val)}`, path });
198
- }
199
- }
196
+ for (const error of validateActionFields(act, options.methodologyVersion)) {
197
+ errors.push({ ...error, message: `Inline action actions[${i}].${error.path}: ${error.message}`, path: `actions[${i}].${error.path}` });
200
198
  }
201
199
  // ACT-011: warn if no duration (duration_days is accepted as an alias)
202
200
  // Network view is unaffected; this is only about CPM/Gantt rendering.
@@ -210,17 +208,20 @@ export function validateActivities(input) {
210
208
  for (let i = 0; i < doc.activities.length; i++) {
211
209
  const a = doc.activities[i];
212
210
  const path = `activities[${i}]`;
213
- // ACT-007: self-loop
211
+ // ACTION-009: self-loop
214
212
  if (Array.isArray(a.predecessors) && a.predecessors.includes(a.id)) {
215
- errors.push({ code: 'ACT-007', message: `Activity "${a.id}" lists itself as a predecessor`, path });
213
+ errors.push({ code: 'ACTION-009', message: `Activity "${a.id}" lists itself as a predecessor`, path });
214
+ }
215
+ if (typeof a.parent === 'string' && !idSet.has(a.parent)) {
216
+ warnings.push({ code: 'ACTION-007', message: `Action "${a.id}" parent "${a.parent}" does not resolve`, path: `${path}.parent` });
216
217
  }
217
- // ACT-005: predecessor existence (intra-document)
218
+ // ACTION-007: predecessor existence (intra-document)
218
219
  for (const predId of (a.predecessors ?? [])) {
219
220
  if (!idSet.has(predId)) {
220
- errors.push({ code: 'ACT-005', message: `Activity "${a.id}" references unknown predecessor "${predId}"`, path });
221
+ warnings.push({ code: 'ACTION-007', message: `Activity "${a.id}" references unknown predecessor "${predId}"`, path });
221
222
  }
222
223
  }
223
- // ACT-008: date format validation + end_date >= start_date.
224
+ // ACTION-010: date format validation + end_date >= start_date.
224
225
  // The orchestrator's pre-release review flagged that the raw lexicographic
225
226
  // string compare is only correct for strict YYYY-MM-DD, with no format
226
227
  // check applied. Now we validate the format first; the order compare runs
@@ -228,27 +229,27 @@ export function validateActivities(input) {
228
229
  // be misleading).
229
230
  let startValid = true;
230
231
  let endValid = true;
231
- if (a.start_date !== undefined) {
232
- if (typeof a.start_date !== 'string' || !DATE_RE.test(a.start_date)) {
233
- errors.push({ code: 'ACT-008', message: `Activity "${a.id}" start_date "${String(a.start_date)}" must be ISO 8601 YYYY-MM-DD`, path });
232
+ if (a.start_date !== undefined && a.start_date !== null) {
233
+ if (typeof a.start_date !== 'string' || !validDate(a.start_date)) {
234
+ errors.push({ code: 'ACTION-010', message: `Activity "${a.id}" start_date "${String(a.start_date)}" must be ISO 8601 YYYY-MM-DD`, path });
234
235
  startValid = false;
235
236
  }
236
237
  }
237
- if (a.end_date !== undefined) {
238
- if (typeof a.end_date !== 'string' || !DATE_RE.test(a.end_date)) {
239
- errors.push({ code: 'ACT-008', message: `Activity "${a.id}" end_date "${String(a.end_date)}" must be ISO 8601 YYYY-MM-DD`, path });
238
+ if (a.end_date !== undefined && a.end_date !== null) {
239
+ if (typeof a.end_date !== 'string' || !validDate(a.end_date)) {
240
+ errors.push({ code: 'ACTION-010', message: `Activity "${a.id}" end_date "${String(a.end_date)}" must be ISO 8601 YYYY-MM-DD`, path });
240
241
  endValid = false;
241
242
  }
242
243
  }
243
244
  if (a.start_date && a.end_date && startValid && endValid) {
244
245
  if (a.end_date < a.start_date) {
245
- errors.push({ code: 'ACT-008', message: `Activity "${a.id}" end_date "${a.end_date}" is before start_date "${a.start_date}"`, path });
246
+ errors.push({ code: 'ACTION-010', message: `Activity "${a.id}" end_date "${a.end_date}" is before start_date "${a.start_date}"`, path });
246
247
  }
247
248
  }
248
249
  }
249
250
  if (errors.length > 0)
250
251
  return { valid: false, errors, warnings };
251
- // ACT-006: no cycles
252
+ // ACTION-008: no cycles
252
253
  const cycleError = detectCycle(doc.activities);
253
254
  if (cycleError)
254
255
  errors.push(cycleError);
@@ -349,13 +350,13 @@ export function validateActivities(input) {
349
350
  }
350
351
  }
351
352
  }
352
- // ACT-019: Gantt view will not render when neither project.start_date nor
353
+ // ACT-009: Gantt view will not render when neither project.start_date nor
353
354
  // any per-activity pinned date pair is present. Network view is unaffected.
354
355
  const hasProjectStart = typeof doc.project?.start_date === 'string' && doc.project.start_date.length > 0;
355
356
  const hasPinnedActivity = doc.activities.some((a) => typeof a.start_date === 'string' && typeof a.end_date === 'string');
356
357
  if (!hasProjectStart && !hasPinnedActivity) {
357
358
  warnings.push({
358
- code: 'ACT-019',
359
+ code: 'ACT-009',
359
360
  message: 'Gantt view will not render: project.start_date is absent and no activity has both start_date and end_date pinned. Network view is unaffected.',
360
361
  });
361
362
  }
@@ -379,7 +380,8 @@ function detectCycle(activities) {
379
380
  inDegree.set(a.id, 0);
380
381
  for (const a of activities) {
381
382
  for (const pred of (a.predecessors ?? [])) {
382
- inDegree.set(a.id, (inDegree.get(a.id) ?? 0) + 1);
383
+ if (successors.has(pred))
384
+ inDegree.set(a.id, (inDegree.get(a.id) ?? 0) + 1);
383
385
  }
384
386
  }
385
387
  const queue = [];
@@ -400,7 +402,7 @@ function detectCycle(activities) {
400
402
  }
401
403
  if (processed < activities.length) {
402
404
  const cycleNodes = activities.filter(a => (inDegree.get(a.id) ?? 0) > 0).map(a => a.id);
403
- return { code: 'ACT-006', message: `Cycle detected in activity dependency graph involving: ${cycleNodes.join(', ')}` };
405
+ return { code: 'ACTION-008', message: `Cycle detected in activity dependency graph involving: ${cycleNodes.join(', ')}` };
404
406
  }
405
407
  return null;
406
408
  }
@@ -1,3 +1,4 @@
1
+ import { schemaFinding } from '../schema-finding.js';
1
2
  const VALID_TYPES = new Set(['application', 'integration', 'platform', 'data_store']);
2
3
  const VALID_STATUSES = new Set(['Draft', 'Active', 'Deprecated', 'Decommissioning']);
3
4
  const VALID_DIRECTIONS = new Set(['inbound', 'outbound', 'bidirectional']);
@@ -18,21 +19,21 @@ const TIME_VARYING_FIELDS = ['owner_role', 'vendor', 'maturity'];
18
19
  export function validateApplicationsCatalogue(input) {
19
20
  const errors = [];
20
21
  const warnings = [];
21
- if (!input || typeof input !== 'object') {
22
- return { valid: false, errors: [{ code: 'APP-001', message: 'Input must be an object' }], warnings };
22
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
23
+ return { valid: false, errors: [schemaFinding('applications inline', '$', 'mapping', input)], warnings };
23
24
  }
24
25
  const raw = input;
25
26
  // APP-001: notation header
26
27
  if (!('notation' in raw)) {
27
- errors.push({ code: 'APP-001', message: 'Missing required field: notation' });
28
+ errors.push({ code: 'HDR-001', message: 'notation field is missing' });
28
29
  }
29
30
  else if (raw['notation'] !== 'applications') {
30
- errors.push({ code: 'APP-001', message: `notation must be "applications", got "${raw['notation']}"` });
31
+ errors.push({ code: 'HDR-002', message: 'notation must be applications' });
31
32
  }
32
33
  if (errors.length > 0)
33
34
  return { valid: false, errors, warnings };
34
35
  // APP-002: catalogue header fields
35
- if (!raw['applications_catalogue'] || typeof raw['applications_catalogue'] !== 'object') {
36
+ if (!raw['applications_catalogue'] || typeof raw['applications_catalogue'] !== 'object' || Array.isArray(raw['applications_catalogue'])) {
36
37
  errors.push({ code: 'APP-002', message: 'Missing required field: applications_catalogue' });
37
38
  return { valid: false, errors, warnings };
38
39
  }
@@ -48,58 +49,58 @@ export function validateApplicationsCatalogue(input) {
48
49
  }
49
50
  if (errors.length > 0)
50
51
  return { valid: false, errors, warnings };
51
- // APP-007: updated_at format
52
+ // Schema: updated_at format
52
53
  if (!DATE_RE.test(cat['updated_at'])) {
53
- errors.push({ code: 'APP-007', message: `applications_catalogue.updated_at must be YYYY-MM-DD, got "${cat['updated_at']}"` });
54
+ errors.push(schemaFinding('applications inline', 'applications_catalogue.updated_at', 'YYYY-MM-DD string', cat['updated_at']));
54
55
  }
55
56
  const applications = cat['applications'];
56
57
  if (!Array.isArray(applications)) {
57
58
  errors.push({ code: 'APP-002', message: 'applications_catalogue.applications must be an array' });
58
59
  return { valid: false, errors, warnings };
59
60
  }
60
- // APP-008: unique app_id
61
+ // Schema: unique app_id
61
62
  const seenIds = new Set();
62
63
  for (let i = 0; i < applications.length; i++) {
63
64
  const rawApp = applications[i];
64
65
  const idx = `applications[${i}]`;
65
- if (!rawApp || typeof rawApp !== 'object') {
66
- errors.push({ code: 'APP-003', message: `${idx} must be an object` });
66
+ if (!rawApp || typeof rawApp !== 'object' || Array.isArray(rawApp)) {
67
+ errors.push(schemaFinding('applications inline', `${idx}`, 'mapping', rawApp));
67
68
  continue;
68
69
  }
69
70
  const a = rawApp;
70
- // APP-003: required per-application fields
71
+ // Schema: required per-application fields
71
72
  if (!a['app_id'] || typeof a['app_id'] !== 'string' || !a['app_id'].trim()) {
72
- errors.push({ code: 'APP-003', message: `${idx}: app_id is required` });
73
+ errors.push(schemaFinding('applications inline', `${idx}.app_id`, 'nonempty string', a['app_id']));
73
74
  }
74
75
  else {
75
76
  const aid = a['app_id'];
76
77
  if (seenIds.has(aid)) {
77
- errors.push({ code: 'APP-008', message: `Duplicate app_id: "${aid}"` });
78
+ errors.push(schemaFinding('applications inline', `${idx}.app_id`, 'unique identifier', a['app_id']));
78
79
  }
79
80
  seenIds.add(aid);
80
81
  }
81
82
  if (!a['name'] || typeof a['name'] !== 'string' || !a['name'].trim()) {
82
- errors.push({ code: 'APP-003', message: `${idx}: name is required` });
83
+ errors.push(schemaFinding('applications inline', `${idx}.name`, 'nonempty string', a['name']));
83
84
  }
84
85
  if (!a['type']) {
85
- errors.push({ code: 'APP-003', message: `${idx}: type is required` });
86
+ errors.push(schemaFinding('applications inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, a['type']));
86
87
  }
87
88
  if (!a['status']) {
88
- errors.push({ code: 'APP-003', message: `${idx}: status is required` });
89
+ errors.push(schemaFinding('applications inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, a['status']));
89
90
  }
90
- // APP-004: type enum
91
+ // Schema: type enum
91
92
  if (a['type'] && !VALID_TYPES.has(a['type'])) {
92
- errors.push({ code: 'APP-004', message: `${idx}: type "${a['type']}" must be one of: application, integration, platform, data_store` });
93
+ errors.push(schemaFinding('applications inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, a['type']));
93
94
  }
94
- // APP-005: status enum (4 values)
95
+ // Schema: status enum (4 values)
95
96
  if (a['status'] && !VALID_STATUSES.has(a['status'])) {
96
- errors.push({ code: 'APP-005', message: `${idx}: status "${a['status']}" must be one of: Draft, Active, Deprecated, Decommissioning` });
97
+ errors.push(schemaFinding('applications inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, a['status']));
97
98
  }
98
- // APP-006: maturity range
99
+ // Schema: maturity range
99
100
  if (a['maturity'] !== undefined) {
100
101
  const m = a['maturity'];
101
102
  if (typeof m !== 'number' || !Number.isInteger(m) || m < 1 || m > 5) {
102
- errors.push({ code: 'APP-006', message: `${idx}: maturity must be an integer 1–5, got "${m}"` });
103
+ errors.push(schemaFinding('applications inline', `${idx}.maturity`, 'integer in 1–5', a['maturity']));
103
104
  }
104
105
  }
105
106
  // VERSIONED-004: time_varying fields must live in the sidecar, not inline.
@@ -112,18 +113,18 @@ export function validateApplicationsCatalogue(input) {
112
113
  });
113
114
  }
114
115
  }
115
- // APP-009: integrations[].direction enum
116
+ // Schema: integrations[].direction enum
116
117
  if (Array.isArray(a['integrations'])) {
117
118
  const integrations = a['integrations'];
118
119
  for (let j = 0; j < integrations.length; j++) {
119
120
  const rawIntg = integrations[j];
120
- if (!rawIntg || typeof rawIntg !== 'object') {
121
- errors.push({ code: 'APP-009', message: `${idx}.integrations[${j}] must be an object` });
121
+ if (!rawIntg || typeof rawIntg !== 'object' || Array.isArray(rawIntg)) {
122
+ errors.push(schemaFinding('applications inline', `${idx}.integrations[${j}]`, 'mapping', rawIntg));
122
123
  continue;
123
124
  }
124
125
  const intg = rawIntg;
125
126
  if (intg['direction'] !== undefined && !VALID_DIRECTIONS.has(intg['direction'])) {
126
- errors.push({ code: 'APP-009', message: `${idx}.integrations[${j}]: direction "${intg['direction']}" must be one of: inbound, outbound, bidirectional` });
127
+ errors.push(schemaFinding('applications inline', `${idx}.integrations[${j}].direction`, 'inbound | outbound | bidirectional', intg['direction']));
127
128
  }
128
129
  // INT-001 — when interface_semantics: true, four fields become required.
129
130
  if (intg['interface_semantics'] === true) {
@@ -1,3 +1,6 @@
1
+ import type { ResolvedCapabilityAttributes } from './resolve-maturity.js';
1
2
  import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
2
3
  export type { ValidationError, ValidationWarning, ValidationResult };
3
- export declare function validateCapabilityMap(input: unknown): ValidationResult;
4
+ export declare function validateCapabilityMap(input: unknown, options?: {
5
+ resolvedAttributes?: ReadonlyMap<string, ResolvedCapabilityAttributes>;
6
+ }): ValidationResult;
@@ -1,3 +1,4 @@
1
+ import { schemaFinding } from '../schema-finding.js';
1
2
  const VALID_TYPES = new Set(['domain', 'supporting']);
2
3
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
3
4
  // Canonical form per `IDS_AND_REFERENCES.md` §2: `CAPABILITY-V…` /
@@ -5,30 +6,24 @@ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
5
6
  // legacy form (`V1`, `H1`) is still accepted so existing maps without the
6
7
  // canonical prefix do not break; migration is tracked separately.
7
8
  const CAP_ID_RE = /^(CAPABILITY-)?(V|H)\d+(\.\d+)*$/;
8
- export function validateCapabilityMap(input) {
9
+ export function validateCapabilityMap(input, options = {}) {
9
10
  const errors = [];
10
11
  const warnings = [];
11
- if (!input || typeof input !== 'object') {
12
- return { valid: false, errors: [{ code: 'CMAP-001', message: 'Input must be an object' }], warnings };
12
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
13
+ return { valid: false, errors: [schemaFinding('capability-map inline', '$', 'mapping', input)], warnings };
13
14
  }
14
15
  const raw = input;
15
- // HDR-001/002 — DSM's own rule codes for the same header check CMAP-001
16
- // already makes (api02/internal/importer/capabilities.go ValidateCapMap),
17
- // added alongside it (not instead of it) so DSM can map a finding straight
18
- // back onto its own import-log taxonomy while CMAP-001 stays the richer,
19
- // pre-existing Studio-native code for this notation's own validator.
16
+ // Shared header rules have precedence over generic schema findings.
20
17
  if (!('notation' in raw)) {
21
- errors.push({ code: 'CMAP-001', message: 'Missing required field: notation' });
22
18
  errors.push({ code: 'HDR-001', message: 'notation field is missing' });
23
19
  }
24
20
  else if (raw['notation'] !== 'capability-map') {
25
- errors.push({ code: 'CMAP-001', message: `notation must be "capability-map", got "${raw['notation']}"` });
26
21
  errors.push({ code: 'HDR-002', message: `notation must be "capability-map", got "${raw['notation']}"` });
27
22
  }
28
23
  if (errors.length > 0)
29
24
  return { valid: false, errors, warnings };
30
25
  const map = raw['capability_map'];
31
- if (!map || typeof map !== 'object') {
26
+ if (!map || typeof map !== 'object' || Array.isArray(map)) {
32
27
  errors.push({ code: 'CMAP-002', message: 'Missing required field: capability_map' });
33
28
  return { valid: false, errors, warnings };
34
29
  }
@@ -42,61 +37,68 @@ export function validateCapabilityMap(input) {
42
37
  if (errors.length > 0)
43
38
  return { valid: false, errors, warnings };
44
39
  if (!DATE_RE.test(m['assessment_date']))
45
- errors.push({ code: 'CMAP-007', message: `capability_map.assessment_date must be YYYY-MM-DD, got "${m['assessment_date']}"` });
40
+ errors.push(schemaFinding('capability-map inline', 'capability_map.assessment_date', 'YYYY-MM-DD string', m['assessment_date']));
46
41
  const caps = m['capabilities'];
47
42
  if (!Array.isArray(caps)) {
48
43
  errors.push({ code: 'CMAP-002', message: 'capability_map.capabilities must be an array' });
49
44
  return { valid: false, errors, warnings };
50
45
  }
51
46
  const seenIds = new Set();
52
- validateCapabilityTree(caps, 'capabilities', errors, seenIds);
47
+ validateCapabilityTree(caps, 'capabilities', errors, seenIds, warnings, options.resolvedAttributes);
53
48
  return { valid: errors.length === 0, errors, warnings };
54
49
  }
55
- function validateCapabilityTree(nodes, pathPrefix, errors, seenIds) {
50
+ function validateCapabilityTree(nodes, pathPrefix, errors, seenIds, warnings, resolved) {
56
51
  for (let i = 0; i < nodes.length; i++) {
57
52
  const rawNode = nodes[i];
58
53
  const nodePath = `${pathPrefix}[${i}]`;
59
- if (!rawNode || typeof rawNode !== 'object') {
60
- errors.push({ code: 'CMAP-003', message: `${nodePath} must be an object` });
54
+ if (!rawNode || typeof rawNode !== 'object' || Array.isArray(rawNode)) {
55
+ errors.push(schemaFinding('capability-map inline', `${nodePath}`, 'mapping', rawNode));
61
56
  continue;
62
57
  }
63
58
  const node = rawNode;
64
59
  if (!node['id'] || typeof node['id'] !== 'string' || !node['id'].trim()) {
65
- errors.push({ code: 'CMAP-003', message: `${nodePath}: id is required` });
60
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.id`, 'nonempty string', node['id']));
66
61
  }
67
62
  else {
68
63
  const id = node['id'];
69
64
  if (seenIds.has(id)) {
70
- errors.push({ code: 'CMAP-008', message: `Duplicate capability id: "${id}"` });
65
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.id`, 'unique identifier', node['id']));
71
66
  }
72
67
  seenIds.add(id);
73
68
  if (!CAP_ID_RE.test(id)) {
74
- 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` });
69
+ 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']));
75
70
  }
76
71
  }
77
72
  if (!node['name'] || typeof node['name'] !== 'string' || !node['name'].trim())
78
- errors.push({ code: 'CMAP-003', message: `${nodePath}: name is required` });
79
- if (node['current_maturity'] === undefined) {
80
- errors.push({ code: 'CMAP-003', message: `${nodePath}: current_maturity is required` });
73
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.name`, 'nonempty string', node['name']));
74
+ for (const field of ['current_maturity', 'target_maturity', 'target_date', 'owner_role']) {
75
+ if (node[field] !== undefined)
76
+ errors.push({ code: 'VERSIONED-004', path: `${nodePath}.${field}`,
77
+ message: `${nodePath}.${field} belongs in ${String(node['id'])}.history.yaml, not inline.` });
78
+ }
79
+ const currentMaturity = node['current_maturity'] ?? resolved?.get(String(node['id']))?.current_maturity;
80
+ if (currentMaturity === undefined && resolved === undefined) {
81
+ warnings.push({ code: 'NOTATION-SKIP-001', path: `${nodePath}.current_maturity`,
82
+ message: `${nodePath}.current_maturity cannot be checked without the capability sidecar catalogue; this file remains unvalidated.` });
81
83
  }
82
84
  else {
83
- const cm = node['current_maturity'];
85
+ const cm = currentMaturity;
84
86
  if (typeof cm !== 'number' || !Number.isInteger(cm) || cm < 1 || cm > 5)
85
- errors.push({ code: 'CMAP-005', message: `${nodePath}: current_maturity must be an integer 1–5, got "${cm}"` });
87
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.current_maturity`, 'integer in 1–5 from the sidecar at the assessment date', currentMaturity));
86
88
  }
87
89
  if (node['target_maturity'] !== undefined) {
88
90
  const tm = node['target_maturity'];
89
91
  if (typeof tm !== 'number' || !Number.isInteger(tm) || tm < 1 || tm > 5)
90
- errors.push({ code: 'CMAP-006', message: `${nodePath}: target_maturity must be an integer 1–5, got "${tm}"` });
92
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.target_maturity`, 'integer in 1–5', node['target_maturity']));
91
93
  }
92
94
  if (node['type'] !== undefined && !VALID_TYPES.has(node['type']))
93
- errors.push({ code: 'CMAP-004', message: `${nodePath}: type "${node['type']}" must be one of: domain, supporting` });
95
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.type`, `one of ${[...VALID_TYPES].join(", ")}`, node['type']));
94
96
  if (node['target_date'] !== undefined) {
95
97
  if (typeof node['target_date'] !== 'string' || !DATE_RE.test(node['target_date']))
96
- errors.push({ code: 'CMAP-007', message: `${nodePath}: target_date must be YYYY-MM-DD, got "${node['target_date']}"` });
98
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.target_date`, 'YYYY-MM-DD string', node['target_date']));
97
99
  }
98
100
  if (node['applications'] !== undefined && !Array.isArray(node['applications']))
99
- errors.push({ code: 'CMAP-003', message: `${nodePath}: applications must be an array` });
101
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.applications`, 'array', node['applications']));
100
102
  // LIFECYCLE-001/004 — DSM's own rule codes (capabilities.go
101
103
  // ValidateCapMap/validateCapMapNodes) for `valid_from`/`valid_to`, the
102
104
  // CONTRACT.md §7 primitive-lifecycle fields. Net new: this notation's own
@@ -133,10 +135,10 @@ function validateCapabilityTree(nodes, pathPrefix, errors, seenIds) {
133
135
  }
134
136
  if (node['children'] !== undefined) {
135
137
  if (!Array.isArray(node['children'])) {
136
- errors.push({ code: 'CMAP-003', message: `${nodePath}: children must be an array` });
138
+ errors.push(schemaFinding('capability-map inline', `${nodePath}.children`, 'array', node['children']));
137
139
  }
138
140
  else {
139
- validateCapabilityTree(node['children'], `${nodePath}.children`, errors, seenIds);
141
+ validateCapabilityTree(node['children'], `${nodePath}.children`, errors, seenIds, warnings, resolved);
140
142
  }
141
143
  }
142
144
  }
@@ -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/
@@ -70,7 +70,7 @@ export function validateCodex(input, options = {}) {
70
70
  if (artefactType && EXTERNAL_CODEX_TYPES.includes(artefactType)) {
71
71
  const jurisdiction = typeof c.jurisdiction === 'string' ? c.jurisdiction.trim() : '';
72
72
  if (!jurisdiction) {
73
- errors.push({ code: 'CODEX-003', message: 'jurisdiction is required for external codex artefacts.', path: 'jurisdiction' });
73
+ errors.push({ code: 'CODEX-002', message: 'jurisdiction is required for external codex artefacts.', path: 'jurisdiction' });
74
74
  }
75
75
  else if (options.folderJurisdiction && jurisdiction.toLowerCase() !== options.folderJurisdiction.toLowerCase()) {
76
76
  errors.push({
@@ -80,7 +80,7 @@ export function validateCodex(input, options = {}) {
80
80
  });
81
81
  }
82
82
  if (typeof c.effective_date !== 'string' || c.effective_date.trim() === '') {
83
- errors.push({ code: 'CODEX-003', message: 'effective_date is required for external codex artefacts.', path: 'effective_date' });
83
+ errors.push({ code: 'CODEX-002', message: 'effective_date is required for external codex artefacts.', path: 'effective_date' });
84
84
  }
85
85
  }
86
86
  if (artefactType && INTERNAL_CODEX_TYPES.includes(artefactType)) {
@@ -44,7 +44,7 @@ export function buildComplianceScan(docs) {
44
44
  const catalogMap = new Map();
45
45
  const pathById = new Map();
46
46
  for (const { path, data } of docs) {
47
- const ingested = ingestComplianceDoc(complianceCanon, data);
47
+ const ingested = ingestComplianceDoc(complianceCanon, data, path);
48
48
  if (ingested)
49
49
  pathById.set(ingested, path);
50
50
  const admitted = admitDocumentToCatalog(catalogMap, data);
@@ -1,3 +1,4 @@
1
+ import type { ChainRecord, ChainFinding } from './types.js';
1
2
  import type { IndexAssertion, IndexRequirement, IndexVerification, IndexNeed, IndexValidation } from './types.js';
2
3
  export interface ComplianceProduct {
3
4
  id: string;
@@ -11,6 +12,8 @@ export interface ComplianceCodexDoc {
11
12
  }
12
13
  /** The bucketed result of scanning a repo for compliance canon. */
13
14
  export interface ComplianceCanon {
15
+ records: ChainRecord[];
16
+ findings: ChainFinding[];
14
17
  products: ComplianceProduct[];
15
18
  requirements: IndexRequirement[];
16
19
  assertions: IndexAssertion[];
@@ -49,4 +52,4 @@ export declare function emptyCanon(): ComplianceCanon;
49
52
  * its id duplicates one already ingested into the same bucket (see
50
53
  * `canon.duplicateIds`).
51
54
  */
52
- export declare function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): string | null;
55
+ export declare function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown, sourcePath?: string): string | null;
@@ -2,8 +2,11 @@
2
2
  // for "what is a compliance artefact" — used by both the Studio extension scan
3
3
  // (webview previews) and the CLI scan (`export-compliance`), so the recognition
4
4
  // rules live once. Pure: takes a parsed YAML document, no IO.
5
+ import { typeOfId } from '../typed-id.js';
5
6
  export function emptyCanon() {
6
7
  return {
8
+ records: [],
9
+ findings: [],
7
10
  products: [],
8
11
  requirements: [],
9
12
  assertions: [],
@@ -40,7 +43,8 @@ function pushUnique(canon, list, item) {
40
43
  * its id duplicates one already ingested into the same bucket (see
41
44
  * `canon.duplicateIds`).
42
45
  */
43
- export function ingestComplianceDoc(canon, doc) {
46
+ export function ingestComplianceDoc(canon, doc, sourcePath = '') {
47
+ retainChainRecord(canon, doc, sourcePath);
44
48
  if (doc === null || typeof doc !== 'object' || Array.isArray(doc))
45
49
  return null;
46
50
  const d = doc;
@@ -128,3 +132,19 @@ export function ingestComplianceDoc(canon, doc) {
128
132
  }
129
133
  return null;
130
134
  }
135
+ /** Retain malformed values and cross-type duplicates without changing legacy buckets. */
136
+ function retainChainRecord(canon, doc, sourcePath) {
137
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
138
+ canon.findings.push({ id: `${sourcePath}:document`, owner: sourcePath, field: '',
139
+ code: 'INTAKE', message: 'Expected a document mapping', reference: false, severity: 'error' });
140
+ return;
141
+ }
142
+ const raw = structuredClone(doc);
143
+ if (typeof raw.id !== 'string' || !raw.id.trim()) {
144
+ if ('id' in raw || 'zone' in raw || ['requirement', 'verification', 'need', 'product', 'release', 'relation', 'driver'].includes(String(raw.notation)))
145
+ canon.findings.push({ id: `${sourcePath}:id`, owner: sourcePath,
146
+ field: 'id', code: 'INTAKE', message: 'Missing record identity', reference: false, severity: 'error' });
147
+ return;
148
+ }
149
+ canon.records.push({ id: raw.id, type: typeOfId(raw.id) ?? '', sourcePath, raw });
150
+ }