@transitrix/diagrams 1.14.0-rc.0 → 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/coverage-metric.js +3 -3
- package/dist/compliance/impact.js +4 -4
- 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/coverage-metric.ts +3 -3
- package/src/compliance/impact.ts +4 -4
- 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
|
@@ -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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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' || !
|
|
143
|
+
if (typeof h !== 'string' || !validDate(h)) {
|
|
139
144
|
errors.push({
|
|
140
|
-
code: 'ACT-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
//
|
|
211
|
+
// ACTION-009: self-loop
|
|
214
212
|
if (Array.isArray(a.predecessors) && a.predecessors.includes(a.id)) {
|
|
215
|
-
errors.push({ code: '
|
|
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
|
-
//
|
|
218
|
+
// ACTION-007: predecessor existence (intra-document)
|
|
218
219
|
for (const predId of (a.predecessors ?? [])) {
|
|
219
220
|
if (!idSet.has(predId)) {
|
|
220
|
-
|
|
221
|
+
warnings.push({ code: 'ACTION-007', message: `Activity "${a.id}" references unknown predecessor "${predId}"`, path });
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
|
-
//
|
|
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' || !
|
|
233
|
-
errors.push({ code: '
|
|
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' || !
|
|
239
|
-
errors.push({ code: '
|
|
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: '
|
|
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
|
-
//
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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: '
|
|
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: [
|
|
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: '
|
|
28
|
+
errors.push({ code: 'HDR-001', message: 'notation field is missing' });
|
|
28
29
|
}
|
|
29
30
|
else if (raw['notation'] !== 'applications') {
|
|
30
|
-
errors.push({ code: '
|
|
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
|
-
//
|
|
52
|
+
// Schema: updated_at format
|
|
52
53
|
if (!DATE_RE.test(cat['updated_at'])) {
|
|
53
|
-
errors.push(
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
71
|
+
// Schema: required per-application fields
|
|
71
72
|
if (!a['app_id'] || typeof a['app_id'] !== 'string' || !a['app_id'].trim()) {
|
|
72
|
-
errors.push(
|
|
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({
|
|
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(
|
|
83
|
+
errors.push(schemaFinding('applications inline', `${idx}.name`, 'nonempty string', a['name']));
|
|
83
84
|
}
|
|
84
85
|
if (!a['type']) {
|
|
85
|
-
errors.push(
|
|
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(
|
|
89
|
+
errors.push(schemaFinding('applications inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, a['status']));
|
|
89
90
|
}
|
|
90
|
-
//
|
|
91
|
+
// Schema: type enum
|
|
91
92
|
if (a['type'] && !VALID_TYPES.has(a['type'])) {
|
|
92
|
-
errors.push(
|
|
93
|
+
errors.push(schemaFinding('applications inline', `${idx}.type`, `one of ${[...VALID_TYPES].join(", ")}`, a['type']));
|
|
93
94
|
}
|
|
94
|
-
//
|
|
95
|
+
// Schema: status enum (4 values)
|
|
95
96
|
if (a['status'] && !VALID_STATUSES.has(a['status'])) {
|
|
96
|
-
errors.push(
|
|
97
|
+
errors.push(schemaFinding('applications inline', `${idx}.status`, `one of ${[...VALID_STATUSES].join(", ")}`, a['status']));
|
|
97
98
|
}
|
|
98
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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
|
|
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: [
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
79
|
-
|
|
80
|
-
|
|
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 =
|
|
85
|
+
const cm = currentMaturity;
|
|
84
86
|
if (typeof cm !== 'number' || !Number.isInteger(cm) || cm < 1 || cm > 5)
|
|
85
|
-
errors.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|
package/dist/codex/validate.js
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/
|
|
@@ -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-
|
|
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-
|
|
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)) {
|
|
@@ -16,14 +16,14 @@ export function collectCoverageViewResolutionFindings(config, catalog, complianc
|
|
|
16
16
|
const t = catalog.typeOf(id);
|
|
17
17
|
if (t === undefined) {
|
|
18
18
|
findings.push({
|
|
19
|
-
code: 'COVMET-
|
|
19
|
+
code: 'COVMET-003',
|
|
20
20
|
severity: 'error',
|
|
21
21
|
message: `subjects.products: "${id}" does not resolve to an admitted artefact.`,
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
else if (t !== 'PRODUCT') {
|
|
25
25
|
findings.push({
|
|
26
|
-
code: 'COVMET-
|
|
26
|
+
code: 'COVMET-003',
|
|
27
27
|
severity: 'error',
|
|
28
28
|
message: `subjects.products: "${id}" resolves to ${t}, not PRODUCT.`,
|
|
29
29
|
});
|
|
@@ -32,7 +32,7 @@ export function collectCoverageViewResolutionFindings(config, catalog, complianc
|
|
|
32
32
|
for (const id of config.regimes?.include ?? []) {
|
|
33
33
|
if (!codexIds.has(id)) {
|
|
34
34
|
findings.push({
|
|
35
|
-
code: 'COVMET-
|
|
35
|
+
code: 'COVMET-003',
|
|
36
36
|
severity: 'error',
|
|
37
37
|
message: `regimes.include: "${id}" does not resolve to an admitted codex artefact.`,
|
|
38
38
|
});
|
|
@@ -26,14 +26,14 @@ export function collectImpactViewResolutionFindings(config, catalog, complianceC
|
|
|
26
26
|
const t = catalog.typeOf(id);
|
|
27
27
|
if (t === undefined) {
|
|
28
28
|
findings.push({
|
|
29
|
-
code: 'COMPIMP-
|
|
29
|
+
code: 'COMPIMP-003',
|
|
30
30
|
severity: 'error',
|
|
31
31
|
message: `${path}: "${id}" does not resolve to an admitted artefact.`,
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
else if (t !== expectedType) {
|
|
35
35
|
findings.push({
|
|
36
|
-
code: 'COMPIMP-
|
|
36
|
+
code: 'COMPIMP-003',
|
|
37
37
|
severity: 'error',
|
|
38
38
|
message: `${path}: "${id}" resolves to ${t}, not ${expectedType}.`,
|
|
39
39
|
});
|
|
@@ -44,7 +44,7 @@ export function collectImpactViewResolutionFindings(config, catalog, complianceC
|
|
|
44
44
|
const t = catalog.typeOf(id);
|
|
45
45
|
if (t === undefined || t !== 'REQUIREMENT') {
|
|
46
46
|
findings.push({
|
|
47
|
-
code: 'COMPIMP-
|
|
47
|
+
code: 'COMPIMP-003',
|
|
48
48
|
severity: 'error',
|
|
49
49
|
message: `obligations.include: "${id}" does not resolve to an admitted REQUIREMENT.`,
|
|
50
50
|
});
|
|
@@ -53,7 +53,7 @@ export function collectImpactViewResolutionFindings(config, catalog, complianceC
|
|
|
53
53
|
for (const id of config.obligations.filter?.derived_from_codex ?? []) {
|
|
54
54
|
if (!codexIds.has(id)) {
|
|
55
55
|
findings.push({
|
|
56
|
-
code: 'COMPIMP-
|
|
56
|
+
code: 'COMPIMP-003',
|
|
57
57
|
severity: 'error',
|
|
58
58
|
message: `obligations.filter.derived_from_codex: "${id}" does not resolve to an admitted codex artefact.`,
|
|
59
59
|
});
|