@transitrix/diagrams 1.14.0-rc.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/standalone/history.d.ts +3 -0
- package/dist/standalone/history.js +67 -0
- package/dist/standalone/validate.d.ts +9 -0
- package/dist/standalone/validate.js +146 -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/standalone/history.ts +45 -0
- package/src/standalone/validate.ts +93 -0
- package/src/webview/entry.ts +6 -2
- package/src/webview/render-goals.ts +2 -1
- package/src/webview/styles.css +8 -0
|
@@ -24,6 +24,10 @@ export function validateScenario(input) {
|
|
|
24
24
|
}
|
|
25
25
|
if (errors.length > 0)
|
|
26
26
|
return { valid: false, errors, warnings };
|
|
27
|
+
if (('view_config' in raw || 'view' in raw || 'scenarios' in raw) && !('scenario' in raw)) {
|
|
28
|
+
return { valid: true, errors, warnings: [{ code: 'NOTATION-SKIP-001',
|
|
29
|
+
message: 'scenarios projection or scenario-set form is unsupported and remains unvalidated.' }] };
|
|
30
|
+
}
|
|
27
31
|
const scn = raw['scenario'];
|
|
28
32
|
if (!scn || typeof scn !== 'object') {
|
|
29
33
|
errors.push({ code: 'SCN-002', message: 'Missing required field: scenario' });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Shared schema identity; callers supply the owning form and exact field. */
|
|
2
|
+
export function schemaFinding(form, path, expected, actual) {
|
|
3
|
+
const type = actual === null ? 'null' : Array.isArray(actual) ? 'array' : typeof actual;
|
|
4
|
+
return { code: 'SCHEMA_INVALID', path,
|
|
5
|
+
message: `${form} field '${path}': expected ${expected}; actual ${type} (${String(actual)}).` };
|
|
6
|
+
}
|
|
@@ -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/dist/webview/entry.js
CHANGED
|
@@ -185,8 +185,13 @@ function dispatchValidate(kind, doc) {
|
|
|
185
185
|
return renderFromValidation('process-map', validateProcessMap(doc), () => renderProcessMapHtml(doc.process_map));
|
|
186
186
|
case 'scenarios':
|
|
187
187
|
return renderFromValidation('scenarios', validateScenario(doc), () => renderScenarioHtml(doc.scenario));
|
|
188
|
-
case 'capability-map':
|
|
189
|
-
|
|
188
|
+
case 'capability-map': {
|
|
189
|
+
const validation = validateCapabilityMap(doc);
|
|
190
|
+
const unresolved = validation.warnings.find(w => w.code === 'NOTATION-SKIP-001');
|
|
191
|
+
if (validation.valid && unresolved)
|
|
192
|
+
return errorResult('capability-map', unresolved.code, unresolved.message);
|
|
193
|
+
return renderFromValidation('capability-map', validation, () => renderCapabilityMapHtml(doc.capability_map));
|
|
194
|
+
}
|
|
190
195
|
default:
|
|
191
196
|
return errorResult(kind, 'NOTATION-NOT-WIRED', `Notation '${kind}' is not wired into the IntelliJ bundle yet.`);
|
|
192
197
|
}
|
|
@@ -41,7 +41,8 @@ export function renderGoalsSvg(tree, options = {}) {
|
|
|
41
41
|
const title = treeName
|
|
42
42
|
? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(`Goal tree — ${treeName}`)}</text>`
|
|
43
43
|
: '';
|
|
44
|
-
|
|
44
|
+
// Reserve a separate header row; the theme centers text on its y coordinate.
|
|
45
|
+
return renderGoalsLayoutSvg(layout, { curvature, entryCurvature, edgeStyle, title, topInset: title ? PAD : 0, embedCssTheme: 'transitrix' });
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
48
|
* The single goals SVG emitter shared by every host. Takes an already-computed
|
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** ACTION field types and the manifest-selected 7.0.0 numeric boundary. */
|
|
2
|
+
export function validateActionFields(
|
|
3
|
+
action: Record<string, unknown>, methodologyVersion?: string,
|
|
4
|
+
): Array<{ code: string; message: string; path: string }> {
|
|
5
|
+
const errors: Array<{ code: string; message: string; path: string }> = [];
|
|
6
|
+
const version = /^(\d+)\.(\d+)\.(\d+)(-[^+]+)?(?:\+.*)?$/.exec(methodologyVersion ?? '');
|
|
7
|
+
const nonnegative = version !== null && (Number(version[1]) > 7 ||
|
|
8
|
+
(Number(version[1]) === 7 && (Number(version[2]) > 0 || Number(version[3]) > 0 || !version[4])));
|
|
9
|
+
const type = (value: unknown): string => value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
|
|
10
|
+
const schema = (path: string, expected: string, value: unknown) => errors.push({
|
|
11
|
+
code: 'SCHEMA_INVALID', path,
|
|
12
|
+
message: `ACTION field '${path}' expected ${expected}, actual ${type(value)} (${String(value)}).`,
|
|
13
|
+
});
|
|
14
|
+
for (const field of ['duration', 'duration_days', 'labor_cost', 'resources_cost', 'effort', 'score', 'sort']) {
|
|
15
|
+
const value = action[field];
|
|
16
|
+
if (value === undefined || (value === null && (field === 'duration' || field === 'duration_days'))) continue;
|
|
17
|
+
const integer = field === 'score' || field === 'sort';
|
|
18
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || (integer && !Number.isInteger(value))) {
|
|
19
|
+
schema(field, integer ? 'finite integer' : 'finite number', value);
|
|
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)) schema('predecessors', 'array of strings', action.predecessors);
|
|
26
|
+
else action.predecessors.forEach((value, index) => {
|
|
27
|
+
if (typeof value !== 'string') schema(`predecessors[${index}]`, 'string', value);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return errors;
|
|
31
|
+
}
|
|
@@ -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
|
}
|