@transitrix/diagrams 1.8.21 → 1.9.18
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/activity-card/types.js +1 -1
- package/dist/agreement.d.ts +15 -0
- package/dist/agreement.js +61 -0
- package/dist/applications/index.d.ts +2 -0
- package/dist/applications/index.js +1 -0
- package/dist/applications/resolve-maturity.d.ts +16 -0
- package/dist/applications/resolve-maturity.js +57 -0
- package/dist/applications/validate.js +20 -0
- package/dist/capability-map/CapabilityCardNode.d.ts +1 -1
- package/dist/capability-map/CapabilityRootNode.d.ts +1 -1
- package/dist/capability-map/index.d.ts +2 -0
- package/dist/capability-map/index.js +1 -0
- package/dist/capability-map/resolve-maturity.d.ts +20 -0
- package/dist/capability-map/resolve-maturity.js +72 -0
- package/dist/capability-map/validate.js +1 -2
- package/dist/compliance/classify.d.ts +10 -1
- package/dist/compliance/classify.js +44 -2
- package/dist/compliance/confidence.js +1 -1
- package/dist/compliance/gap-report.d.ts +31 -1
- package/dist/compliance/gap-report.js +55 -3
- package/dist/compliance/html.js +14 -6
- package/dist/compliance/impact.js +1 -1
- package/dist/compliance/index.d.ts +3 -1
- package/dist/compliance/index.js +2 -0
- package/dist/compliance/markdown.js +26 -4
- package/dist/compliance/reverse-index.d.ts +6 -1
- package/dist/compliance/reverse-index.js +27 -3
- package/dist/compliance/trace.js +5 -1
- package/dist/compliance/types.d.ts +70 -0
- package/dist/compliance/types.js +1 -2
- package/dist/compliance/views.js +1 -1
- package/dist/compliance-matrix/types.d.ts +1 -1
- package/dist/compliance-matrix/types.js +1 -1
- package/dist/confidence/score.js +1 -1
- package/dist/constraint/validate.js +3 -0
- package/dist/edge-path.js +1 -1
- package/dist/factor/validate.js +2 -2
- package/dist/fgca/chain-table.js +1 -1
- package/dist/fgca/nodeTypes.d.ts +4 -4
- package/dist/fgca/nodes/FGCAActivityNode.d.ts +1 -1
- package/dist/fgca/nodes/FGCAChangeNode.d.ts +1 -1
- package/dist/fgca/nodes/FGCAFactorNode.d.ts +1 -1
- package/dist/fgca/nodes/FGCAGoalNode.d.ts +1 -1
- package/dist/fgca/parse-canonical.d.ts +2 -2
- package/dist/fgca/parse-canonical.js +2 -2
- package/dist/fgca/preview-layout.d.ts +1 -1
- package/dist/goals/GoalNode.d.ts +1 -1
- package/dist/goals/layout.d.ts +1 -1
- package/dist/goals/layout.js +1 -1
- package/dist/goals/parse-canonical.d.ts +1 -1
- package/dist/goals/parse-canonical.js +1 -1
- package/dist/goals/types.d.ts +1 -1
- package/dist/goals/validate.js +2 -2
- package/dist/link-suspicion.d.ts +43 -0
- package/dist/link-suspicion.js +156 -0
- package/dist/metric/index.d.ts +4 -0
- package/dist/metric/index.js +2 -0
- package/dist/metric/types.d.ts +24 -0
- package/dist/metric/types.js +7 -0
- package/dist/metric/validate.d.ts +7 -0
- package/dist/metric/validate.js +90 -0
- package/dist/need/index.d.ts +3 -0
- package/dist/need/index.js +1 -0
- package/dist/need/types.d.ts +21 -0
- package/dist/need/types.js +3 -0
- package/dist/need/validate.d.ts +7 -0
- package/dist/need/validate.js +60 -0
- package/dist/repo-validate/check-versioned-attributes.d.ts +4 -0
- package/dist/repo-validate/check-versioned-attributes.js +105 -0
- package/dist/repo-validate/validate-repo.js +3 -0
- package/dist/requirement/index.d.ts +2 -2
- package/dist/requirement/index.js +1 -1
- package/dist/requirement/types.d.ts +18 -0
- package/dist/requirement/types.js +2 -0
- package/dist/requirement/validate.js +22 -1
- package/dist/risk/index.d.ts +4 -0
- package/dist/risk/index.js +2 -0
- package/dist/risk/types.d.ts +25 -0
- package/dist/risk/types.js +3 -0
- package/dist/risk/validate.d.ts +7 -0
- package/dist/risk/validate.js +108 -0
- package/dist/schema-version.d.ts +9 -6
- package/dist/schema-version.js +9 -6
- package/dist/theme/themes.js +3 -2
- package/dist/validation/index.d.ts +4 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/types.d.ts +31 -0
- package/dist/validation/types.js +21 -0
- package/dist/validation/validate.d.ts +7 -0
- package/dist/validation/validate.js +106 -0
- package/dist/verification/index.d.ts +4 -0
- package/dist/verification/index.js +2 -0
- package/dist/verification/types.d.ts +31 -0
- package/dist/verification/types.js +12 -0
- package/dist/verification/validate.d.ts +7 -0
- package/dist/verification/validate.js +104 -0
- package/dist/versioned-attribute/index.d.ts +4 -0
- package/dist/versioned-attribute/index.js +2 -0
- package/dist/versioned-attribute/resolve.d.ts +15 -0
- package/dist/versioned-attribute/resolve.js +37 -0
- package/dist/versioned-attribute/types.d.ts +12 -0
- package/dist/versioned-attribute/types.js +6 -0
- package/dist/versioned-attribute/validate.d.ts +17 -0
- package/dist/versioned-attribute/validate.js +87 -0
- package/dist/webview/render-applications.d.ts +15 -1
- package/dist/webview/render-applications.js +7 -2
- package/dist/webview/render-blocks.js +6 -4
- package/dist/webview/render-capability-map.d.ts +16 -1
- package/dist/webview/render-capability-map.js +4 -1
- package/dist/webview/render-process.d.ts +1 -1
- package/dist/webview/render-process.js +5 -5
- package/package.json +9 -6
- package/src/activity-card/types.ts +1 -1
- package/src/agreement.ts +71 -0
- package/src/applications/index.ts +3 -0
- package/src/applications/resolve-maturity.ts +66 -0
- package/src/applications/validate.ts +23 -0
- package/src/capability-map/index.ts +7 -0
- package/src/capability-map/resolve-maturity.ts +84 -0
- package/src/capability-map/validate.ts +1 -2
- package/src/compliance/classify.ts +54 -3
- package/src/compliance/confidence.ts +1 -1
- package/src/compliance/gap-report.ts +91 -4
- package/src/compliance/html.ts +18 -6
- package/src/compliance/impact.ts +1 -1
- package/src/compliance/index.ts +6 -0
- package/src/compliance/markdown.ts +18 -4
- package/src/compliance/reverse-index.ts +35 -4
- package/src/compliance/trace.ts +7 -1
- package/src/compliance/types.ts +75 -2
- package/src/compliance/views.ts +1 -1
- package/src/compliance-matrix/types.ts +2 -2
- package/src/confidence/score.ts +1 -1
- package/src/constraint/validate.ts +4 -0
- package/src/edge-path.ts +1 -1
- package/src/factor/validate.ts +2 -2
- package/src/fgca/chain-table.ts +1 -1
- package/src/fgca/parse-canonical.ts +2 -2
- package/src/fgca/preview-layout.ts +1 -1
- package/src/goals/layout.ts +1 -1
- package/src/goals/parse-canonical.ts +1 -1
- package/src/goals/types.ts +1 -1
- package/src/goals/validate.ts +2 -2
- package/src/link-suspicion.ts +181 -0
- package/src/metric/index.ts +4 -0
- package/src/metric/types.ts +38 -0
- package/src/metric/validate.ts +102 -0
- package/src/need/index.ts +3 -0
- package/src/need/types.ts +31 -0
- package/src/need/validate.ts +71 -0
- package/src/repo-validate/check-versioned-attributes.ts +112 -0
- package/src/repo-validate/validate-repo.ts +3 -0
- package/src/requirement/index.ts +2 -2
- package/src/requirement/types.ts +25 -0
- package/src/requirement/validate.ts +25 -1
- package/src/risk/index.ts +4 -0
- package/src/risk/types.ts +35 -0
- package/src/risk/validate.ts +121 -0
- package/src/schema-version.ts +9 -6
- package/src/theme/themes.ts +3 -2
- package/src/validation/index.ts +4 -0
- package/src/validation/types.ts +57 -0
- package/src/validation/validate.ts +113 -0
- package/src/verification/index.ts +4 -0
- package/src/verification/types.ts +48 -0
- package/src/verification/validate.ts +111 -0
- package/src/versioned-attribute/index.ts +4 -0
- package/src/versioned-attribute/resolve.ts +45 -0
- package/src/versioned-attribute/types.ts +19 -0
- package/src/versioned-attribute/validate.ts +109 -0
- package/src/webview/render-applications.ts +25 -2
- package/src/webview/render-blocks.ts +8 -4
- package/src/webview/render-capability-map.ts +23 -1
- package/src/webview/render-process.ts +5 -5
- package/src/webview/styles.css +6 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Link suspicion — methodology CONTRACT.md §16, reference implementation
|
|
2
|
+
// `scripts/check-link-suspicion.mjs`. Pure: content-identity normalisation
|
|
3
|
+
// (§16.1) and the suspicion verdict (§16.2 / §16.3's mechanical hatch) over
|
|
4
|
+
// already-fetched file text. Git plumbing (finding the anchor commit,
|
|
5
|
+
// reading a file at a ref) and the repo walk are IO and live in the CLI
|
|
6
|
+
// (`src/repo-validate.ts`), same split as the rest of this package.
|
|
7
|
+
//
|
|
8
|
+
// Nothing here is stored: a caller re-derives the verdict on every check;
|
|
9
|
+
// no file ever carries a `suspicious: true` flag (§16.2).
|
|
10
|
+
// The administrative envelope (§16.1) — field names CONTRACT.md already
|
|
11
|
+
// defines by name (§6 / §6.1 / §6.2 / §6.3 when present / §7). One list,
|
|
12
|
+
// defined once; a spec revision that adds an envelope field extends this
|
|
13
|
+
// list in the same revision rather than inventing a second mechanism.
|
|
14
|
+
export const ENVELOPE_FIELDS = [
|
|
15
|
+
'zone', 'admitted_at', 'admitted_by', 'gate_checks', 'derived_from',
|
|
16
|
+
'admission_state', 'proposed_at', 'proposed_by', 'owner_to_confirm',
|
|
17
|
+
'rejected_at', 'rejected_by', 'rejection_reason',
|
|
18
|
+
'reviewer_authority',
|
|
19
|
+
'agreement', 'agreed_by', 'agreed_at',
|
|
20
|
+
'valid_from', 'valid_to',
|
|
21
|
+
];
|
|
22
|
+
function stripComment(line) {
|
|
23
|
+
let inQuotes = false;
|
|
24
|
+
for (let i = 0; i < line.length; i++) {
|
|
25
|
+
const ch = line[i];
|
|
26
|
+
if (ch === '"')
|
|
27
|
+
inQuotes = !inQuotes;
|
|
28
|
+
else if (ch === '#' && !inQuotes)
|
|
29
|
+
return line.slice(0, i);
|
|
30
|
+
}
|
|
31
|
+
return line;
|
|
32
|
+
}
|
|
33
|
+
/** Every non-blank, non-comment line of the file, except those belonging to
|
|
34
|
+
* an `ENVELOPE_FIELDS` block (the key line and its more-indented
|
|
35
|
+
* continuation lines), whitespace-normalised and sorted —
|
|
36
|
+
* order-independent, formatting-independent, comment-independent. Not a
|
|
37
|
+
* general YAML parser — a flat, line-oriented pass over the already-known
|
|
38
|
+
* envelope field names, the same posture the methodology reference script
|
|
39
|
+
* takes (its own header comment cites `baseline-manifest.mjs` /
|
|
40
|
+
* `check-agreement.mjs` for precedent). */
|
|
41
|
+
export function statementLines(text) {
|
|
42
|
+
const kept = [];
|
|
43
|
+
let skipIndent = null;
|
|
44
|
+
for (const raw of text.split('\n')) {
|
|
45
|
+
const stripped = stripComment(raw).replace(/\s+$/, '');
|
|
46
|
+
if (!stripped.trim())
|
|
47
|
+
continue;
|
|
48
|
+
const indent = stripped.match(/^ */)?.[0].length ?? 0;
|
|
49
|
+
if (skipIndent !== null) {
|
|
50
|
+
if (indent > skipIndent)
|
|
51
|
+
continue;
|
|
52
|
+
skipIndent = null;
|
|
53
|
+
}
|
|
54
|
+
const keyMatch = stripped.match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*):/);
|
|
55
|
+
if (keyMatch && keyMatch[1].length === 0 && ENVELOPE_FIELDS.includes(keyMatch[2])) {
|
|
56
|
+
skipIndent = indent;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
kept.push(stripped.trim().replace(/\s+/g, ' '));
|
|
60
|
+
}
|
|
61
|
+
return kept.sort();
|
|
62
|
+
}
|
|
63
|
+
function sameLines(a, b) {
|
|
64
|
+
return a.length === b.length && a.every((line, i) => line === b[i]);
|
|
65
|
+
}
|
|
66
|
+
function unquote(s) {
|
|
67
|
+
const m = s.match(/^"(.*)"$/);
|
|
68
|
+
return m ? m[1] : s;
|
|
69
|
+
}
|
|
70
|
+
function normalizeEditLine(s) {
|
|
71
|
+
return unquote(s.trim()).trim().replace(/\s+/g, ' ');
|
|
72
|
+
}
|
|
73
|
+
/** Minimal parse of the fixed `TRANSFORM.yaml` shape — `mechanical: true`,
|
|
74
|
+
* `applies_to: [exact relative paths]`, `line_edits: [{from, to}]`. Not a
|
|
75
|
+
* general YAML parser, same posture as {@link statementLines}. */
|
|
76
|
+
export function parseMigrationManifest(text) {
|
|
77
|
+
const manifest = { mechanical: false, appliesTo: [], lineEdits: [] };
|
|
78
|
+
let section = null;
|
|
79
|
+
let current = null;
|
|
80
|
+
for (const raw of text.split('\n')) {
|
|
81
|
+
const line = raw.replace(/\s+$/, '');
|
|
82
|
+
if (!line.trim() || line.trim().startsWith('#'))
|
|
83
|
+
continue;
|
|
84
|
+
const topMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
85
|
+
if (topMatch) {
|
|
86
|
+
const [, key, val] = topMatch;
|
|
87
|
+
if (key === 'mechanical') {
|
|
88
|
+
manifest.mechanical = val.trim() === 'true';
|
|
89
|
+
section = null;
|
|
90
|
+
}
|
|
91
|
+
else if (key === 'applies_to') {
|
|
92
|
+
section = 'applies_to';
|
|
93
|
+
}
|
|
94
|
+
else if (key === 'line_edits') {
|
|
95
|
+
section = 'line_edits';
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (section === 'applies_to') {
|
|
100
|
+
const m = line.match(/^\s*-\s*(.+)$/);
|
|
101
|
+
if (m)
|
|
102
|
+
manifest.appliesTo.push(unquote(m[1].trim()));
|
|
103
|
+
}
|
|
104
|
+
else if (section === 'line_edits') {
|
|
105
|
+
const fromMatch = line.match(/^\s*-\s*from:\s*(.+)$/);
|
|
106
|
+
if (fromMatch) {
|
|
107
|
+
current = { from: normalizeEditLine(fromMatch[1]), to: '' };
|
|
108
|
+
manifest.lineEdits.push(current);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const toMatch = line.match(/^\s*to:\s*(.+)$/);
|
|
112
|
+
if (toMatch && current)
|
|
113
|
+
current.to = normalizeEditLine(toMatch[1]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return manifest;
|
|
117
|
+
}
|
|
118
|
+
/** Replay a manifest's declared `lineEdits` against the before-state and
|
|
119
|
+
* check the result matches the after-state exactly — the independent
|
|
120
|
+
* verification §16.3 requires: the tool's `mechanical: true` declaration is
|
|
121
|
+
* never trusted on its own; only a replay that reproduces the after-state
|
|
122
|
+
* suppresses suspicion. */
|
|
123
|
+
function replayExplains(beforeLines, afterLines, lineEdits) {
|
|
124
|
+
const working = [...beforeLines];
|
|
125
|
+
for (const edit of lineEdits) {
|
|
126
|
+
const idx = working.indexOf(edit.from);
|
|
127
|
+
if (idx === -1)
|
|
128
|
+
return false;
|
|
129
|
+
working.splice(idx, 1, edit.to);
|
|
130
|
+
}
|
|
131
|
+
return sameLines([...working].sort(), [...afterLines].sort());
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* §16.2: *Suspicious* ⟺ the target's content identity at the anchor commit
|
|
135
|
+
* differs from its content identity now, **and** §16.3's hatch does not
|
|
136
|
+
* explain the difference.
|
|
137
|
+
*
|
|
138
|
+
* `beforeText` / `afterText` are already-fetched (git IO lives in the
|
|
139
|
+
* caller); `undefined` for either (no anchor, or the target no longer
|
|
140
|
+
* exists) means nothing to compare — not suspicious, per the reference
|
|
141
|
+
* script's `target-absent-at-anchor` / `target-absent-now` cases.
|
|
142
|
+
*/
|
|
143
|
+
export function checkSuspicion(beforeText, afterText, applicableManifests = []) {
|
|
144
|
+
if (beforeText === undefined || afterText === undefined)
|
|
145
|
+
return { suspicious: false };
|
|
146
|
+
const beforeLines = statementLines(beforeText);
|
|
147
|
+
const afterLines = statementLines(afterText);
|
|
148
|
+
if (sameLines(beforeLines, afterLines))
|
|
149
|
+
return { suspicious: false };
|
|
150
|
+
for (const manifest of applicableManifests) {
|
|
151
|
+
if (replayExplains(beforeLines, afterLines, manifest.lineEdits)) {
|
|
152
|
+
return { suspicious: false };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { suspicious: true, hatchRefused: applicableManifests.some((m) => m.mechanical) };
|
|
156
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
2
|
+
/** How to read movement relative to `target` (§7.27). */
|
|
3
|
+
export type MetricDirectionOfGood = 'higher_is_better' | 'lower_is_better' | 'on_target';
|
|
4
|
+
export declare const METRIC_DIRECTIONS_OF_GOOD: readonly MetricDirectionOfGood[];
|
|
5
|
+
export interface Metric {
|
|
6
|
+
notation: 'metric';
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
/** Typed IDs of what this metric tracks — GOAL, CAPABILITY, or PROCESS. Non-empty. */
|
|
10
|
+
measures: string[];
|
|
11
|
+
/** Free-form unit the value and `target` are expressed in (percent, days, …). */
|
|
12
|
+
unit: string;
|
|
13
|
+
target: number;
|
|
14
|
+
direction_of_good: MetricDirectionOfGood;
|
|
15
|
+
/** ROLE-… accountable for tracking this metric and acting on it. */
|
|
16
|
+
owner_role: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
zone: 'canon';
|
|
19
|
+
admitted_at: string;
|
|
20
|
+
admitted_by: string;
|
|
21
|
+
gate_checks: GateChecks;
|
|
22
|
+
valid_from: string;
|
|
23
|
+
valid_to: string | null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ValidationResult } from '../validation-types.js';
|
|
2
|
+
import { type CanonCatalog } from '../typed-id.js';
|
|
3
|
+
export interface MetricValidateOptions {
|
|
4
|
+
/** When provided, METRIC-002/004 enforce artefact existence for cross-refs. */
|
|
5
|
+
catalog?: CanonCatalog;
|
|
6
|
+
}
|
|
7
|
+
export declare function validateMetric(input: unknown, options?: MetricValidateOptions): ValidationResult;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// METRIC validator — methodology ELEMENT_PRIMITIVES.md §7.27 / §9.
|
|
2
|
+
//
|
|
3
|
+
// Codes:
|
|
4
|
+
// METRIC-001 — shape / id grammar / required envelope + per-type fields
|
|
5
|
+
// (measures, unit, target, direction_of_good, owner_role).
|
|
6
|
+
// METRIC-002 — measures is empty, or an entry does not resolve to an
|
|
7
|
+
// admitted GOAL / CAPABILITY / PROCESS in canon.
|
|
8
|
+
// METRIC-003 — direction_of_good outside {higher_is_better, lower_is_better,
|
|
9
|
+
// on_target}.
|
|
10
|
+
// METRIC-004 — owner_role does not resolve to an admitted ROLE in canon.
|
|
11
|
+
//
|
|
12
|
+
// Resolution rules (METRIC-002/004) that need artefact *existence* run only
|
|
13
|
+
// when a `CanonCatalog` is supplied; the TYPE checks are derivable from the
|
|
14
|
+
// id prefix and run either way.
|
|
15
|
+
import { typeOfId, isCanonicalIdOfType } from '../typed-id.js';
|
|
16
|
+
import { METRIC_DIRECTIONS_OF_GOOD } from './types.js';
|
|
17
|
+
const MEASURES_TYPES = ['GOAL', 'CAPABILITY', 'PROCESS'];
|
|
18
|
+
export function validateMetric(input, options = {}) {
|
|
19
|
+
const errors = [];
|
|
20
|
+
const warnings = [];
|
|
21
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
22
|
+
errors.push({ code: 'METRIC-001', message: 'Metric must be a YAML mapping.' });
|
|
23
|
+
return { valid: false, errors, warnings };
|
|
24
|
+
}
|
|
25
|
+
const m = input;
|
|
26
|
+
// METRIC-001 — id grammar + notation tag + envelope + plain per-type fields.
|
|
27
|
+
if (!isCanonicalIdOfType(m.id, 'METRIC')) {
|
|
28
|
+
errors.push({ code: 'METRIC-001', message: `id "${String(m.id)}" must match METRIC-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
29
|
+
}
|
|
30
|
+
if (m.notation !== 'metric') {
|
|
31
|
+
errors.push({ code: 'METRIC-001', message: 'notation must be the fixed value "metric".', path: 'notation' });
|
|
32
|
+
}
|
|
33
|
+
if (m.zone !== 'canon') {
|
|
34
|
+
errors.push({ code: 'METRIC-001', message: 'zone is required and must be "canon".', path: 'zone' });
|
|
35
|
+
}
|
|
36
|
+
for (const f of ['name', 'admitted_at', 'admitted_by', 'valid_from', 'unit', 'owner_role']) {
|
|
37
|
+
if (typeof m[f] !== 'string' || m[f].trim() === '') {
|
|
38
|
+
errors.push({ code: 'METRIC-001', message: `${f} is required.`, path: f });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (m.gate_checks === null || typeof m.gate_checks !== 'object') {
|
|
42
|
+
errors.push({ code: 'METRIC-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
43
|
+
}
|
|
44
|
+
if (!('valid_to' in m) || !(typeof m.valid_to === 'string' || m.valid_to === null)) {
|
|
45
|
+
errors.push({ code: 'METRIC-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
46
|
+
}
|
|
47
|
+
if (typeof m.target !== 'number' || Number.isNaN(m.target)) {
|
|
48
|
+
errors.push({ code: 'METRIC-001', message: 'target is required and must be a number.', path: 'target' });
|
|
49
|
+
}
|
|
50
|
+
// direction_of_good — required (METRIC-001) + enum (METRIC-003).
|
|
51
|
+
const dir = m.direction_of_good;
|
|
52
|
+
if (dir === undefined || dir === null || dir === '') {
|
|
53
|
+
errors.push({ code: 'METRIC-001', message: 'direction_of_good is required.', path: 'direction_of_good' });
|
|
54
|
+
}
|
|
55
|
+
else if (!METRIC_DIRECTIONS_OF_GOOD.includes(dir)) {
|
|
56
|
+
errors.push({
|
|
57
|
+
code: 'METRIC-003',
|
|
58
|
+
message: `direction_of_good "${String(dir)}" must be one of ${METRIC_DIRECTIONS_OF_GOOD.join(', ')}.`,
|
|
59
|
+
path: 'direction_of_good',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// measures — required non-empty list (METRIC-001), each resolves to GOAL/CAPABILITY/PROCESS (METRIC-002).
|
|
63
|
+
const measures = m.measures;
|
|
64
|
+
if (!Array.isArray(measures) || measures.length === 0) {
|
|
65
|
+
errors.push({ code: 'METRIC-001', message: 'measures is required and must be a non-empty list of typed IDs.', path: 'measures' });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
measures.forEach((ref, i) => {
|
|
69
|
+
const type = typeOfId(ref);
|
|
70
|
+
if (!type || !MEASURES_TYPES.includes(type)) {
|
|
71
|
+
errors.push({ code: 'METRIC-002', message: `measures[${i}] "${String(ref)}" must resolve to a GOAL, CAPABILITY, or PROCESS.`, path: `measures[${i}]` });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (options.catalog && options.catalog.typeOf(ref) === undefined) {
|
|
75
|
+
errors.push({ code: 'METRIC-002', message: `measures[${i}] "${String(ref)}" does not resolve to an admitted artefact.`, path: `measures[${i}]` });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// owner_role — must be a typed ROLE id (METRIC-004), and resolve when a catalog is supplied.
|
|
80
|
+
const ownerRole = m.owner_role;
|
|
81
|
+
if (typeof ownerRole === 'string' && ownerRole.trim() !== '') {
|
|
82
|
+
if (!isCanonicalIdOfType(ownerRole, 'ROLE')) {
|
|
83
|
+
errors.push({ code: 'METRIC-004', message: `owner_role "${ownerRole}" must be a typed ROLE id.`, path: 'owner_role' });
|
|
84
|
+
}
|
|
85
|
+
else if (options.catalog && options.catalog.typeOf(ownerRole) === undefined) {
|
|
86
|
+
errors.push({ code: 'METRIC-004', message: `owner_role "${ownerRole}" does not resolve to an admitted artefact.`, path: 'owner_role' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { validateNeed } from './validate.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
2
|
+
import type { AgreementValue } from '../agreement.js';
|
|
3
|
+
export interface Need {
|
|
4
|
+
notation: 'need';
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
/** Typed ID of the STAKEHOLDER whose need this is. */
|
|
8
|
+
stakeholder: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
/** `draft` \| `agreed` \| `disputed`. Absent ⇒ `agreed` (back-compat). */
|
|
11
|
+
agreement?: AgreementValue;
|
|
12
|
+
/** Required whenever `agreement` is present (AGREE-003). Written only by a human when `agreement: agreed` (AGREE-002). */
|
|
13
|
+
agreed_by?: string;
|
|
14
|
+
agreed_at?: string;
|
|
15
|
+
zone: 'canon';
|
|
16
|
+
admitted_at: string;
|
|
17
|
+
admitted_by: string;
|
|
18
|
+
gate_checks: GateChecks;
|
|
19
|
+
valid_from: string;
|
|
20
|
+
valid_to: string | null;
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ValidationResult } from '../validation-types.js';
|
|
2
|
+
import { type CanonCatalog } from '../typed-id.js';
|
|
3
|
+
export interface NeedValidateOptions {
|
|
4
|
+
/** When provided, NEED-002 enforces artefact existence for `stakeholder`. */
|
|
5
|
+
catalog?: CanonCatalog;
|
|
6
|
+
}
|
|
7
|
+
export declare function validateNeed(input: unknown, options?: NeedValidateOptions): ValidationResult;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// NEED validator — methodology ELEMENT_PRIMITIVES.md §7.28 / §9.
|
|
2
|
+
//
|
|
3
|
+
// Codes:
|
|
4
|
+
// NEED-001 — shape / id grammar / required envelope fields, including the
|
|
5
|
+
// required per-type `stakeholder` field.
|
|
6
|
+
// NEED-002 — stakeholder does not resolve to an admitted STAKEHOLDER in canon.
|
|
7
|
+
//
|
|
8
|
+
// The cross-cutting NEED-COVERAGE-001 / NEED-VALIDATION-COVERAGE-001..002
|
|
9
|
+
// reverse-trace rules are owned elsewhere (CONTRACT.md §8, the compliance
|
|
10
|
+
// reverse-index) and are out of scope for this single-artefact validator —
|
|
11
|
+
// same posture as `validateRequirement` / `validateVerification`.
|
|
12
|
+
//
|
|
13
|
+
// Resolution (NEED-002 existence) runs only when a `CanonCatalog` is
|
|
14
|
+
// supplied; the TYPE check is derivable from the id prefix and runs either way.
|
|
15
|
+
import { isCanonicalIdOfType } from '../typed-id.js';
|
|
16
|
+
import { checkAgreement } from '../agreement.js';
|
|
17
|
+
export function validateNeed(input, options = {}) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
const warnings = [];
|
|
20
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
21
|
+
errors.push({ code: 'NEED-001', message: 'Need must be a YAML mapping.' });
|
|
22
|
+
return { valid: false, errors, warnings };
|
|
23
|
+
}
|
|
24
|
+
const n = input;
|
|
25
|
+
// NEED-001 — id grammar + notation tag + envelope.
|
|
26
|
+
if (!isCanonicalIdOfType(n.id, 'NEED')) {
|
|
27
|
+
errors.push({ code: 'NEED-001', message: `id "${String(n.id)}" must match NEED-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
28
|
+
}
|
|
29
|
+
if (n.notation !== 'need') {
|
|
30
|
+
errors.push({ code: 'NEED-001', message: 'notation must be the fixed value "need".', path: 'notation' });
|
|
31
|
+
}
|
|
32
|
+
if (n.zone !== 'canon') {
|
|
33
|
+
errors.push({ code: 'NEED-001', message: 'zone is required and must be "canon".', path: 'zone' });
|
|
34
|
+
}
|
|
35
|
+
for (const f of ['name', 'admitted_at', 'admitted_by', 'valid_from']) {
|
|
36
|
+
if (typeof n[f] !== 'string' || n[f].trim() === '') {
|
|
37
|
+
errors.push({ code: 'NEED-001', message: `${f} is required.`, path: f });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (n.gate_checks === null || typeof n.gate_checks !== 'object') {
|
|
41
|
+
errors.push({ code: 'NEED-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
42
|
+
}
|
|
43
|
+
if (!('valid_to' in n) || !(typeof n.valid_to === 'string' || n.valid_to === null)) {
|
|
44
|
+
errors.push({ code: 'NEED-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
45
|
+
}
|
|
46
|
+
// stakeholder — required (NEED-001) + resolves to STAKEHOLDER (NEED-002).
|
|
47
|
+
const stakeholder = n.stakeholder;
|
|
48
|
+
if (typeof stakeholder !== 'string' || stakeholder.trim() === '') {
|
|
49
|
+
errors.push({ code: 'NEED-001', message: 'stakeholder is required.', path: 'stakeholder' });
|
|
50
|
+
}
|
|
51
|
+
else if (!isCanonicalIdOfType(stakeholder, 'STAKEHOLDER')) {
|
|
52
|
+
errors.push({ code: 'NEED-002', message: `stakeholder "${stakeholder}" must be a typed STAKEHOLDER id.`, path: 'stakeholder' });
|
|
53
|
+
}
|
|
54
|
+
else if (options.catalog && options.catalog.typeOf(stakeholder) === undefined) {
|
|
55
|
+
errors.push({ code: 'NEED-002', message: `stakeholder "${stakeholder}" does not resolve to an admitted artefact.`, path: 'stakeholder' });
|
|
56
|
+
}
|
|
57
|
+
// AGREE-001..003 — agreement axis (CONTRACT.md §6.3.1), optional; absent ⇒ agreed.
|
|
58
|
+
errors.push(...checkAgreement(n));
|
|
59
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
60
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RepoFinding, RepoModelInput } from './types.js';
|
|
2
|
+
/** Run the versioned-attribute sidecar checks (VERSIONED-001..005) over the
|
|
3
|
+
* loaded element set and append findings. Pure, deterministic order. */
|
|
4
|
+
export declare function checkVersionedAttributes(input: RepoModelInput, findings: RepoFinding[]): void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Versioned-attribute sidecar checks (CONTRACT.md §9) — `VERSIONED-001..005`.
|
|
2
|
+
//
|
|
3
|
+
// A sidecar is any doc under `canon/elements/**` shaped like one (`target` +
|
|
4
|
+
// `attribute_versions`, no `id` — same "no id -> not an element" convention
|
|
5
|
+
// `docId` already uses to skip these docs everywhere else in this file's
|
|
6
|
+
// sibling checks). Both halves of §9.3 live here:
|
|
7
|
+
// - VERSIONED-001 — a sidecar's `target` must resolve to an admitted
|
|
8
|
+
// primitive in this same model.
|
|
9
|
+
// - VERSIONED-002/003/005 — the sidecar's own array shape, delegated to
|
|
10
|
+
// the shared `../versioned-attribute` module (needs VERSIONED-005's
|
|
11
|
+
// bound: the target's own `valid_from`/`valid_to`, read from the
|
|
12
|
+
// resolved target doc).
|
|
13
|
+
// - VERSIONED-004 — a field declared `time_varying` present inline on its
|
|
14
|
+
// primitive, instead of only in the sidecar.
|
|
15
|
+
//
|
|
16
|
+
// `TIME_VARYING_FIELDS` is deliberately narrower than the full candidate
|
|
17
|
+
// list in CONTRACT.md §9.4: it only enforces fields already `time_varying`
|
|
18
|
+
// on methodology's *currently-merged* notation specs. `capability` covers
|
|
19
|
+
// `current_maturity`, `target_maturity`, `owner_role`, `target_date` per
|
|
20
|
+
// `05-capability-map.md` §13/§14. Applications-catalogue `maturity` (and its
|
|
21
|
+
// sibling `owner_role`/`vendor`) is on the same 2026-08-02 maturity ADR but
|
|
22
|
+
// is not enforced here yet — it lives on the `applications[]` list inside a
|
|
23
|
+
// catalogue view document, not on a standalone element primitive, and this
|
|
24
|
+
// check only walks `elements`.
|
|
25
|
+
import { parseSidecar, validateSidecar } from '../versioned-attribute/index.js';
|
|
26
|
+
import { docId } from './validate-repo.js';
|
|
27
|
+
const PScope = 'repo';
|
|
28
|
+
const TIME_VARYING_FIELDS = {
|
|
29
|
+
capability: ['current_maturity', 'target_maturity', 'owner_role', 'target_date'],
|
|
30
|
+
};
|
|
31
|
+
function readString(data, key) {
|
|
32
|
+
const v = data[key];
|
|
33
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
34
|
+
}
|
|
35
|
+
/** VERSIONED-004 — a time_varying field present inline on its primitive. */
|
|
36
|
+
function checkInlineTimeVarying(elements, findings) {
|
|
37
|
+
for (const doc of elements) {
|
|
38
|
+
if (!doc.data)
|
|
39
|
+
continue;
|
|
40
|
+
const id = docId(doc);
|
|
41
|
+
if (!id)
|
|
42
|
+
continue; // sidecars carry no id — not a primitive
|
|
43
|
+
const notation = doc.data['notation'];
|
|
44
|
+
if (typeof notation !== 'string')
|
|
45
|
+
continue;
|
|
46
|
+
const fields = TIME_VARYING_FIELDS[notation];
|
|
47
|
+
if (!fields)
|
|
48
|
+
continue;
|
|
49
|
+
for (const field of fields) {
|
|
50
|
+
if (doc.data[field] !== undefined) {
|
|
51
|
+
findings.push({
|
|
52
|
+
scope: PScope,
|
|
53
|
+
id,
|
|
54
|
+
ruleId: 'VERSIONED-004',
|
|
55
|
+
message: `VERSIONED-004: '${id}' declares '${field}' inline; it is time_varying and belongs in ` +
|
|
56
|
+
`the sidecar '${id}.history.yaml' (CONTRACT.md §9), not on the primitive.`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** VERSIONED-001/002/003/005 — every sidecar doc found under `canon/elements/**`. */
|
|
63
|
+
function checkSidecars(elements, findings) {
|
|
64
|
+
const elementById = new Map();
|
|
65
|
+
for (const doc of elements) {
|
|
66
|
+
const id = docId(doc);
|
|
67
|
+
if (id)
|
|
68
|
+
elementById.set(id, doc);
|
|
69
|
+
}
|
|
70
|
+
for (const doc of elements) {
|
|
71
|
+
if (!doc.data || docId(doc))
|
|
72
|
+
continue; // sidecars have no id
|
|
73
|
+
const sidecar = parseSidecar(doc.data);
|
|
74
|
+
if (!sidecar)
|
|
75
|
+
continue; // not shaped like a sidecar — ignore, same as docId's element/non-element split
|
|
76
|
+
const targetDoc = elementById.get(sidecar.target);
|
|
77
|
+
if (!targetDoc) {
|
|
78
|
+
findings.push({
|
|
79
|
+
scope: PScope,
|
|
80
|
+
id: sidecar.target,
|
|
81
|
+
ruleId: 'VERSIONED-001',
|
|
82
|
+
message: `VERSIONED-001: sidecar '${doc.path}' target '${sidecar.target}' does not resolve to an admitted primitive.`,
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const targetValidFrom = targetDoc.data ? readString(targetDoc.data, 'valid_from') : undefined;
|
|
87
|
+
const validToRaw = targetDoc.data?.['valid_to'];
|
|
88
|
+
const targetValidTo = validToRaw === null ? null : typeof validToRaw === 'string' ? validToRaw : undefined;
|
|
89
|
+
for (const f of validateSidecar(sidecar, targetValidFrom, targetValidTo)) {
|
|
90
|
+
findings.push({
|
|
91
|
+
scope: PScope,
|
|
92
|
+
id: sidecar.target,
|
|
93
|
+
ruleId: f.code,
|
|
94
|
+
severity: f.severity,
|
|
95
|
+
message: `${f.code}: sidecar '${doc.path}' ${f.message}`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Run the versioned-attribute sidecar checks (VERSIONED-001..005) over the
|
|
101
|
+
* loaded element set and append findings. Pure, deterministic order. */
|
|
102
|
+
export function checkVersionedAttributes(input, findings) {
|
|
103
|
+
checkInlineTimeVarying(input.elements, findings);
|
|
104
|
+
checkSidecars(input.elements, findings);
|
|
105
|
+
}
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// deferred per the ADR.
|
|
29
29
|
import { checkStrategyChainSemantics } from './check-strategy-chain.js';
|
|
30
30
|
import { checkElementHygiene } from './check-element-hygiene.js';
|
|
31
|
+
import { checkVersionedAttributes } from './check-versioned-attributes.js';
|
|
31
32
|
const PScope = 'repo';
|
|
32
33
|
/** Statuses that, per lint.py's policy check, require an owner. */
|
|
33
34
|
const OWNER_REQUIRED_STATUSES = new Set(['Active', 'Production']);
|
|
@@ -412,5 +413,7 @@ export function validateRepoModel(input) {
|
|
|
412
413
|
// Phase 8 — standalone-element envelope rules ported from DSM's Go
|
|
413
414
|
// Validate*Element functions (GOAL-ELEM-001..003, ACTION-001/002/005).
|
|
414
415
|
checkElementHygiene(input, findings);
|
|
416
|
+
// Phase 9 — versioned-attribute sidecar rules (CONTRACT.md §9, VERSIONED-001..005).
|
|
417
|
+
checkVersionedAttributes(input, findings);
|
|
415
418
|
return findings;
|
|
416
419
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Requirement, RequirementSeverity, GateChecks } from './types.js';
|
|
2
|
-
export { REQUIREMENT_DERIVED_FROM_TYPES } from './types.js';
|
|
1
|
+
export type { Requirement, RequirementSeverity, RequirementLevel, RequirementKind, GateChecks } from './types.js';
|
|
2
|
+
export { REQUIREMENT_DERIVED_FROM_TYPES, REQUIREMENT_LEVELS, REQUIREMENT_KINDS } from './types.js';
|
|
3
3
|
export { validateRequirement } from './validate.js';
|
|
4
4
|
export type { RequirementValidateOptions } from './validate.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { REQUIREMENT_DERIVED_FROM_TYPES } from './types.js';
|
|
1
|
+
export { REQUIREMENT_DERIVED_FROM_TYPES, REQUIREMENT_LEVELS, REQUIREMENT_KINDS } from './types.js';
|
|
2
2
|
export { validateRequirement } from './validate.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgreementValue } from '../agreement.js';
|
|
1
2
|
export type RequirementSeverity = 'high' | 'medium' | 'low';
|
|
2
3
|
/** Standard canon admission gate checks (CONTRACT.md §6). */
|
|
3
4
|
export interface GateChecks {
|
|
@@ -7,6 +8,12 @@ export interface GateChecks {
|
|
|
7
8
|
}
|
|
8
9
|
/** TYPEs a requirement may be derived from (15-requirement.md §2, REQ-003). */
|
|
9
10
|
export declare const REQUIREMENT_DERIVED_FROM_TYPES: readonly ["LAW", "REGULATION", "POLICY", "INTERNAL_STANDARD"];
|
|
11
|
+
/** ISO/IEC/IEEE 29148 specification-tier ladder (15-requirement.md §2.5, REQ-005). */
|
|
12
|
+
export type RequirementLevel = 'stakeholder' | 'system' | 'software';
|
|
13
|
+
export declare const REQUIREMENT_LEVELS: readonly RequirementLevel[];
|
|
14
|
+
/** Functional vs quality classification (15-requirement.md §2.6, REQ-006). */
|
|
15
|
+
export type RequirementKind = 'functional' | 'quality';
|
|
16
|
+
export declare const REQUIREMENT_KINDS: readonly RequirementKind[];
|
|
10
17
|
export interface Requirement {
|
|
11
18
|
notation: 'requirement';
|
|
12
19
|
id: string;
|
|
@@ -14,8 +21,19 @@ export interface Requirement {
|
|
|
14
21
|
description: string;
|
|
15
22
|
/** Organisation-defined priority. */
|
|
16
23
|
severity?: RequirementSeverity;
|
|
24
|
+
/** ISO/IEC/IEEE 29148 specification tier (§2.5, REQ-005). */
|
|
25
|
+
level?: RequirementLevel;
|
|
26
|
+
/** Functional vs quality classification (§2.6, REQ-006). */
|
|
27
|
+
kind?: RequirementKind;
|
|
17
28
|
/** Typed IDs of the codex source documents this requirement is drawn from. */
|
|
18
29
|
derived_from?: string[];
|
|
30
|
+
/** Typed ID of the upstream NEED this requirement traces to (§2.7, REQ-SERVES-001). Optional, singular. */
|
|
31
|
+
serves?: string;
|
|
32
|
+
/** `draft` \| `agreed` \| `disputed`. Absent ⇒ `agreed` (back-compat). */
|
|
33
|
+
agreement?: AgreementValue;
|
|
34
|
+
/** Required whenever `agreement` is present (AGREE-003). Written only by a human when `agreement: agreed` (AGREE-002). */
|
|
35
|
+
agreed_by?: string;
|
|
36
|
+
agreed_at?: string;
|
|
19
37
|
zone: 'canon';
|
|
20
38
|
admitted_at: string;
|
|
21
39
|
admitted_by: string;
|
|
@@ -2,3 +2,5 @@
|
|
|
2
2
|
// Schema: methodology notations/elements/15-requirement.md §2.
|
|
3
3
|
/** TYPEs a requirement may be derived from (15-requirement.md §2, REQ-003). */
|
|
4
4
|
export const REQUIREMENT_DERIVED_FROM_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'];
|
|
5
|
+
export const REQUIREMENT_LEVELS = ['stakeholder', 'system', 'software'];
|
|
6
|
+
export const REQUIREMENT_KINDS = ['functional', 'quality'];
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// supplied; the TYPE check (REQ-003) is derivable from the id prefix and runs
|
|
10
10
|
// either way.
|
|
11
11
|
import { typeOfId, isCanonicalIdOfType } from '../typed-id.js';
|
|
12
|
-
import {
|
|
12
|
+
import { checkAgreement } from '../agreement.js';
|
|
13
|
+
import { REQUIREMENT_DERIVED_FROM_TYPES, REQUIREMENT_LEVELS, REQUIREMENT_KINDS } from './types.js';
|
|
13
14
|
const REQUIRED_FIELDS = [
|
|
14
15
|
'notation', 'name', 'description', 'zone', 'admitted_at', 'admitted_by', 'gate_checks', 'valid_from',
|
|
15
16
|
];
|
|
@@ -77,5 +78,25 @@ export function validateRequirement(input, options = {}) {
|
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
80
|
}
|
|
81
|
+
// REQ-005 — level closed vocabulary (optional; only checked when present).
|
|
82
|
+
if (r.level !== undefined && !REQUIREMENT_LEVELS.includes(r.level)) {
|
|
83
|
+
errors.push({ code: 'REQ-005', message: `level "${String(r.level)}" must be one of ${REQUIREMENT_LEVELS.join(', ')}.`, path: 'level' });
|
|
84
|
+
}
|
|
85
|
+
// REQ-006 — kind closed vocabulary (optional; only checked when present).
|
|
86
|
+
if (r.kind !== undefined && !REQUIREMENT_KINDS.includes(r.kind)) {
|
|
87
|
+
errors.push({ code: 'REQ-006', message: `kind "${String(r.kind)}" must be one of ${REQUIREMENT_KINDS.join(', ')}.`, path: 'kind' });
|
|
88
|
+
}
|
|
89
|
+
// REQ-SERVES-001 — serves resolves to an admitted NEED (optional, singular).
|
|
90
|
+
if (r.serves !== undefined) {
|
|
91
|
+
const serves = r.serves;
|
|
92
|
+
if (typeof serves !== 'string' || serves.trim() === '' || typeOfId(serves) !== 'NEED') {
|
|
93
|
+
errors.push({ code: 'REQ-SERVES-001', message: `serves "${String(serves)}" must be a typed NEED id.`, path: 'serves' });
|
|
94
|
+
}
|
|
95
|
+
else if (options.catalog && options.catalog.typeOf(serves) === undefined) {
|
|
96
|
+
errors.push({ code: 'REQ-SERVES-001', message: `serves "${serves}" does not resolve to an admitted NEED.`, path: 'serves' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// AGREE-001..003 — agreement axis (CONTRACT.md §6.3.1), optional; absent ⇒ agreed.
|
|
100
|
+
errors.push(...checkAgreement(r));
|
|
80
101
|
return { valid: errors.length === 0, errors, warnings };
|
|
81
102
|
}
|