@transitrix/diagrams 1.8.20 → 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.d.ts +1 -1
- package/dist/compliance/html.js +15 -7
- 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 +19 -7
- 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
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* historically but the layout code only uses IDs as opaque keys, so
|
|
18
18
|
* string IDs work fine.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* This lives at the input boundary so the library's
|
|
21
21
|
* internal types (currently using DSM-API form — numeric IDs and
|
|
22
22
|
* singular cross-refs) can stay where they are. A follow-up task can
|
|
23
23
|
* unify the internal types with the canonical form across all sister
|
|
@@ -354,7 +354,7 @@ export interface CanonicalFGAResult extends ValidationResult {
|
|
|
354
354
|
* FGA-001..011). The resulting internal `FGCADoc` carries `activity.goal_id`,
|
|
355
355
|
* which is exactly what the renderer needs to draw goal → activity edges —
|
|
356
356
|
* the field whose absence produced the "FGA nodes render, no edges" bug
|
|
357
|
-
* (transitrix/methodology#65
|
|
357
|
+
* (transitrix/methodology#65).
|
|
358
358
|
*
|
|
359
359
|
* Lives beside `parseCanonicalFGCA` (rather than inline in the extension) so
|
|
360
360
|
* the canonical FGA path is unit-testable and the library owns the single
|
|
@@ -49,7 +49,7 @@ export interface FGCAPreviewLayoutOptions {
|
|
|
49
49
|
colGap?: number;
|
|
50
50
|
/** Vertical gap (px) between stacked nodes within a column. Default matches the historical hardcoded value. */
|
|
51
51
|
rowGap?: number;
|
|
52
|
-
/** Trim to a level cap or a single root goal
|
|
52
|
+
/** Trim to a level cap or a single root goal. Defaults to 'all'. */
|
|
53
53
|
scope?: Scope;
|
|
54
54
|
/** Entity node width (px). Default {@link FGCA_NODE_W}. */
|
|
55
55
|
nodeWidth?: number;
|
package/src/goals/layout.ts
CHANGED
|
@@ -8,7 +8,7 @@ const DEFAULT_RANK_SEP = 80;
|
|
|
8
8
|
const DEFAULT_NODE_SEP = 40;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Trims a flat goal list to a scope
|
|
11
|
+
* Trims a flat goal list to a scope.
|
|
12
12
|
* - 'level' → goals with `level <= maxLevel`.
|
|
13
13
|
* - 'root' → the root goal (matched by string id) plus all its descendants
|
|
14
14
|
* via `parent_id`; an empty list when the root is absent.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* mapped to internal sequential numbers, `parent: GOAL-X` becomes
|
|
16
16
|
* `parent_id: <numeric>`, `parent_id: 0` denotes a root.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* This lives at the input boundary so the library's
|
|
19
19
|
* internal types (numeric IDs) stay where they are. Convergence on
|
|
20
20
|
* canonical typed-string IDs across all sister modules is a follow-up
|
|
21
21
|
* task.
|
package/src/goals/types.ts
CHANGED
|
@@ -60,7 +60,7 @@ export interface LayoutOptions {
|
|
|
60
60
|
nodeSep?: number;
|
|
61
61
|
hideCollapsed?: number[];
|
|
62
62
|
viewDepth?: number | null;
|
|
63
|
-
/** Trim the tree to a level cap or a subtree root
|
|
63
|
+
/** Trim the tree to a level cap or a subtree root. Defaults to 'all'. */
|
|
64
64
|
scope?: Scope;
|
|
65
65
|
}
|
|
66
66
|
|
package/src/goals/validate.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function validateGoalTree(input: unknown): ValidationResult {
|
|
|
21
21
|
const tree = raw as unknown as GoalTree;
|
|
22
22
|
|
|
23
23
|
// GOALS-013 — goal_types[].level values must be contiguous starting from 0.
|
|
24
|
-
// Per the N+1 rule
|
|
24
|
+
// Per the N+1 rule, the level set must
|
|
25
25
|
// be exactly {0, 1, …, N}; a gap breaks the parent→child level invariant
|
|
26
26
|
// and means GOALS-012 cannot be enforced consistently.
|
|
27
27
|
const levels = tree.goal_types
|
|
@@ -96,7 +96,7 @@ export function validateGoalTree(input: unknown): ValidationResult {
|
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
98
|
// GOALS-012 — parent must be exactly one level above the child (N+1
|
|
99
|
-
// hierarchy
|
|
99
|
+
// hierarchy). Only enforced when the
|
|
100
100
|
// parent is resolvable; a missing parent is covered by BROKEN_PARENT_REF.
|
|
101
101
|
if (g.parent_id !== 0) {
|
|
102
102
|
const parentLevel = levelById.get(g.parent_id);
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
|
|
11
|
+
// The administrative envelope (§16.1) — field names CONTRACT.md already
|
|
12
|
+
// defines by name (§6 / §6.1 / §6.2 / §6.3 when present / §7). One list,
|
|
13
|
+
// defined once; a spec revision that adds an envelope field extends this
|
|
14
|
+
// list in the same revision rather than inventing a second mechanism.
|
|
15
|
+
export const ENVELOPE_FIELDS = [
|
|
16
|
+
'zone', 'admitted_at', 'admitted_by', 'gate_checks', 'derived_from',
|
|
17
|
+
'admission_state', 'proposed_at', 'proposed_by', 'owner_to_confirm',
|
|
18
|
+
'rejected_at', 'rejected_by', 'rejection_reason',
|
|
19
|
+
'reviewer_authority',
|
|
20
|
+
'agreement', 'agreed_by', 'agreed_at',
|
|
21
|
+
'valid_from', 'valid_to',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function stripComment(line: string): string {
|
|
25
|
+
let inQuotes = false;
|
|
26
|
+
for (let i = 0; i < line.length; i++) {
|
|
27
|
+
const ch = line[i];
|
|
28
|
+
if (ch === '"') inQuotes = !inQuotes;
|
|
29
|
+
else if (ch === '#' && !inQuotes) return line.slice(0, i);
|
|
30
|
+
}
|
|
31
|
+
return line;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Every non-blank, non-comment line of the file, except those belonging to
|
|
35
|
+
* an `ENVELOPE_FIELDS` block (the key line and its more-indented
|
|
36
|
+
* continuation lines), whitespace-normalised and sorted —
|
|
37
|
+
* order-independent, formatting-independent, comment-independent. Not a
|
|
38
|
+
* general YAML parser — a flat, line-oriented pass over the already-known
|
|
39
|
+
* envelope field names, the same posture the methodology reference script
|
|
40
|
+
* takes (its own header comment cites `baseline-manifest.mjs` /
|
|
41
|
+
* `check-agreement.mjs` for precedent). */
|
|
42
|
+
export function statementLines(text: string): string[] {
|
|
43
|
+
const kept: string[] = [];
|
|
44
|
+
let skipIndent: number | null = null;
|
|
45
|
+
for (const raw of text.split('\n')) {
|
|
46
|
+
const stripped = stripComment(raw).replace(/\s+$/, '');
|
|
47
|
+
if (!stripped.trim()) continue;
|
|
48
|
+
const indent = stripped.match(/^ */)?.[0].length ?? 0;
|
|
49
|
+
if (skipIndent !== null) {
|
|
50
|
+
if (indent > skipIndent) continue;
|
|
51
|
+
skipIndent = null;
|
|
52
|
+
}
|
|
53
|
+
const keyMatch = stripped.match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*):/);
|
|
54
|
+
if (keyMatch && keyMatch[1].length === 0 && ENVELOPE_FIELDS.includes(keyMatch[2])) {
|
|
55
|
+
skipIndent = indent;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
kept.push(stripped.trim().replace(/\s+/g, ' '));
|
|
59
|
+
}
|
|
60
|
+
return kept.sort();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sameLines(a: string[], b: string[]): boolean {
|
|
64
|
+
return a.length === b.length && a.every((line, i) => line === b[i]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface LineEdit {
|
|
68
|
+
from: string;
|
|
69
|
+
to: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A parsed `migrations/<slug>/TRANSFORM.yaml` manifest (§16.3). */
|
|
73
|
+
export interface MigrationManifest {
|
|
74
|
+
slug: string;
|
|
75
|
+
mechanical: boolean;
|
|
76
|
+
appliesTo: string[];
|
|
77
|
+
lineEdits: LineEdit[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function unquote(s: string): string {
|
|
81
|
+
const m = s.match(/^"(.*)"$/);
|
|
82
|
+
return m ? m[1] : s;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeEditLine(s: string): string {
|
|
86
|
+
return unquote(s.trim()).trim().replace(/\s+/g, ' ');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Minimal parse of the fixed `TRANSFORM.yaml` shape — `mechanical: true`,
|
|
90
|
+
* `applies_to: [exact relative paths]`, `line_edits: [{from, to}]`. Not a
|
|
91
|
+
* general YAML parser, same posture as {@link statementLines}. */
|
|
92
|
+
export function parseMigrationManifest(text: string): Omit<MigrationManifest, 'slug'> {
|
|
93
|
+
const manifest: Omit<MigrationManifest, 'slug'> = { mechanical: false, appliesTo: [], lineEdits: [] };
|
|
94
|
+
let section: 'applies_to' | 'line_edits' | null = null;
|
|
95
|
+
let current: LineEdit | null = null;
|
|
96
|
+
for (const raw of text.split('\n')) {
|
|
97
|
+
const line = raw.replace(/\s+$/, '');
|
|
98
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
99
|
+
|
|
100
|
+
const topMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
101
|
+
if (topMatch) {
|
|
102
|
+
const [, key, val] = topMatch;
|
|
103
|
+
if (key === 'mechanical') {
|
|
104
|
+
manifest.mechanical = val.trim() === 'true';
|
|
105
|
+
section = null;
|
|
106
|
+
} else if (key === 'applies_to') {
|
|
107
|
+
section = 'applies_to';
|
|
108
|
+
} else if (key === 'line_edits') {
|
|
109
|
+
section = 'line_edits';
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (section === 'applies_to') {
|
|
115
|
+
const m = line.match(/^\s*-\s*(.+)$/);
|
|
116
|
+
if (m) manifest.appliesTo.push(unquote(m[1].trim()));
|
|
117
|
+
} else if (section === 'line_edits') {
|
|
118
|
+
const fromMatch = line.match(/^\s*-\s*from:\s*(.+)$/);
|
|
119
|
+
if (fromMatch) {
|
|
120
|
+
current = { from: normalizeEditLine(fromMatch[1]), to: '' };
|
|
121
|
+
manifest.lineEdits.push(current);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const toMatch = line.match(/^\s*to:\s*(.+)$/);
|
|
125
|
+
if (toMatch && current) current.to = normalizeEditLine(toMatch[1]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return manifest;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Replay a manifest's declared `lineEdits` against the before-state and
|
|
132
|
+
* check the result matches the after-state exactly — the independent
|
|
133
|
+
* verification §16.3 requires: the tool's `mechanical: true` declaration is
|
|
134
|
+
* never trusted on its own; only a replay that reproduces the after-state
|
|
135
|
+
* suppresses suspicion. */
|
|
136
|
+
function replayExplains(beforeLines: string[], afterLines: string[], lineEdits: LineEdit[]): boolean {
|
|
137
|
+
const working = [...beforeLines];
|
|
138
|
+
for (const edit of lineEdits) {
|
|
139
|
+
const idx = working.indexOf(edit.from);
|
|
140
|
+
if (idx === -1) return false;
|
|
141
|
+
working.splice(idx, 1, edit.to);
|
|
142
|
+
}
|
|
143
|
+
return sameLines([...working].sort(), [...afterLines].sort());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface SuspicionResult {
|
|
147
|
+
suspicious: boolean;
|
|
148
|
+
/** True when a manifest declared `mechanical: true` for this target but
|
|
149
|
+
* its replay did not fully explain the change — the hatch does not
|
|
150
|
+
* self-grant; suspicion stands regardless of the flag (§16.3). */
|
|
151
|
+
hatchRefused?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* §16.2: *Suspicious* ⟺ the target's content identity at the anchor commit
|
|
156
|
+
* differs from its content identity now, **and** §16.3's hatch does not
|
|
157
|
+
* explain the difference.
|
|
158
|
+
*
|
|
159
|
+
* `beforeText` / `afterText` are already-fetched (git IO lives in the
|
|
160
|
+
* caller); `undefined` for either (no anchor, or the target no longer
|
|
161
|
+
* exists) means nothing to compare — not suspicious, per the reference
|
|
162
|
+
* script's `target-absent-at-anchor` / `target-absent-now` cases.
|
|
163
|
+
*/
|
|
164
|
+
export function checkSuspicion(
|
|
165
|
+
beforeText: string | undefined,
|
|
166
|
+
afterText: string | undefined,
|
|
167
|
+
applicableManifests: Array<Omit<MigrationManifest, 'slug'>> = [],
|
|
168
|
+
): SuspicionResult {
|
|
169
|
+
if (beforeText === undefined || afterText === undefined) return { suspicious: false };
|
|
170
|
+
|
|
171
|
+
const beforeLines = statementLines(beforeText);
|
|
172
|
+
const afterLines = statementLines(afterText);
|
|
173
|
+
if (sameLines(beforeLines, afterLines)) return { suspicious: false };
|
|
174
|
+
|
|
175
|
+
for (const manifest of applicableManifests) {
|
|
176
|
+
if (replayExplains(beforeLines, afterLines, manifest.lineEdits)) {
|
|
177
|
+
return { suspicious: false };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { suspicious: true, hatchRefused: applicableManifests.some((m) => m.mechanical) };
|
|
181
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// METRIC — motivation-layer managed indicator (no ArchiMate counterpart).
|
|
2
|
+
// Schema: methodology notations/ELEMENT_PRIMITIVES.md §7.27.
|
|
3
|
+
|
|
4
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
5
|
+
|
|
6
|
+
/** How to read movement relative to `target` (§7.27). */
|
|
7
|
+
export type MetricDirectionOfGood = 'higher_is_better' | 'lower_is_better' | 'on_target';
|
|
8
|
+
|
|
9
|
+
export const METRIC_DIRECTIONS_OF_GOOD: readonly MetricDirectionOfGood[] = [
|
|
10
|
+
'higher_is_better',
|
|
11
|
+
'lower_is_better',
|
|
12
|
+
'on_target',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export interface Metric {
|
|
16
|
+
notation: 'metric';
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
/** Typed IDs of what this metric tracks — GOAL, CAPABILITY, or PROCESS. Non-empty. */
|
|
20
|
+
measures: string[];
|
|
21
|
+
/** Free-form unit the value and `target` are expressed in (percent, days, …). */
|
|
22
|
+
unit: string;
|
|
23
|
+
target: number;
|
|
24
|
+
direction_of_good: MetricDirectionOfGood;
|
|
25
|
+
/** ROLE-… accountable for tracking this metric and acting on it. */
|
|
26
|
+
owner_role: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
|
|
29
|
+
// Admission record (CONTRACT.md §6).
|
|
30
|
+
zone: 'canon';
|
|
31
|
+
admitted_at: string;
|
|
32
|
+
admitted_by: string;
|
|
33
|
+
gate_checks: GateChecks;
|
|
34
|
+
|
|
35
|
+
// Primitive lifecycle (CONTRACT.md §7).
|
|
36
|
+
valid_from: string;
|
|
37
|
+
valid_to: string | null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
|
|
16
|
+
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
17
|
+
import { typeOfId, isCanonicalIdOfType, type CanonCatalog } from '../typed-id.js';
|
|
18
|
+
import { METRIC_DIRECTIONS_OF_GOOD } from './types.js';
|
|
19
|
+
|
|
20
|
+
export interface MetricValidateOptions {
|
|
21
|
+
/** When provided, METRIC-002/004 enforce artefact existence for cross-refs. */
|
|
22
|
+
catalog?: CanonCatalog;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MEASURES_TYPES = ['GOAL', 'CAPABILITY', 'PROCESS'] as const;
|
|
26
|
+
|
|
27
|
+
export function validateMetric(input: unknown, options: MetricValidateOptions = {}): ValidationResult {
|
|
28
|
+
const errors: ValidationError[] = [];
|
|
29
|
+
const warnings: ValidationWarning[] = [];
|
|
30
|
+
|
|
31
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
32
|
+
errors.push({ code: 'METRIC-001', message: 'Metric must be a YAML mapping.' });
|
|
33
|
+
return { valid: false, errors, warnings };
|
|
34
|
+
}
|
|
35
|
+
const m = input as Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
// METRIC-001 — id grammar + notation tag + envelope + plain per-type fields.
|
|
38
|
+
if (!isCanonicalIdOfType(m.id, 'METRIC')) {
|
|
39
|
+
errors.push({ code: 'METRIC-001', message: `id "${String(m.id)}" must match METRIC-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
40
|
+
}
|
|
41
|
+
if (m.notation !== 'metric') {
|
|
42
|
+
errors.push({ code: 'METRIC-001', message: 'notation must be the fixed value "metric".', path: 'notation' });
|
|
43
|
+
}
|
|
44
|
+
if (m.zone !== 'canon') {
|
|
45
|
+
errors.push({ code: 'METRIC-001', message: 'zone is required and must be "canon".', path: 'zone' });
|
|
46
|
+
}
|
|
47
|
+
for (const f of ['name', 'admitted_at', 'admitted_by', 'valid_from', 'unit', 'owner_role'] as const) {
|
|
48
|
+
if (typeof m[f] !== 'string' || (m[f] as string).trim() === '') {
|
|
49
|
+
errors.push({ code: 'METRIC-001', message: `${f} is required.`, path: f });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (m.gate_checks === null || typeof m.gate_checks !== 'object') {
|
|
53
|
+
errors.push({ code: 'METRIC-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
54
|
+
}
|
|
55
|
+
if (!('valid_to' in m) || !(typeof m.valid_to === 'string' || m.valid_to === null)) {
|
|
56
|
+
errors.push({ code: 'METRIC-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
57
|
+
}
|
|
58
|
+
if (typeof m.target !== 'number' || Number.isNaN(m.target)) {
|
|
59
|
+
errors.push({ code: 'METRIC-001', message: 'target is required and must be a number.', path: 'target' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// direction_of_good — required (METRIC-001) + enum (METRIC-003).
|
|
63
|
+
const dir = m.direction_of_good;
|
|
64
|
+
if (dir === undefined || dir === null || dir === '') {
|
|
65
|
+
errors.push({ code: 'METRIC-001', message: 'direction_of_good is required.', path: 'direction_of_good' });
|
|
66
|
+
} else if (!(METRIC_DIRECTIONS_OF_GOOD as readonly string[]).includes(dir as string)) {
|
|
67
|
+
errors.push({
|
|
68
|
+
code: 'METRIC-003',
|
|
69
|
+
message: `direction_of_good "${String(dir)}" must be one of ${METRIC_DIRECTIONS_OF_GOOD.join(', ')}.`,
|
|
70
|
+
path: 'direction_of_good',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// measures — required non-empty list (METRIC-001), each resolves to GOAL/CAPABILITY/PROCESS (METRIC-002).
|
|
75
|
+
const measures = m.measures;
|
|
76
|
+
if (!Array.isArray(measures) || measures.length === 0) {
|
|
77
|
+
errors.push({ code: 'METRIC-001', message: 'measures is required and must be a non-empty list of typed IDs.', path: 'measures' });
|
|
78
|
+
} else {
|
|
79
|
+
measures.forEach((ref, i) => {
|
|
80
|
+
const type = typeOfId(ref);
|
|
81
|
+
if (!type || !(MEASURES_TYPES as readonly string[]).includes(type as typeof MEASURES_TYPES[number])) {
|
|
82
|
+
errors.push({ code: 'METRIC-002', message: `measures[${i}] "${String(ref)}" must resolve to a GOAL, CAPABILITY, or PROCESS.`, path: `measures[${i}]` });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (options.catalog && options.catalog.typeOf(ref as string) === undefined) {
|
|
86
|
+
errors.push({ code: 'METRIC-002', message: `measures[${i}] "${String(ref)}" does not resolve to an admitted artefact.`, path: `measures[${i}]` });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// owner_role — must be a typed ROLE id (METRIC-004), and resolve when a catalog is supplied.
|
|
92
|
+
const ownerRole = m.owner_role;
|
|
93
|
+
if (typeof ownerRole === 'string' && ownerRole.trim() !== '') {
|
|
94
|
+
if (!isCanonicalIdOfType(ownerRole, 'ROLE')) {
|
|
95
|
+
errors.push({ code: 'METRIC-004', message: `owner_role "${ownerRole}" must be a typed ROLE id.`, path: 'owner_role' });
|
|
96
|
+
} else if (options.catalog && options.catalog.typeOf(ownerRole) === undefined) {
|
|
97
|
+
errors.push({ code: 'METRIC-004', message: `owner_role "${ownerRole}" does not resolve to an admitted artefact.`, path: 'owner_role' });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
102
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// NEED — motivation-layer stakeholder/user need (no ArchiMate counterpart).
|
|
2
|
+
// Upstream of REQUIREMENT — schema: methodology notations/ELEMENT_PRIMITIVES.md §7.28.
|
|
3
|
+
|
|
4
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
5
|
+
import type { AgreementValue } from '../agreement.js';
|
|
6
|
+
|
|
7
|
+
export interface Need {
|
|
8
|
+
notation: 'need';
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
/** Typed ID of the STAKEHOLDER whose need this is. */
|
|
12
|
+
stakeholder: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
|
|
15
|
+
// Agreement axis (CONTRACT.md §6.3, when present) — independent of admission.
|
|
16
|
+
/** `draft` \| `agreed` \| `disputed`. Absent ⇒ `agreed` (back-compat). */
|
|
17
|
+
agreement?: AgreementValue;
|
|
18
|
+
/** Required whenever `agreement` is present (AGREE-003). Written only by a human when `agreement: agreed` (AGREE-002). */
|
|
19
|
+
agreed_by?: string;
|
|
20
|
+
agreed_at?: string;
|
|
21
|
+
|
|
22
|
+
// Admission record (CONTRACT.md §6).
|
|
23
|
+
zone: 'canon';
|
|
24
|
+
admitted_at: string;
|
|
25
|
+
admitted_by: string;
|
|
26
|
+
gate_checks: GateChecks;
|
|
27
|
+
|
|
28
|
+
// Primitive lifecycle (CONTRACT.md §7).
|
|
29
|
+
valid_from: string;
|
|
30
|
+
valid_to: string | null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
|
|
16
|
+
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
17
|
+
import { isCanonicalIdOfType, type CanonCatalog } from '../typed-id.js';
|
|
18
|
+
import { checkAgreement } from '../agreement.js';
|
|
19
|
+
|
|
20
|
+
export interface NeedValidateOptions {
|
|
21
|
+
/** When provided, NEED-002 enforces artefact existence for `stakeholder`. */
|
|
22
|
+
catalog?: CanonCatalog;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateNeed(input: unknown, options: NeedValidateOptions = {}): ValidationResult {
|
|
26
|
+
const errors: ValidationError[] = [];
|
|
27
|
+
const warnings: ValidationWarning[] = [];
|
|
28
|
+
|
|
29
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
30
|
+
errors.push({ code: 'NEED-001', message: 'Need must be a YAML mapping.' });
|
|
31
|
+
return { valid: false, errors, warnings };
|
|
32
|
+
}
|
|
33
|
+
const n = input as Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
// NEED-001 — id grammar + notation tag + envelope.
|
|
36
|
+
if (!isCanonicalIdOfType(n.id, 'NEED')) {
|
|
37
|
+
errors.push({ code: 'NEED-001', message: `id "${String(n.id)}" must match NEED-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
38
|
+
}
|
|
39
|
+
if (n.notation !== 'need') {
|
|
40
|
+
errors.push({ code: 'NEED-001', message: 'notation must be the fixed value "need".', path: 'notation' });
|
|
41
|
+
}
|
|
42
|
+
if (n.zone !== 'canon') {
|
|
43
|
+
errors.push({ code: 'NEED-001', message: 'zone is required and must be "canon".', path: 'zone' });
|
|
44
|
+
}
|
|
45
|
+
for (const f of ['name', 'admitted_at', 'admitted_by', 'valid_from'] as const) {
|
|
46
|
+
if (typeof n[f] !== 'string' || (n[f] as string).trim() === '') {
|
|
47
|
+
errors.push({ code: 'NEED-001', message: `${f} is required.`, path: f });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (n.gate_checks === null || typeof n.gate_checks !== 'object') {
|
|
51
|
+
errors.push({ code: 'NEED-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
52
|
+
}
|
|
53
|
+
if (!('valid_to' in n) || !(typeof n.valid_to === 'string' || n.valid_to === null)) {
|
|
54
|
+
errors.push({ code: 'NEED-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// stakeholder — required (NEED-001) + resolves to STAKEHOLDER (NEED-002).
|
|
58
|
+
const stakeholder = n.stakeholder;
|
|
59
|
+
if (typeof stakeholder !== 'string' || stakeholder.trim() === '') {
|
|
60
|
+
errors.push({ code: 'NEED-001', message: 'stakeholder is required.', path: 'stakeholder' });
|
|
61
|
+
} else if (!isCanonicalIdOfType(stakeholder, 'STAKEHOLDER')) {
|
|
62
|
+
errors.push({ code: 'NEED-002', message: `stakeholder "${stakeholder}" must be a typed STAKEHOLDER id.`, path: 'stakeholder' });
|
|
63
|
+
} else if (options.catalog && options.catalog.typeOf(stakeholder) === undefined) {
|
|
64
|
+
errors.push({ code: 'NEED-002', message: `stakeholder "${stakeholder}" does not resolve to an admitted artefact.`, path: 'stakeholder' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// AGREE-001..003 — agreement axis (CONTRACT.md §6.3.1), optional; absent ⇒ agreed.
|
|
68
|
+
errors.push(...checkAgreement(n));
|
|
69
|
+
|
|
70
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
71
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
|
|
26
|
+
import { parseSidecar, validateSidecar } from '../versioned-attribute/index.js';
|
|
27
|
+
import { docId } from './validate-repo.js';
|
|
28
|
+
import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
|
|
29
|
+
|
|
30
|
+
const PScope: RepoFinding['scope'] = 'repo';
|
|
31
|
+
|
|
32
|
+
const TIME_VARYING_FIELDS: Record<string, string[]> = {
|
|
33
|
+
capability: ['current_maturity', 'target_maturity', 'owner_role', 'target_date'],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function readString(data: Record<string, unknown>, key: string): string | undefined {
|
|
37
|
+
const v = data[key];
|
|
38
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** VERSIONED-004 — a time_varying field present inline on its primitive. */
|
|
42
|
+
function checkInlineTimeVarying(elements: RepoDoc[], findings: RepoFinding[]): void {
|
|
43
|
+
for (const doc of elements) {
|
|
44
|
+
if (!doc.data) continue;
|
|
45
|
+
const id = docId(doc);
|
|
46
|
+
if (!id) continue; // sidecars carry no id — not a primitive
|
|
47
|
+
const notation = doc.data['notation'];
|
|
48
|
+
if (typeof notation !== 'string') continue;
|
|
49
|
+
const fields = TIME_VARYING_FIELDS[notation];
|
|
50
|
+
if (!fields) continue;
|
|
51
|
+
|
|
52
|
+
for (const field of fields) {
|
|
53
|
+
if (doc.data[field] !== undefined) {
|
|
54
|
+
findings.push({
|
|
55
|
+
scope: PScope,
|
|
56
|
+
id,
|
|
57
|
+
ruleId: 'VERSIONED-004',
|
|
58
|
+
message:
|
|
59
|
+
`VERSIONED-004: '${id}' declares '${field}' inline; it is time_varying and belongs in ` +
|
|
60
|
+
`the sidecar '${id}.history.yaml' (CONTRACT.md §9), not on the primitive.`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** VERSIONED-001/002/003/005 — every sidecar doc found under `canon/elements/**`. */
|
|
68
|
+
function checkSidecars(elements: RepoDoc[], findings: RepoFinding[]): void {
|
|
69
|
+
const elementById = new Map<string, RepoDoc>();
|
|
70
|
+
for (const doc of elements) {
|
|
71
|
+
const id = docId(doc);
|
|
72
|
+
if (id) elementById.set(id, doc);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const doc of elements) {
|
|
76
|
+
if (!doc.data || docId(doc)) continue; // sidecars have no id
|
|
77
|
+
const sidecar = parseSidecar(doc.data);
|
|
78
|
+
if (!sidecar) continue; // not shaped like a sidecar — ignore, same as docId's element/non-element split
|
|
79
|
+
|
|
80
|
+
const targetDoc = elementById.get(sidecar.target);
|
|
81
|
+
if (!targetDoc) {
|
|
82
|
+
findings.push({
|
|
83
|
+
scope: PScope,
|
|
84
|
+
id: sidecar.target,
|
|
85
|
+
ruleId: 'VERSIONED-001',
|
|
86
|
+
message: `VERSIONED-001: sidecar '${doc.path}' target '${sidecar.target}' does not resolve to an admitted primitive.`,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const targetValidFrom = targetDoc.data ? readString(targetDoc.data, 'valid_from') : undefined;
|
|
92
|
+
const validToRaw = targetDoc.data?.['valid_to'];
|
|
93
|
+
const targetValidTo = validToRaw === null ? null : typeof validToRaw === 'string' ? validToRaw : undefined;
|
|
94
|
+
|
|
95
|
+
for (const f of validateSidecar(sidecar, targetValidFrom, targetValidTo)) {
|
|
96
|
+
findings.push({
|
|
97
|
+
scope: PScope,
|
|
98
|
+
id: sidecar.target,
|
|
99
|
+
ruleId: f.code,
|
|
100
|
+
severity: f.severity,
|
|
101
|
+
message: `${f.code}: sidecar '${doc.path}' ${f.message}`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Run the versioned-attribute sidecar checks (VERSIONED-001..005) over the
|
|
108
|
+
* loaded element set and append findings. Pure, deterministic order. */
|
|
109
|
+
export function checkVersionedAttributes(input: RepoModelInput, findings: RepoFinding[]): void {
|
|
110
|
+
checkInlineTimeVarying(input.elements, findings);
|
|
111
|
+
checkSidecars(input.elements, findings);
|
|
112
|
+
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import { checkStrategyChainSemantics } from './check-strategy-chain.js';
|
|
31
31
|
import { checkElementHygiene } from './check-element-hygiene.js';
|
|
32
|
+
import { checkVersionedAttributes } from './check-versioned-attributes.js';
|
|
32
33
|
import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
|
|
33
34
|
|
|
34
35
|
const PScope: RepoFinding['scope'] = 'repo';
|
|
@@ -428,5 +429,7 @@ export function validateRepoModel(input: RepoModelInput): RepoFinding[] {
|
|
|
428
429
|
// Phase 8 — standalone-element envelope rules ported from DSM's Go
|
|
429
430
|
// Validate*Element functions (GOAL-ELEM-001..003, ACTION-001/002/005).
|
|
430
431
|
checkElementHygiene(input, findings);
|
|
432
|
+
// Phase 9 — versioned-attribute sidecar rules (CONTRACT.md §9, VERSIONED-001..005).
|
|
433
|
+
checkVersionedAttributes(input, findings);
|
|
431
434
|
return findings;
|
|
432
435
|
}
|
package/src/requirement/index.ts
CHANGED
|
@@ -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';
|