@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.
- package/dist/activities/action-fields.d.ts +6 -0
- package/dist/activities/action-fields.js +34 -0
- package/dist/activities/validate.d.ts +3 -1
- package/dist/activities/validate.js +36 -34
- package/dist/applications/validate.js +27 -26
- package/dist/capability-map/validate.d.ts +4 -1
- package/dist/capability-map/validate.js +32 -30
- package/dist/codex/validate.js +3 -3
- package/dist/compliance/canon-catalog.js +1 -1
- package/dist/compliance/classify.d.ts +4 -1
- package/dist/compliance/classify.js +21 -1
- package/dist/compliance/coverage-metric.js +3 -3
- package/dist/compliance/impact.js +4 -4
- package/dist/compliance/index.d.ts +3 -0
- package/dist/compliance/index.js +1 -0
- package/dist/compliance/requirement-chain.d.ts +111 -0
- package/dist/compliance/requirement-chain.js +501 -0
- package/dist/compliance/reverse-index.js +4 -0
- package/dist/compliance/types.d.ts +20 -0
- package/dist/fgca/parse-canonical.d.ts +0 -25
- package/dist/fgca/parse-canonical.js +16 -26
- package/dist/fgca/projection-origin.d.ts +2 -0
- package/dist/fgca/projection-origin.js +2 -0
- package/dist/fgca/resolver.d.ts +0 -8
- package/dist/fgca/resolver.js +2 -0
- package/dist/products/validate.js +30 -25
- package/dist/repo-validate/check-strategy-chain.d.ts +8 -2
- package/dist/repo-validate/check-strategy-chain.js +64 -64
- package/dist/repo-validate/types.d.ts +2 -0
- package/dist/repo-validate/validate-repo.js +2 -0
- package/dist/risk/index.d.ts +1 -1
- package/dist/risk/types.d.ts +8 -1
- package/dist/risk/validate.js +19 -4
- package/dist/scenarios/validate.js +4 -0
- package/dist/schema-finding.d.ts +3 -0
- package/dist/schema-finding.js +6 -0
- package/dist/webview/entry.js +7 -2
- package/dist/webview/render-goals.js +2 -1
- package/package.json +1 -1
- package/src/activities/action-fields.ts +31 -0
- package/src/activities/validate.ts +37 -34
- package/src/applications/validate.ts +27 -26
- package/src/capability-map/validate.ts +33 -29
- package/src/codex/validate.ts +3 -3
- package/src/compliance/canon-catalog.ts +1 -1
- package/src/compliance/classify.ts +24 -1
- package/src/compliance/coverage-metric.ts +3 -3
- package/src/compliance/impact.ts +4 -4
- package/src/compliance/index.ts +3 -0
- package/src/compliance/requirement-chain.ts +430 -0
- package/src/compliance/reverse-index.ts +3 -0
- package/src/compliance/types.ts +21 -0
- package/src/fgca/parse-canonical.ts +14 -1
- package/src/fgca/projection-origin.ts +2 -0
- package/src/fgca/resolver.ts +2 -0
- package/src/products/validate.ts +31 -25
- package/src/repo-validate/check-strategy-chain.ts +69 -64
- package/src/repo-validate/types.ts +2 -0
- package/src/repo-validate/validate-repo.ts +2 -0
- package/src/risk/index.ts +1 -1
- package/src/risk/types.ts +9 -1
- package/src/risk/validate.ts +19 -5
- package/src/scenarios/validate.ts +5 -0
- package/src/schema-finding.ts +8 -0
- package/src/webview/entry.ts +6 -2
- package/src/webview/render-goals.ts +2 -1
- package/src/webview/styles.css +8 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateActionFields } from './action-fields.js';
|
|
1
2
|
import type {
|
|
2
3
|
ActivityDoc,
|
|
3
4
|
Activity,
|
|
@@ -8,6 +9,11 @@ import type {
|
|
|
8
9
|
import { isObject, str, reachesRootViaParent } from '../canon-resolver-utils.js';
|
|
9
10
|
import { ACT_021_OMITTED_KEY } from './resolver.js';
|
|
10
11
|
|
|
12
|
+
function validDate(value: string): boolean {
|
|
13
|
+
const date = new Date(value);
|
|
14
|
+
return DATE_RE.test(value) && Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
18
|
const VALID_WEEKDAYS = new Set(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);
|
|
13
19
|
|
|
@@ -61,7 +67,7 @@ function collectAct021FromAuthored(
|
|
|
61
67
|
return out;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
export function validateActivities(input: unknown): ActivityValidationResult {
|
|
70
|
+
export function validateActivities(input: unknown, options: { methodologyVersion?: string } = {}): ActivityValidationResult {
|
|
65
71
|
const errors: ActivityValidationError[] = [];
|
|
66
72
|
const warnings: ActivityValidationWarning[] = [];
|
|
67
73
|
|
|
@@ -104,7 +110,7 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
104
110
|
warnings.push(...collectAct021FromAuthored(raw, rawArray));
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
// ACT-
|
|
113
|
+
// ACT-007 / ACT-008: project.calendar validation (run before per-activity loop;
|
|
108
114
|
// surfaces clearly even when activities also have issues).
|
|
109
115
|
if (raw.project && typeof raw.project === 'object') {
|
|
110
116
|
const project = raw.project as Record<string, unknown>;
|
|
@@ -113,7 +119,7 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
113
119
|
const calendar = cal as Record<string, unknown>;
|
|
114
120
|
if (calendar.working_days !== undefined) {
|
|
115
121
|
if (!Array.isArray(calendar.working_days)) {
|
|
116
|
-
errors.push({ code: 'ACT-
|
|
122
|
+
errors.push({ code: 'ACT-007', message: 'project.calendar.working_days must be an array of weekday names', path: 'project.calendar.working_days' });
|
|
117
123
|
} else {
|
|
118
124
|
const seen = new Set<string>();
|
|
119
125
|
for (let i = 0; i < calendar.working_days.length; i++) {
|
|
@@ -121,13 +127,13 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
121
127
|
const day = typeof raw === 'string' ? raw.toLowerCase() : '';
|
|
122
128
|
if (!VALID_WEEKDAYS.has(day)) {
|
|
123
129
|
errors.push({
|
|
124
|
-
code: 'ACT-
|
|
130
|
+
code: 'ACT-007',
|
|
125
131
|
message: `project.calendar.working_days[${i}] "${String(raw)}" must be one of: mon, tue, wed, thu, fri, sat, sun`,
|
|
126
132
|
path: `project.calendar.working_days[${i}]`,
|
|
127
133
|
});
|
|
128
134
|
} else if (seen.has(day)) {
|
|
129
135
|
errors.push({
|
|
130
|
-
code: 'ACT-
|
|
136
|
+
code: 'ACT-007',
|
|
131
137
|
message: `project.calendar.working_days has duplicate entry "${day}"`,
|
|
132
138
|
path: `project.calendar.working_days[${i}]`,
|
|
133
139
|
});
|
|
@@ -139,13 +145,13 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
139
145
|
}
|
|
140
146
|
if (calendar.holidays !== undefined) {
|
|
141
147
|
if (!Array.isArray(calendar.holidays)) {
|
|
142
|
-
errors.push({ code: 'ACT-
|
|
148
|
+
errors.push({ code: 'ACT-008', message: 'project.calendar.holidays must be an array of ISO 8601 dates', path: 'project.calendar.holidays' });
|
|
143
149
|
} else {
|
|
144
150
|
for (let i = 0; i < calendar.holidays.length; i++) {
|
|
145
151
|
const h = calendar.holidays[i];
|
|
146
|
-
if (typeof h !== 'string' || !
|
|
152
|
+
if (typeof h !== 'string' || !validDate(h)) {
|
|
147
153
|
errors.push({
|
|
148
|
-
code: 'ACT-
|
|
154
|
+
code: 'ACT-008',
|
|
149
155
|
message: `project.calendar.holidays[${i}] "${String(h)}" must be an ISO 8601 date (YYYY-MM-DD)`,
|
|
150
156
|
path: `project.calendar.holidays[${i}]`,
|
|
151
157
|
});
|
|
@@ -204,15 +210,8 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
204
210
|
idSet.add(id);
|
|
205
211
|
}
|
|
206
212
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
for (const field of numericFields) {
|
|
210
|
-
const val = act[field];
|
|
211
|
-
if (val !== undefined && val !== null) {
|
|
212
|
-
if (typeof val !== 'number' || val < 0) {
|
|
213
|
-
errors.push({ code: 'ACT-009', message: `Activity "${id}" field "${field}" must be a non-negative number, got ${String(val)}`, path });
|
|
214
|
-
}
|
|
215
|
-
}
|
|
213
|
+
for (const error of validateActionFields(act, options.methodologyVersion)) {
|
|
214
|
+
errors.push({ ...error, message: `Inline action actions[${i}].${error.path}: ${error.message}`, path: `actions[${i}].${error.path}` });
|
|
216
215
|
}
|
|
217
216
|
|
|
218
217
|
// ACT-011: warn if no duration (duration_days is accepted as an alias)
|
|
@@ -229,19 +228,23 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
229
228
|
const a = doc.activities[i];
|
|
230
229
|
const path = `activities[${i}]`;
|
|
231
230
|
|
|
232
|
-
//
|
|
231
|
+
// ACTION-009: self-loop
|
|
233
232
|
if (Array.isArray(a.predecessors) && a.predecessors.includes(a.id)) {
|
|
234
|
-
errors.push({ code: '
|
|
233
|
+
errors.push({ code: 'ACTION-009', message: `Activity "${a.id}" lists itself as a predecessor`, path });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (typeof a.parent === 'string' && !idSet.has(a.parent)) {
|
|
237
|
+
warnings.push({ code: 'ACTION-007', message: `Action "${a.id}" parent "${a.parent}" does not resolve`, path: `${path}.parent` });
|
|
235
238
|
}
|
|
236
239
|
|
|
237
|
-
//
|
|
240
|
+
// ACTION-007: predecessor existence (intra-document)
|
|
238
241
|
for (const predId of (a.predecessors ?? [])) {
|
|
239
242
|
if (!idSet.has(predId)) {
|
|
240
|
-
|
|
243
|
+
warnings.push({ code: 'ACTION-007', message: `Activity "${a.id}" references unknown predecessor "${predId}"`, path });
|
|
241
244
|
}
|
|
242
245
|
}
|
|
243
246
|
|
|
244
|
-
//
|
|
247
|
+
// ACTION-010: date format validation + end_date >= start_date.
|
|
245
248
|
// The orchestrator's pre-release review flagged that the raw lexicographic
|
|
246
249
|
// string compare is only correct for strict YYYY-MM-DD, with no format
|
|
247
250
|
// check applied. Now we validate the format first; the order compare runs
|
|
@@ -249,28 +252,28 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
249
252
|
// be misleading).
|
|
250
253
|
let startValid = true;
|
|
251
254
|
let endValid = true;
|
|
252
|
-
if (a.start_date !== undefined) {
|
|
253
|
-
if (typeof a.start_date !== 'string' || !
|
|
254
|
-
errors.push({ code: '
|
|
255
|
+
if (a.start_date !== undefined && a.start_date !== null) {
|
|
256
|
+
if (typeof a.start_date !== 'string' || !validDate(a.start_date)) {
|
|
257
|
+
errors.push({ code: 'ACTION-010', message: `Activity "${a.id}" start_date "${String(a.start_date)}" must be ISO 8601 YYYY-MM-DD`, path });
|
|
255
258
|
startValid = false;
|
|
256
259
|
}
|
|
257
260
|
}
|
|
258
|
-
if (a.end_date !== undefined) {
|
|
259
|
-
if (typeof a.end_date !== 'string' || !
|
|
260
|
-
errors.push({ code: '
|
|
261
|
+
if (a.end_date !== undefined && a.end_date !== null) {
|
|
262
|
+
if (typeof a.end_date !== 'string' || !validDate(a.end_date)) {
|
|
263
|
+
errors.push({ code: 'ACTION-010', message: `Activity "${a.id}" end_date "${String(a.end_date)}" must be ISO 8601 YYYY-MM-DD`, path });
|
|
261
264
|
endValid = false;
|
|
262
265
|
}
|
|
263
266
|
}
|
|
264
267
|
if (a.start_date && a.end_date && startValid && endValid) {
|
|
265
268
|
if (a.end_date < a.start_date) {
|
|
266
|
-
errors.push({ code: '
|
|
269
|
+
errors.push({ code: 'ACTION-010', message: `Activity "${a.id}" end_date "${a.end_date}" is before start_date "${a.start_date}"`, path });
|
|
267
270
|
}
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
|
|
271
274
|
if (errors.length > 0) return { valid: false, errors, warnings };
|
|
272
275
|
|
|
273
|
-
//
|
|
276
|
+
// ACTION-008: no cycles
|
|
274
277
|
const cycleError = detectCycle(doc.activities);
|
|
275
278
|
if (cycleError) errors.push(cycleError);
|
|
276
279
|
|
|
@@ -375,7 +378,7 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
375
378
|
}
|
|
376
379
|
}
|
|
377
380
|
|
|
378
|
-
// ACT-
|
|
381
|
+
// ACT-009: Gantt view will not render when neither project.start_date nor
|
|
379
382
|
// any per-activity pinned date pair is present. Network view is unaffected.
|
|
380
383
|
const hasProjectStart = typeof doc.project?.start_date === 'string' && doc.project.start_date.length > 0;
|
|
381
384
|
const hasPinnedActivity = doc.activities.some(
|
|
@@ -383,7 +386,7 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
383
386
|
);
|
|
384
387
|
if (!hasProjectStart && !hasPinnedActivity) {
|
|
385
388
|
warnings.push({
|
|
386
|
-
code: 'ACT-
|
|
389
|
+
code: 'ACT-009',
|
|
387
390
|
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.',
|
|
388
391
|
});
|
|
389
392
|
}
|
|
@@ -408,7 +411,7 @@ function detectCycle(activities: Activity[]): ActivityValidationError | null {
|
|
|
408
411
|
for (const a of activities) inDegree.set(a.id, 0);
|
|
409
412
|
for (const a of activities) {
|
|
410
413
|
for (const pred of (a.predecessors ?? [])) {
|
|
411
|
-
inDegree.set(a.id, (inDegree.get(a.id) ?? 0) + 1);
|
|
414
|
+
if (successors.has(pred)) inDegree.set(a.id, (inDegree.get(a.id) ?? 0) + 1);
|
|
412
415
|
}
|
|
413
416
|
}
|
|
414
417
|
|
|
@@ -430,7 +433,7 @@ function detectCycle(activities: Activity[]): ActivityValidationError | null {
|
|
|
430
433
|
|
|
431
434
|
if (processed < activities.length) {
|
|
432
435
|
const cycleNodes = activities.filter(a => (inDegree.get(a.id) ?? 0) > 0).map(a => a.id);
|
|
433
|
-
return { code: '
|
|
436
|
+
return { code: 'ACTION-008', message: `Cycle detected in activity dependency graph involving: ${cycleNodes.join(', ')}` };
|
|
434
437
|
}
|
|
435
438
|
return null;
|
|
436
439
|
}
|
|
@@ -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: [
|
|
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: '
|
|
39
|
+
errors.push({ code: 'HDR-001', message: 'notation field is missing' });
|
|
39
40
|
} else if (raw['notation'] !== 'applications') {
|
|
40
|
-
errors.push({ code: '
|
|
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
|
-
//
|
|
66
|
+
// Schema: updated_at format
|
|
66
67
|
if (!DATE_RE.test(cat['updated_at'] as string)) {
|
|
67
|
-
errors.push(
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
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(
|
|
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({
|
|
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(
|
|
102
|
+
errors.push(schemaFinding('applications inline', `${idx}.name`, 'nonempty string', a['name']));
|
|
102
103
|
}
|
|
103
104
|
if (!a['type']) {
|
|
104
|
-
errors.push(
|
|
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(
|
|
108
|
+
errors.push(schemaFinding('applications inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, a['status']));
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
//
|
|
111
|
+
// Schema: type enum
|
|
111
112
|
if (a['type'] && !VALID_TYPES.has(a['type'] as ApplicationType)) {
|
|
112
|
-
errors.push(
|
|
113
|
+
errors.push(schemaFinding('applications inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, a['type']));
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
//
|
|
116
|
+
// Schema: status enum (4 values)
|
|
116
117
|
if (a['status'] && !VALID_STATUSES.has(a['status'] as ApplicationStatus)) {
|
|
117
|
-
errors.push(
|
|
118
|
+
errors.push(schemaFinding('applications inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, a['status']));
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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: [
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
96
|
+
errors.push(schemaFinding('capability-map inline', `${nodePath}.name`, 'nonempty string', node['name']));
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
errors.push({
|
|
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 =
|
|
107
|
+
const cm = currentMaturity;
|
|
104
108
|
if (typeof cm !== 'number' || !Number.isInteger(cm) || cm < 1 || cm > 5)
|
|
105
|
-
errors.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|
package/src/codex/validate.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
104
|
+
errors.push({ code: 'CODEX-002', message: 'effective_date is required for external codex artefacts.', path: 'effective_date' });
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -62,7 +62,7 @@ export function buildComplianceScan(docs: ScannedYamlDoc[]): ComplianceScanResul
|
|
|
62
62
|
const pathById = new Map<string, string>();
|
|
63
63
|
|
|
64
64
|
for (const { path, data } of docs) {
|
|
65
|
-
const ingested = ingestComplianceDoc(complianceCanon, data);
|
|
65
|
+
const ingested = ingestComplianceDoc(complianceCanon, data, path);
|
|
66
66
|
if (ingested) pathById.set(ingested, path);
|
|
67
67
|
const admitted = admitDocumentToCatalog(catalogMap, data);
|
|
68
68
|
if (admitted && !pathById.has(admitted)) pathById.set(admitted, path);
|
|
@@ -3,6 +3,8 @@
|
|
|
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
5
|
|
|
6
|
+
import { typeOfId } from '../typed-id.js';
|
|
7
|
+
import type { ChainRecord, ChainFinding } from './types.js';
|
|
6
8
|
import type { AssertionStatus } from '../assertion/types.js';
|
|
7
9
|
import type { VerificationMethod, VerificationOutcome } from '../verification/types.js';
|
|
8
10
|
import type { ValidationMethod, ValidationOutcome } from '../validation/types.js';
|
|
@@ -21,6 +23,8 @@ export interface ComplianceCodexDoc {
|
|
|
21
23
|
|
|
22
24
|
/** The bucketed result of scanning a repo for compliance canon. */
|
|
23
25
|
export interface ComplianceCanon {
|
|
26
|
+
records: ChainRecord[];
|
|
27
|
+
findings: ChainFinding[];
|
|
24
28
|
products: ComplianceProduct[];
|
|
25
29
|
requirements: IndexRequirement[];
|
|
26
30
|
assertions: IndexAssertion[];
|
|
@@ -52,6 +56,8 @@ export interface ComplianceCanon {
|
|
|
52
56
|
|
|
53
57
|
export function emptyCanon(): ComplianceCanon {
|
|
54
58
|
return {
|
|
59
|
+
records: [],
|
|
60
|
+
findings: [],
|
|
55
61
|
products: [],
|
|
56
62
|
requirements: [],
|
|
57
63
|
assertions: [],
|
|
@@ -92,7 +98,8 @@ function pushUnique<T extends { id: string }>(canon: ComplianceCanon, list: T[],
|
|
|
92
98
|
* its id duplicates one already ingested into the same bucket (see
|
|
93
99
|
* `canon.duplicateIds`).
|
|
94
100
|
*/
|
|
95
|
-
export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): string | null {
|
|
101
|
+
export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown, sourcePath = ''): string | null {
|
|
102
|
+
retainChainRecord(canon, doc, sourcePath);
|
|
96
103
|
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) return null;
|
|
97
104
|
const d = doc as Record<string, unknown>;
|
|
98
105
|
const id = str(d.id);
|
|
@@ -178,3 +185,19 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
|
|
|
178
185
|
}
|
|
179
186
|
return null;
|
|
180
187
|
}
|
|
188
|
+
|
|
189
|
+
/** Retain malformed values and cross-type duplicates without changing legacy buckets. */
|
|
190
|
+
function retainChainRecord(canon: ComplianceCanon, doc: unknown, sourcePath: string): void {
|
|
191
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
192
|
+
canon.findings.push({ id: `${sourcePath}:document`, owner: sourcePath, field: '',
|
|
193
|
+
code: 'INTAKE', message: 'Expected a document mapping', reference: false, severity: 'error' });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const raw = structuredClone(doc) as Record<string, unknown>;
|
|
197
|
+
if (typeof raw.id !== 'string' || !raw.id.trim()) {
|
|
198
|
+
if ('id' in raw || 'zone' in raw || ['requirement', 'verification', 'need', 'product', 'release', 'relation', 'driver'].includes(String(raw.notation))) canon.findings.push({ id: `${sourcePath}:id`, owner: sourcePath,
|
|
199
|
+
field: 'id', code: 'INTAKE', message: 'Missing record identity', reference: false, severity: 'error' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
canon.records.push({ id: raw.id, type: typeOfId(raw.id) ?? '', sourcePath, raw });
|
|
203
|
+
}
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
136
|
+
code: 'COVMET-003',
|
|
137
137
|
severity: 'error',
|
|
138
138
|
message: `regimes.include: "${id}" does not resolve to an admitted codex artefact.`,
|
|
139
139
|
});
|