@transitrix/diagrams 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mapping, isoDate } from './validate.js';
|
|
2
|
+
import { validateAttributeArray } from '../versioned-attribute/validate.js';
|
|
3
|
+
import { isCanonicalIdOfType } from '../typed-id.js';
|
|
4
|
+
export function validateHistory(input, options = {}) {
|
|
5
|
+
const errors = [], warnings = [];
|
|
6
|
+
const fail = (field, expected, actual, code = 'SCHEMA_INVALID') => errors.push({ code, message: `history sidecar: ${field}: expected ${expected}; actual ${JSON.stringify(actual) ?? 'missing'}.`, path: field });
|
|
7
|
+
if (!mapping(input)) {
|
|
8
|
+
fail('$', 'mapping', input);
|
|
9
|
+
return { valid: false, errors, warnings };
|
|
10
|
+
}
|
|
11
|
+
const d = input;
|
|
12
|
+
if (typeof d.target !== 'string' || !d.target.trim())
|
|
13
|
+
fail('target', 'canonical target ID', d.target, 'VERSIONED-001');
|
|
14
|
+
const target = typeof d.target === 'string' ? options.documents?.get(d.target) : undefined;
|
|
15
|
+
if (options.documents && (!target || !options.catalog?.typeOf(d.target)))
|
|
16
|
+
fail('target', 'admitted primitive', d.target, 'VERSIONED-001');
|
|
17
|
+
const notation = target?.notation;
|
|
18
|
+
const attributes = { capability: ['current_maturity', 'target_maturity', 'target_date', 'owner_role'], application: ['maturity', 'vendor', 'owner_role'] };
|
|
19
|
+
const allowed = typeof notation === 'string' ? attributes[notation] : undefined;
|
|
20
|
+
if (!allowed)
|
|
21
|
+
warnings.push({ code: 'NOTATION-SKIP-001', message: 'History target schema is unavailable; attribute coverage remains unvalidated.' });
|
|
22
|
+
if (!mapping(d.attribute_versions))
|
|
23
|
+
fail('attribute_versions', 'mapping', d.attribute_versions);
|
|
24
|
+
else
|
|
25
|
+
for (const [attribute, entries] of Object.entries(d.attribute_versions)) {
|
|
26
|
+
const field = `attribute_versions.${attribute}`;
|
|
27
|
+
if (allowed && !allowed.includes(attribute))
|
|
28
|
+
fail(field, 'declared time_varying attribute', attribute);
|
|
29
|
+
if (!Array.isArray(entries)) {
|
|
30
|
+
fail(field, 'array', entries);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const checked = [];
|
|
34
|
+
for (const [i, entry] of entries.entries()) {
|
|
35
|
+
const key = `${field}[${i}]`;
|
|
36
|
+
if (!mapping(entry)) {
|
|
37
|
+
fail(key, 'mapping', entry);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!isoDate(entry.valid_from))
|
|
41
|
+
fail(`${key}.valid_from`, 'ISO calendar date', entry.valid_from);
|
|
42
|
+
const value = entry.value;
|
|
43
|
+
const scalar = value === null || ['string', 'number', 'boolean'].includes(typeof value);
|
|
44
|
+
if (!('value' in entry) || !scalar)
|
|
45
|
+
fail(`${key}.value`, 'scalar or null', value);
|
|
46
|
+
if (scalar && isoDate(entry.valid_from))
|
|
47
|
+
checked.push({ valid_from: entry.valid_from, value: value });
|
|
48
|
+
if (value === null || !allowed?.includes(attribute))
|
|
49
|
+
continue;
|
|
50
|
+
if (['current_maturity', 'target_maturity', 'maturity'].includes(attribute) && !(Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 5))
|
|
51
|
+
fail(`${key}.value`, 'integer 1..5 or null', value);
|
|
52
|
+
if (attribute === 'target_date' && !isoDate(value))
|
|
53
|
+
fail(`${key}.value`, 'ISO calendar date or null', value);
|
|
54
|
+
if (attribute === 'vendor' && typeof value !== 'string')
|
|
55
|
+
fail(`${key}.value`, 'string or null', value);
|
|
56
|
+
if (attribute === 'owner_role' && (!isCanonicalIdOfType(value, 'ROLE') || (options.catalog && options.catalog.typeOf(value) !== 'ROLE')))
|
|
57
|
+
fail(`${key}.value`, 'admitted ROLE reference or null', value);
|
|
58
|
+
}
|
|
59
|
+
for (const finding of validateAttributeArray(attribute, checked, typeof target?.valid_from === 'string' ? target.valid_from : undefined, target?.valid_to === null || typeof target?.valid_to === 'string' ? target.valid_to : undefined)) {
|
|
60
|
+
(finding.severity === 'error' ? errors : warnings).push({ code: finding.code, message: finding.message });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const key of ['notation', 'id', 'zone', 'admitted_at', 'admitted_by', 'valid_from', 'valid_to'])
|
|
64
|
+
if (key in d)
|
|
65
|
+
fail(key, 'absent on headerless history sidecar', d[key]);
|
|
66
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
67
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CanonCatalog } from '../typed-id.js';
|
|
2
|
+
import type { ValidationResult } from '../validation-types.js';
|
|
3
|
+
export declare const mapping: (v: unknown) => v is Record<string, unknown>;
|
|
4
|
+
export declare const isoDate: (v: unknown) => v is string;
|
|
5
|
+
export interface StandaloneOptions {
|
|
6
|
+
catalog?: CanonCatalog;
|
|
7
|
+
documents?: ReadonlyMap<string, Record<string, unknown>>;
|
|
8
|
+
}
|
|
9
|
+
export declare function validateStandalone(notation: string, input: unknown, options?: StandaloneOptions): ValidationResult;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Standalone forms from ELEMENT_PRIMITIVES and CONTRACT (Methodology 7).
|
|
2
|
+
// Field payloads are opaque: only their published identity/admission contract is checked.
|
|
3
|
+
import { isCanonicalIdOfType, typeOfId } from '../typed-id.js';
|
|
4
|
+
export const mapping = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
5
|
+
const text = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
6
|
+
export const isoDate = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v) && Number.isFinite(Date.parse(v)) && new Date(v).toISOString().slice(0, 10) === v;
|
|
7
|
+
const capabilityId = (v) => typeof v === 'string' && /^CAPABILITY-[VH][1-9]\d*(?:\.[1-9]\d*)*$/.test(v);
|
|
8
|
+
export function validateStandalone(notation, input, options = {}) {
|
|
9
|
+
const errors = [], warnings = [];
|
|
10
|
+
const fail = (field, expected, actual, code = 'SCHEMA_INVALID') => errors.push({ code, message: `${notation} standalone: ${field}: expected ${expected}; actual ${JSON.stringify(actual) ?? 'missing'}.`, path: field });
|
|
11
|
+
if (!mapping(input)) {
|
|
12
|
+
fail('$', 'mapping', input);
|
|
13
|
+
return { valid: false, errors, warnings };
|
|
14
|
+
}
|
|
15
|
+
const d = input, field = ['draft', 'observation'].includes(notation);
|
|
16
|
+
const requiredText = (key) => { if (!text(d[key]))
|
|
17
|
+
fail(key, 'nonempty string', d[key]); };
|
|
18
|
+
const optionalText = (key) => { if (key in d && !text(d[key]))
|
|
19
|
+
fail(key, 'nonempty string', d[key]); };
|
|
20
|
+
if (d.notation !== notation && !(field && d.notation === undefined && d.type === notation.toUpperCase()))
|
|
21
|
+
fail('notation', notation, d.notation);
|
|
22
|
+
if (!(notation === 'capability' ? capabilityId(d.id) : isCanonicalIdOfType(d.id, notation.toUpperCase())))
|
|
23
|
+
fail('id', `${notation.toUpperCase()} canonical ID`, d.id);
|
|
24
|
+
if (!field)
|
|
25
|
+
requiredText('name');
|
|
26
|
+
else
|
|
27
|
+
optionalText('name');
|
|
28
|
+
if (d.zone !== (field ? 'field' : 'canon'))
|
|
29
|
+
fail('zone', field ? 'field' : 'canon', d.zone);
|
|
30
|
+
requiredText('admitted_by');
|
|
31
|
+
if (!isoDate(d.admitted_at))
|
|
32
|
+
fail('admitted_at', 'ISO calendar date', d.admitted_at);
|
|
33
|
+
if (!mapping(d.gate_checks))
|
|
34
|
+
fail('gate_checks', 'mapping', d.gate_checks);
|
|
35
|
+
else
|
|
36
|
+
for (const key of field ? ['provenance'] : ['uniqueness', 'consistency', 'completeness']) {
|
|
37
|
+
if (!text(d.gate_checks[key]))
|
|
38
|
+
fail(`gate_checks.${key}`, 'nonempty check outcome', d.gate_checks[key]);
|
|
39
|
+
}
|
|
40
|
+
if (!field) {
|
|
41
|
+
if (!isoDate(d.valid_from))
|
|
42
|
+
fail('valid_from', 'ISO calendar date', d.valid_from, 'LIFECYCLE-001');
|
|
43
|
+
if (!(d.valid_to === null || isoDate(d.valid_to)))
|
|
44
|
+
fail('valid_to', 'ISO calendar date or null', d.valid_to, 'LIFECYCLE-002');
|
|
45
|
+
if (isoDate(d.valid_from) && isoDate(d.valid_to) && d.valid_to < d.valid_from)
|
|
46
|
+
fail('valid_to', 'date on or after valid_from', d.valid_to, 'LIFECYCLE-003');
|
|
47
|
+
}
|
|
48
|
+
const ref = (key, value, types) => {
|
|
49
|
+
const type = typeOfId(value);
|
|
50
|
+
if (!type || !types.includes(type) || !(type === 'CAPABILITY' ? capabilityId(value) : isCanonicalIdOfType(value, type))) {
|
|
51
|
+
fail(key, types.join('|') + ' canonical reference', value);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (options.catalog && !types.includes(options.catalog.typeOf(value) ?? ''))
|
|
55
|
+
fail(key, 'reference to admitted ' + types.join('|'), value);
|
|
56
|
+
};
|
|
57
|
+
const reference = (key, types, required = false) => { if (required || key in d)
|
|
58
|
+
ref(key, d[key], types); };
|
|
59
|
+
const references = (key, types, required = false) => {
|
|
60
|
+
if (!required && !(key in d))
|
|
61
|
+
return;
|
|
62
|
+
const values = d[key];
|
|
63
|
+
if (!Array.isArray(values) || (required && values.length === 0)) {
|
|
64
|
+
fail(key, required ? 'nonempty list of references' : 'list of references', values);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
values.forEach((v, i) => ref(`${key}[${i}]`, v, types));
|
|
68
|
+
};
|
|
69
|
+
optionalText('description');
|
|
70
|
+
if (notation === 'product') {
|
|
71
|
+
if (!['digital_product', 'service', 'platform', 'bundle'].includes(d.type))
|
|
72
|
+
fail('type', 'digital_product|service|platform|bundle', d.type);
|
|
73
|
+
optionalText('domain');
|
|
74
|
+
reference('owner_role', ['ROLE']);
|
|
75
|
+
if ('maturity' in d && !(Number.isInteger(d.maturity) && Number(d.maturity) >= 1 && Number(d.maturity) <= 5))
|
|
76
|
+
fail('maturity', 'integer 1..5', d.maturity);
|
|
77
|
+
references('capabilities', ['CAPABILITY']);
|
|
78
|
+
references('processes', ['PROCESS']);
|
|
79
|
+
references('supporting_apps', ['APPLICATION']);
|
|
80
|
+
}
|
|
81
|
+
else if (notation === 'capability') {
|
|
82
|
+
if (!['domain', 'supporting'].includes(d.type))
|
|
83
|
+
fail('type', 'domain|supporting', d.type);
|
|
84
|
+
for (const key of ['current_maturity', 'target_maturity', 'owner_role', 'target_date'])
|
|
85
|
+
if (key in d)
|
|
86
|
+
fail(key, 'attribute in history sidecar, absent inline', d[key], 'VERSIONED-004');
|
|
87
|
+
reference('business_process', ['PROCESS']);
|
|
88
|
+
references('applications', ['APPLICATION']);
|
|
89
|
+
}
|
|
90
|
+
else if (notation === 'role') {
|
|
91
|
+
optionalText('responsibility_area');
|
|
92
|
+
reference('unit', ['ACTOR']);
|
|
93
|
+
const unit = typeof d.unit === 'string' ? options.documents?.get(d.unit) : undefined;
|
|
94
|
+
if (unit && unit.type !== 'business_unit')
|
|
95
|
+
fail('unit', 'ACTOR of type business_unit', d.unit);
|
|
96
|
+
}
|
|
97
|
+
else if (notation === 'assessment') {
|
|
98
|
+
requiredText('description');
|
|
99
|
+
reference('assesses', ['DRIVER', 'FACTOR'], true);
|
|
100
|
+
for (const key of ['method', 'source'])
|
|
101
|
+
optionalText(key);
|
|
102
|
+
if ('observed_at' in d && !isoDate(d.observed_at))
|
|
103
|
+
fail('observed_at', 'ISO calendar date', d.observed_at);
|
|
104
|
+
for (const key of ['polarity', 'swot', 'type'])
|
|
105
|
+
if (key in d)
|
|
106
|
+
fail(key, 'absent on assessment', d[key]);
|
|
107
|
+
if ('geographic_scope' in d) {
|
|
108
|
+
const values = d.geographic_scope;
|
|
109
|
+
// Country vocabulary is checked by the shared jurisdiction set below.
|
|
110
|
+
if (!Array.isArray(values) || !values.length || values.some(v => typeof v !== 'string' || !COUNTRIES.has(v.toLowerCase())) || (values.some(v => typeof v === 'string' && v.toLowerCase() === 'global') && values.length !== 1))
|
|
111
|
+
fail('geographic_scope', 'nonempty ISO country/eu list, or [global]', values, 'ASSESS-001');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (notation === 'scenario') {
|
|
115
|
+
references('pursues', ['GOAL'], true);
|
|
116
|
+
reference('arrives_at', ['TARGET_STATE'], true);
|
|
117
|
+
references('steps', ['ACTION', 'CHANGE'], true);
|
|
118
|
+
const target = typeof d.arrives_at === 'string' ? options.documents?.get(d.arrives_at) : undefined;
|
|
119
|
+
if (target?.type === 'base')
|
|
120
|
+
fail('arrives_at', 'target plateau, not base', d.arrives_at);
|
|
121
|
+
for (const key of ['type', 'vision', 'factors_view', 'status', 'drivers', 'capabilities', 'processes', 'applications', 'products'])
|
|
122
|
+
if (key in d)
|
|
123
|
+
fail(key, 'absent on standalone scenario', d[key]);
|
|
124
|
+
optionalText('link');
|
|
125
|
+
}
|
|
126
|
+
else if (field) {
|
|
127
|
+
const provenance = d.provenance;
|
|
128
|
+
if (provenance === undefined || provenance === null || (typeof provenance === 'string' && !provenance.trim()) || (mapping(provenance) && Object.keys(provenance).length === 0))
|
|
129
|
+
fail('provenance', 'recorded source evidence', provenance);
|
|
130
|
+
else if (!mapping(provenance))
|
|
131
|
+
warnings.push({ code: 'NOTATION-SKIP-001', message: 'Field provenance form is unsupported; source evidence remains unvalidated.' });
|
|
132
|
+
else {
|
|
133
|
+
// Two supported evidence forms: captured source and versioned source import.
|
|
134
|
+
// Other forms remain visible as unsupported, not falsely validated or forbidden.
|
|
135
|
+
const captured = text(provenance.captured_by) && isoDate(provenance.captured_on) && text(provenance.setting);
|
|
136
|
+
const imported = text(provenance.source_revision) && text(provenance.original_path) && text(provenance.source_hash) && isoDate(provenance.captured_on);
|
|
137
|
+
if (!captured && !imported)
|
|
138
|
+
warnings.push({ code: 'NOTATION-SKIP-001', message: 'Field provenance form is unsupported: source, capture date and context cannot all be checked.' });
|
|
139
|
+
}
|
|
140
|
+
if ('source_quality' in d && !['authoritative', 'corroborated', 'single_source', 'unverified'].includes(d.source_quality))
|
|
141
|
+
fail('source_quality', 'authoritative|corroborated|single_source|unverified', d.source_quality);
|
|
142
|
+
}
|
|
143
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
144
|
+
}
|
|
145
|
+
// ISO 3166-1 alpha-2 vocabulary, plus the published eu/global scope tokens.
|
|
146
|
+
const COUNTRIES = new Set(('ad ae af ag ai al am ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bl bm bn bo bq br bs bt bv bw by bz ca cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz de dj dk dm do dz ec ee eg eh er es et fi fj fk fm fo fr ga gb gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mf mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sj sk sl sm sn so sr ss st sv sx sy sz tc td tf tg th tj tk tl tm tn to tr tt tv tw tz ua ug um us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw eu global').split(' '));
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Strict raw history validation. Never filter malformed entries before checking them.
|
|
2
|
+
import type { ValidationResult } from '../validation-types.js';
|
|
3
|
+
import { mapping, isoDate, type StandaloneOptions } from './validate.js';
|
|
4
|
+
import { validateAttributeArray } from '../versioned-attribute/validate.js';
|
|
5
|
+
import { isCanonicalIdOfType } from '../typed-id.js';
|
|
6
|
+
|
|
7
|
+
export function validateHistory(input: unknown, options: StandaloneOptions = {}): ValidationResult {
|
|
8
|
+
const errors: ValidationResult['errors'] = [], warnings: ValidationResult['warnings'] = [];
|
|
9
|
+
const fail = (field: string, expected: string, actual: unknown, code = 'SCHEMA_INVALID') => errors.push({code, message: `history sidecar: ${field}: expected ${expected}; actual ${JSON.stringify(actual) ?? 'missing'}.`, path:field});
|
|
10
|
+
if (!mapping(input)) { fail('$', 'mapping', input); return {valid:false, errors, warnings}; }
|
|
11
|
+
const d = input;
|
|
12
|
+
if (typeof d.target !== 'string' || !d.target.trim()) fail('target', 'canonical target ID', d.target, 'VERSIONED-001');
|
|
13
|
+
const target = typeof d.target === 'string' ? options.documents?.get(d.target) : undefined;
|
|
14
|
+
if (options.documents && (!target || !options.catalog?.typeOf(d.target as string))) fail('target', 'admitted primitive', d.target, 'VERSIONED-001');
|
|
15
|
+
const notation = target?.notation;
|
|
16
|
+
const attributes: Record<string, string[]> = {capability: ['current_maturity','target_maturity','target_date','owner_role'], application: ['maturity','vendor','owner_role']};
|
|
17
|
+
const allowed = typeof notation === 'string' ? attributes[notation] : undefined;
|
|
18
|
+
if (!allowed) warnings.push({code:'NOTATION-SKIP-001', message: 'History target schema is unavailable; attribute coverage remains unvalidated.'});
|
|
19
|
+
if (!mapping(d.attribute_versions)) fail('attribute_versions', 'mapping', d.attribute_versions);
|
|
20
|
+
else for (const [attribute, entries] of Object.entries(d.attribute_versions)) {
|
|
21
|
+
const field = `attribute_versions.${attribute}`;
|
|
22
|
+
if (allowed && !allowed.includes(attribute)) fail(field, 'declared time_varying attribute', attribute);
|
|
23
|
+
if (!Array.isArray(entries)) { fail(field, 'array', entries); continue; }
|
|
24
|
+
const checked: {valid_from: string; value: string | number | boolean | null}[] = [];
|
|
25
|
+
for (const [i, entry] of entries.entries()) {
|
|
26
|
+
const key = `${field}[${i}]`;
|
|
27
|
+
if (!mapping(entry)) { fail(key, 'mapping', entry); continue; }
|
|
28
|
+
if (!isoDate(entry.valid_from)) fail(`${key}.valid_from`, 'ISO calendar date', entry.valid_from);
|
|
29
|
+
const value = entry.value;
|
|
30
|
+
const scalar = value === null || ['string','number','boolean'].includes(typeof value);
|
|
31
|
+
if (!('value' in entry) || !scalar) fail(`${key}.value`, 'scalar or null', value);
|
|
32
|
+
if (scalar && isoDate(entry.valid_from)) checked.push({valid_from: entry.valid_from, value: value as string | number | boolean | null});
|
|
33
|
+
if (value === null || !allowed?.includes(attribute)) continue;
|
|
34
|
+
if (['current_maturity','target_maturity','maturity'].includes(attribute) && !(Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 5)) fail(`${key}.value`, 'integer 1..5 or null', value);
|
|
35
|
+
if (attribute === 'target_date' && !isoDate(value)) fail(`${key}.value`, 'ISO calendar date or null', value);
|
|
36
|
+
if (attribute === 'vendor' && typeof value !== 'string') fail(`${key}.value`, 'string or null', value);
|
|
37
|
+
if (attribute === 'owner_role' && (!isCanonicalIdOfType(value, 'ROLE') || (options.catalog && options.catalog.typeOf(value as string) !== 'ROLE'))) fail(`${key}.value`, 'admitted ROLE reference or null', value);
|
|
38
|
+
}
|
|
39
|
+
for (const finding of validateAttributeArray(attribute, checked, typeof target?.valid_from === 'string' ? target.valid_from : undefined, target?.valid_to === null || typeof target?.valid_to === 'string' ? target.valid_to : undefined)) {
|
|
40
|
+
(finding.severity === 'error' ? errors : warnings).push({code:finding.code, message:finding.message});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const key of ['notation','id','zone','admitted_at','admitted_by','valid_from','valid_to']) if (key in d) fail(key, 'absent on headerless history sidecar', d[key]);
|
|
44
|
+
return {valid:errors.length === 0, errors, warnings};
|
|
45
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Standalone forms from ELEMENT_PRIMITIVES and CONTRACT (Methodology 7).
|
|
2
|
+
// Field payloads are opaque: only their published identity/admission contract is checked.
|
|
3
|
+
import { isCanonicalIdOfType, typeOfId, type CanonCatalog } from '../typed-id.js';
|
|
4
|
+
import type { ValidationResult } from '../validation-types.js';
|
|
5
|
+
|
|
6
|
+
export const mapping = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
7
|
+
const text = (v: unknown): v is string => typeof v === 'string' && v.trim().length > 0;
|
|
8
|
+
export const isoDate = (v: unknown): v is string => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v) && Number.isFinite(Date.parse(v)) && new Date(v).toISOString().slice(0, 10) === v;
|
|
9
|
+
const capabilityId = (v: unknown) => typeof v === 'string' && /^CAPABILITY-[VH][1-9]\d*(?:\.[1-9]\d*)*$/.test(v);
|
|
10
|
+
export interface StandaloneOptions { catalog?: CanonCatalog; documents?: ReadonlyMap<string, Record<string, unknown>> }
|
|
11
|
+
|
|
12
|
+
export function validateStandalone(notation: string, input: unknown, options: StandaloneOptions = {}): ValidationResult {
|
|
13
|
+
const errors: ValidationResult['errors'] = [], warnings: ValidationResult['warnings'] = [];
|
|
14
|
+
const fail = (field: string, expected: string, actual: unknown, code = 'SCHEMA_INVALID') => errors.push({code, message: `${notation} standalone: ${field}: expected ${expected}; actual ${JSON.stringify(actual) ?? 'missing'}.`, path: field});
|
|
15
|
+
if (!mapping(input)) { fail('$', 'mapping', input); return {valid:false, errors, warnings}; }
|
|
16
|
+
const d = input, field = ['draft', 'observation'].includes(notation);
|
|
17
|
+
const requiredText = (key: string) => { if (!text(d[key])) fail(key, 'nonempty string', d[key]); };
|
|
18
|
+
const optionalText = (key: string) => { if (key in d && !text(d[key])) fail(key, 'nonempty string', d[key]); };
|
|
19
|
+
if (d.notation !== notation && !(field && d.notation === undefined && d.type === notation.toUpperCase())) fail('notation', notation, d.notation);
|
|
20
|
+
if (!(notation === 'capability' ? capabilityId(d.id) : isCanonicalIdOfType(d.id, notation.toUpperCase()))) fail('id', `${notation.toUpperCase()} canonical ID`, d.id);
|
|
21
|
+
if (!field) requiredText('name'); else optionalText('name');
|
|
22
|
+
if (d.zone !== (field ? 'field' : 'canon')) fail('zone', field ? 'field' : 'canon', d.zone);
|
|
23
|
+
requiredText('admitted_by');
|
|
24
|
+
if (!isoDate(d.admitted_at)) fail('admitted_at', 'ISO calendar date', d.admitted_at);
|
|
25
|
+
if (!mapping(d.gate_checks)) fail('gate_checks', 'mapping', d.gate_checks);
|
|
26
|
+
else for (const key of field ? ['provenance'] : ['uniqueness', 'consistency', 'completeness']) {
|
|
27
|
+
if (!text(d.gate_checks[key])) fail(`gate_checks.${key}`, 'nonempty check outcome', d.gate_checks[key]);
|
|
28
|
+
}
|
|
29
|
+
if (!field) {
|
|
30
|
+
if (!isoDate(d.valid_from)) fail('valid_from', 'ISO calendar date', d.valid_from, 'LIFECYCLE-001');
|
|
31
|
+
if (!(d.valid_to === null || isoDate(d.valid_to))) fail('valid_to', 'ISO calendar date or null', d.valid_to, 'LIFECYCLE-002');
|
|
32
|
+
if (isoDate(d.valid_from) && isoDate(d.valid_to) && d.valid_to < d.valid_from) fail('valid_to', 'date on or after valid_from', d.valid_to, 'LIFECYCLE-003');
|
|
33
|
+
}
|
|
34
|
+
const ref = (key: string, value: unknown, types: string[]) => {
|
|
35
|
+
const type = typeOfId(value);
|
|
36
|
+
if (!type || !types.includes(type) || !(type === 'CAPABILITY' ? capabilityId(value) : isCanonicalIdOfType(value, type))) { fail(key, types.join('|') + ' canonical reference', value); return; }
|
|
37
|
+
if (options.catalog && !types.includes(options.catalog.typeOf(value as string) ?? '')) fail(key, 'reference to admitted ' + types.join('|'), value);
|
|
38
|
+
};
|
|
39
|
+
const reference = (key: string, types: string[], required = false) => { if (required || key in d) ref(key, d[key], types); };
|
|
40
|
+
const references = (key: string, types: string[], required = false) => {
|
|
41
|
+
if (!required && !(key in d)) return;
|
|
42
|
+
const values = d[key];
|
|
43
|
+
if (!Array.isArray(values) || (required && values.length === 0)) { fail(key, required ? 'nonempty list of references' : 'list of references', values); return; }
|
|
44
|
+
values.forEach((v, i) => ref(`${key}[${i}]`, v, types));
|
|
45
|
+
};
|
|
46
|
+
optionalText('description');
|
|
47
|
+
if (notation === 'product') {
|
|
48
|
+
if (!['digital_product','service','platform','bundle'].includes(d.type as string)) fail('type', 'digital_product|service|platform|bundle', d.type);
|
|
49
|
+
optionalText('domain'); reference('owner_role', ['ROLE']);
|
|
50
|
+
if ('maturity' in d && !(Number.isInteger(d.maturity) && Number(d.maturity) >= 1 && Number(d.maturity) <= 5)) fail('maturity', 'integer 1..5', d.maturity);
|
|
51
|
+
references('capabilities', ['CAPABILITY']); references('processes', ['PROCESS']); references('supporting_apps', ['APPLICATION']);
|
|
52
|
+
} else if (notation === 'capability') {
|
|
53
|
+
if (!['domain','supporting'].includes(d.type as string)) fail('type', 'domain|supporting', d.type);
|
|
54
|
+
for (const key of ['current_maturity','target_maturity','owner_role','target_date']) if (key in d) fail(key, 'attribute in history sidecar, absent inline', d[key], 'VERSIONED-004');
|
|
55
|
+
reference('business_process', ['PROCESS']); references('applications', ['APPLICATION']);
|
|
56
|
+
} else if (notation === 'role') {
|
|
57
|
+
optionalText('responsibility_area'); reference('unit', ['ACTOR']);
|
|
58
|
+
const unit = typeof d.unit === 'string' ? options.documents?.get(d.unit) : undefined;
|
|
59
|
+
if (unit && unit.type !== 'business_unit') fail('unit', 'ACTOR of type business_unit', d.unit);
|
|
60
|
+
} else if (notation === 'assessment') {
|
|
61
|
+
requiredText('description'); reference('assesses', ['DRIVER','FACTOR'], true);
|
|
62
|
+
for (const key of ['method','source']) optionalText(key);
|
|
63
|
+
if ('observed_at' in d && !isoDate(d.observed_at)) fail('observed_at', 'ISO calendar date', d.observed_at);
|
|
64
|
+
for (const key of ['polarity','swot','type']) if (key in d) fail(key, 'absent on assessment', d[key]);
|
|
65
|
+
if ('geographic_scope' in d) {
|
|
66
|
+
const values = d.geographic_scope;
|
|
67
|
+
// Country vocabulary is checked by the shared jurisdiction set below.
|
|
68
|
+
if (!Array.isArray(values) || !values.length || values.some(v => typeof v !== 'string' || !COUNTRIES.has(v.toLowerCase())) || (values.some(v => typeof v === 'string' && v.toLowerCase() === 'global') && values.length !== 1)) fail('geographic_scope', 'nonempty ISO country/eu list, or [global]', values, 'ASSESS-001');
|
|
69
|
+
}
|
|
70
|
+
} else if (notation === 'scenario') {
|
|
71
|
+
references('pursues', ['GOAL'], true); reference('arrives_at', ['TARGET_STATE'], true); references('steps', ['ACTION','CHANGE'], true);
|
|
72
|
+
const target = typeof d.arrives_at === 'string' ? options.documents?.get(d.arrives_at) : undefined;
|
|
73
|
+
if (target?.type === 'base') fail('arrives_at', 'target plateau, not base', d.arrives_at);
|
|
74
|
+
for (const key of ['type','vision','factors_view','status','drivers','capabilities','processes','applications','products']) if (key in d) fail(key, 'absent on standalone scenario', d[key]);
|
|
75
|
+
optionalText('link');
|
|
76
|
+
} else if (field) {
|
|
77
|
+
const provenance = d.provenance;
|
|
78
|
+
if (provenance === undefined || provenance === null || (typeof provenance === 'string' && !provenance.trim()) || (mapping(provenance) && Object.keys(provenance).length === 0)) fail('provenance', 'recorded source evidence', provenance);
|
|
79
|
+
else if (!mapping(provenance)) warnings.push({code:'NOTATION-SKIP-001',message:'Field provenance form is unsupported; source evidence remains unvalidated.'});
|
|
80
|
+
else {
|
|
81
|
+
// Two supported evidence forms: captured source and versioned source import.
|
|
82
|
+
// Other forms remain visible as unsupported, not falsely validated or forbidden.
|
|
83
|
+
const captured = text(provenance.captured_by) && isoDate(provenance.captured_on) && text(provenance.setting);
|
|
84
|
+
const imported = text(provenance.source_revision) && text(provenance.original_path) && text(provenance.source_hash) && isoDate(provenance.captured_on);
|
|
85
|
+
if (!captured && !imported) warnings.push({code:'NOTATION-SKIP-001',message:'Field provenance form is unsupported: source, capture date and context cannot all be checked.'});
|
|
86
|
+
}
|
|
87
|
+
if ('source_quality' in d && !['authoritative','corroborated','single_source','unverified'].includes(d.source_quality as string)) fail('source_quality', 'authoritative|corroborated|single_source|unverified', d.source_quality);
|
|
88
|
+
}
|
|
89
|
+
return {valid: errors.length === 0, errors, warnings};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ISO 3166-1 alpha-2 vocabulary, plus the published eu/global scope tokens.
|
|
93
|
+
const COUNTRIES = new Set(('ad ae af ag ai al am ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bl bm bn bo bq br bs bt bv bw by bz ca cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz de dj dk dm do dz ec ee eg eh er es et fi fj fk fm fo fr ga gb gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mf mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sj sk sl sm sn so sr ss st sv sx sy sz tc td tf tg th tj tk tl tm tn to tr tt tv tw tz ua ug um us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw eu global').split(' '));
|