@transitrix/diagrams 1.8.21 → 1.10.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.
Files changed (190) hide show
  1. package/dist/activity-card/types.js +1 -1
  2. package/dist/agreement.d.ts +15 -0
  3. package/dist/agreement.js +61 -0
  4. package/dist/applications/index.d.ts +2 -0
  5. package/dist/applications/index.js +1 -0
  6. package/dist/applications/resolve-maturity.d.ts +16 -0
  7. package/dist/applications/resolve-maturity.js +57 -0
  8. package/dist/applications/validate.js +20 -0
  9. package/dist/capability-map/CapabilityCardNode.d.ts +1 -1
  10. package/dist/capability-map/CapabilityRootNode.d.ts +1 -1
  11. package/dist/capability-map/index.d.ts +2 -0
  12. package/dist/capability-map/index.js +1 -0
  13. package/dist/capability-map/resolve-maturity.d.ts +20 -0
  14. package/dist/capability-map/resolve-maturity.js +72 -0
  15. package/dist/capability-map/validate.js +1 -2
  16. package/dist/codex/index.d.ts +1 -1
  17. package/dist/codex/index.js +1 -1
  18. package/dist/codex/types.d.ts +9 -2
  19. package/dist/codex/types.js +10 -3
  20. package/dist/codex/validate.js +29 -3
  21. package/dist/compliance/classify.d.ts +21 -2
  22. package/dist/compliance/classify.js +68 -13
  23. package/dist/compliance/confidence.js +1 -1
  24. package/dist/compliance/gap-report.d.ts +31 -1
  25. package/dist/compliance/gap-report.js +55 -3
  26. package/dist/compliance/html.js +14 -6
  27. package/dist/compliance/impact.js +1 -1
  28. package/dist/compliance/index.d.ts +3 -1
  29. package/dist/compliance/index.js +2 -0
  30. package/dist/compliance/markdown.js +26 -4
  31. package/dist/compliance/reverse-index.d.ts +6 -1
  32. package/dist/compliance/reverse-index.js +27 -3
  33. package/dist/compliance/trace.js +5 -1
  34. package/dist/compliance/types.d.ts +70 -0
  35. package/dist/compliance/types.js +1 -2
  36. package/dist/compliance/views.js +1 -1
  37. package/dist/compliance-matrix/build.js +0 -0
  38. package/dist/compliance-matrix/types.d.ts +1 -1
  39. package/dist/compliance-matrix/types.js +1 -1
  40. package/dist/confidence/score.js +1 -1
  41. package/dist/constraint/validate.js +3 -0
  42. package/dist/edge-path.js +1 -1
  43. package/dist/factor/validate.js +2 -2
  44. package/dist/fgca/chain-table.js +1 -1
  45. package/dist/fgca/nodeTypes.d.ts +4 -4
  46. package/dist/fgca/nodes/FGCAActivityNode.d.ts +1 -1
  47. package/dist/fgca/nodes/FGCAChangeNode.d.ts +1 -1
  48. package/dist/fgca/nodes/FGCAFactorNode.d.ts +1 -1
  49. package/dist/fgca/nodes/FGCAGoalNode.d.ts +1 -1
  50. package/dist/fgca/parse-canonical.d.ts +2 -2
  51. package/dist/fgca/parse-canonical.js +2 -2
  52. package/dist/fgca/preview-layout.d.ts +1 -1
  53. package/dist/goals/GoalNode.d.ts +1 -1
  54. package/dist/goals/layout.d.ts +1 -1
  55. package/dist/goals/layout.js +1 -1
  56. package/dist/goals/parse-canonical.d.ts +1 -1
  57. package/dist/goals/parse-canonical.js +1 -1
  58. package/dist/goals/types.d.ts +1 -1
  59. package/dist/goals/validate.js +2 -2
  60. package/dist/link-suspicion.d.ts +43 -0
  61. package/dist/link-suspicion.js +156 -0
  62. package/dist/metric/index.d.ts +4 -0
  63. package/dist/metric/index.js +2 -0
  64. package/dist/metric/types.d.ts +24 -0
  65. package/dist/metric/types.js +7 -0
  66. package/dist/metric/validate.d.ts +7 -0
  67. package/dist/metric/validate.js +90 -0
  68. package/dist/need/index.d.ts +3 -0
  69. package/dist/need/index.js +1 -0
  70. package/dist/need/types.d.ts +21 -0
  71. package/dist/need/types.js +3 -0
  72. package/dist/need/validate.d.ts +7 -0
  73. package/dist/need/validate.js +60 -0
  74. package/dist/repo-validate/check-candidate-fields.d.ts +6 -0
  75. package/dist/repo-validate/check-candidate-fields.js +87 -0
  76. package/dist/repo-validate/check-versioned-attributes.d.ts +4 -0
  77. package/dist/repo-validate/check-versioned-attributes.js +105 -0
  78. package/dist/repo-validate/validate-repo.js +7 -0
  79. package/dist/requirement/index.d.ts +2 -2
  80. package/dist/requirement/index.js +1 -1
  81. package/dist/requirement/types.d.ts +19 -1
  82. package/dist/requirement/types.js +3 -1
  83. package/dist/requirement/validate.js +22 -1
  84. package/dist/risk/index.d.ts +4 -0
  85. package/dist/risk/index.js +2 -0
  86. package/dist/risk/types.d.ts +25 -0
  87. package/dist/risk/types.js +3 -0
  88. package/dist/risk/validate.d.ts +7 -0
  89. package/dist/risk/validate.js +108 -0
  90. package/dist/schema-version.d.ts +9 -6
  91. package/dist/schema-version.js +9 -6
  92. package/dist/theme/themes.js +3 -2
  93. package/dist/validation/index.d.ts +4 -0
  94. package/dist/validation/index.js +2 -0
  95. package/dist/validation/types.d.ts +31 -0
  96. package/dist/validation/types.js +21 -0
  97. package/dist/validation/validate.d.ts +7 -0
  98. package/dist/validation/validate.js +106 -0
  99. package/dist/verification/index.d.ts +4 -0
  100. package/dist/verification/index.js +2 -0
  101. package/dist/verification/types.d.ts +31 -0
  102. package/dist/verification/types.js +12 -0
  103. package/dist/verification/validate.d.ts +7 -0
  104. package/dist/verification/validate.js +104 -0
  105. package/dist/versioned-attribute/index.d.ts +4 -0
  106. package/dist/versioned-attribute/index.js +2 -0
  107. package/dist/versioned-attribute/resolve.d.ts +15 -0
  108. package/dist/versioned-attribute/resolve.js +37 -0
  109. package/dist/versioned-attribute/types.d.ts +12 -0
  110. package/dist/versioned-attribute/types.js +6 -0
  111. package/dist/versioned-attribute/validate.d.ts +17 -0
  112. package/dist/versioned-attribute/validate.js +87 -0
  113. package/dist/webview/entry.js +11 -4
  114. package/dist/webview/render-applications.d.ts +15 -1
  115. package/dist/webview/render-applications.js +7 -2
  116. package/dist/webview/render-blocks.js +6 -4
  117. package/dist/webview/render-capability-map.d.ts +16 -1
  118. package/dist/webview/render-capability-map.js +4 -1
  119. package/dist/webview/render-process.d.ts +1 -1
  120. package/dist/webview/render-process.js +5 -5
  121. package/package.json +9 -6
  122. package/src/activity-card/types.ts +1 -1
  123. package/src/agreement.ts +71 -0
  124. package/src/applications/index.ts +3 -0
  125. package/src/applications/resolve-maturity.ts +66 -0
  126. package/src/applications/validate.ts +23 -0
  127. package/src/capability-map/index.ts +7 -0
  128. package/src/capability-map/resolve-maturity.ts +84 -0
  129. package/src/capability-map/validate.ts +1 -2
  130. package/src/codex/index.ts +1 -1
  131. package/src/codex/types.ts +11 -3
  132. package/src/codex/validate.ts +30 -2
  133. package/src/compliance/classify.ts +87 -14
  134. package/src/compliance/confidence.ts +1 -1
  135. package/src/compliance/gap-report.ts +91 -4
  136. package/src/compliance/html.ts +18 -6
  137. package/src/compliance/impact.ts +1 -1
  138. package/src/compliance/index.ts +6 -0
  139. package/src/compliance/markdown.ts +18 -4
  140. package/src/compliance/reverse-index.ts +35 -4
  141. package/src/compliance/trace.ts +7 -1
  142. package/src/compliance/types.ts +75 -2
  143. package/src/compliance/views.ts +1 -1
  144. package/src/compliance-matrix/build.ts +0 -0
  145. package/src/compliance-matrix/types.ts +2 -2
  146. package/src/confidence/score.ts +1 -1
  147. package/src/constraint/validate.ts +4 -0
  148. package/src/edge-path.ts +1 -1
  149. package/src/factor/validate.ts +2 -2
  150. package/src/fgca/chain-table.ts +1 -1
  151. package/src/fgca/parse-canonical.ts +2 -2
  152. package/src/fgca/preview-layout.ts +1 -1
  153. package/src/goals/layout.ts +1 -1
  154. package/src/goals/parse-canonical.ts +1 -1
  155. package/src/goals/types.ts +1 -1
  156. package/src/goals/validate.ts +2 -2
  157. package/src/link-suspicion.ts +181 -0
  158. package/src/metric/index.ts +4 -0
  159. package/src/metric/types.ts +38 -0
  160. package/src/metric/validate.ts +102 -0
  161. package/src/need/index.ts +3 -0
  162. package/src/need/types.ts +31 -0
  163. package/src/need/validate.ts +71 -0
  164. package/src/repo-validate/check-candidate-fields.ts +93 -0
  165. package/src/repo-validate/check-versioned-attributes.ts +112 -0
  166. package/src/repo-validate/validate-repo.ts +7 -0
  167. package/src/requirement/index.ts +2 -2
  168. package/src/requirement/types.ts +26 -1
  169. package/src/requirement/validate.ts +25 -1
  170. package/src/risk/index.ts +4 -0
  171. package/src/risk/types.ts +35 -0
  172. package/src/risk/validate.ts +121 -0
  173. package/src/schema-version.ts +9 -6
  174. package/src/theme/themes.ts +3 -2
  175. package/src/validation/index.ts +4 -0
  176. package/src/validation/types.ts +57 -0
  177. package/src/validation/validate.ts +113 -0
  178. package/src/verification/index.ts +4 -0
  179. package/src/verification/types.ts +48 -0
  180. package/src/verification/validate.ts +111 -0
  181. package/src/versioned-attribute/index.ts +4 -0
  182. package/src/versioned-attribute/resolve.ts +45 -0
  183. package/src/versioned-attribute/types.ts +19 -0
  184. package/src/versioned-attribute/validate.ts +109 -0
  185. package/src/webview/entry.ts +13 -6
  186. package/src/webview/render-applications.ts +25 -2
  187. package/src/webview/render-blocks.ts +8 -4
  188. package/src/webview/render-capability-map.ts +23 -1
  189. package/src/webview/render-process.ts +5 -5
  190. package/src/webview/styles.css +6 -0
@@ -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,4 @@
1
+ export type { Metric, MetricDirectionOfGood } from './types.js';
2
+ export { METRIC_DIRECTIONS_OF_GOOD } from './types.js';
3
+ export { validateMetric } from './validate.js';
4
+ export type { MetricValidateOptions } from './validate.js';
@@ -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,3 @@
1
+ export type { Need } from './types.js';
2
+ export { validateNeed } from './validate.js';
3
+ export type { NeedValidateOptions } from './validate.js';
@@ -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,93 @@
1
+ // Candidate-only fields on admitted canon — `ADMIT-009`.
2
+ //
3
+ // `ELEMENT_PRIMITIVES.md` §7.29 (methodology v3.6.0) states it normatively for
4
+ // `RELEASE` and then generalises it to every TYPE:
5
+ //
6
+ // > `RELEASE` has no `extraction_confidence` field — and neither does any
7
+ // > other canon element, of any TYPE. `extraction_confidence` is a review
8
+ // > flag on an ingest candidate (`candidate.extraction_confidence`,
9
+ // > vocabulary.yaml); it is surfaced in the review queue, drives
10
+ // > reviewer-authority routing (CONTRACT.md §6.2), and is never persisted
11
+ // > into canon. An admitted element carrying it is a defect in whatever
12
+ // > wrote it, not a precedent to follow.
13
+ //
14
+ // Nothing enforced that mechanically. The ingest pipeline that produces the
15
+ // field lives in `methodology`'s `packages/ingest-cli` and only ever writes
16
+ // *candidates*; admission into `canon/elements/**` happens in an adopter's own
17
+ // tooling, which is `@transitrix/cli validate`'s side of the fence — so this is
18
+ // where the rule has to bite.
19
+ //
20
+ // **Why the check keys on `zone`, not on the folder.** `extraction_confidence`
21
+ // is legitimate on a candidate and meaningless — but not a defect — elsewhere;
22
+ // only the *admitted* form is wrong. `zone: canon` is the admission marker
23
+ // every standalone element carries (`ELEMENT_PRIMITIVES.md` §3, required
24
+ // envelope field, `ELEM-001`), and it is the discriminator against the two
25
+ // zones that may legitimately carry harvest metadata: `zone: field`, whose
26
+ // provenance contract owns `source_quality` (CONTRACT.md §5/§11.2), and
27
+ // `zone: codex`. Sidecars (`*.history.yaml`, `*.runstate.yaml`) carry no
28
+ // `zone` and no `id`, so they fall out for free — the same "no id -> not an
29
+ // element" split `docId` already makes for every sibling check in this folder.
30
+ //
31
+ // **Rule code.** Studio-authored as `ELEM-CANDIDATE-FIELD-001` (in the shape
32
+ // methodology already uses for a named element-envelope rule outside the
33
+ // numbered run — `ELEM-ALIAS-001`, `ELEM-FORMER-ID-001` — deliberately not
34
+ // `ELEM-006`, since the numbered `ELEM-*` run is methodology's to extend and
35
+ // squatting the next free number would collide with whatever it registers
36
+ // there next). Renamed to `ADMIT-009` (transitrix/methodology#505, merged
37
+ // 2026-08-19) now that methodology has registered a code of its own for
38
+ // §7.29 — no functional change, this is that alias.
39
+
40
+ import { docId } from './validate-repo.js';
41
+ import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
42
+
43
+ const PScope: RepoFinding['scope'] = 'repo';
44
+
45
+ /**
46
+ * Fields that belong to an ingest candidate and are never persisted into
47
+ * canon. Each entry names the field and the provenance pattern that replaces
48
+ * it, so the finding points at the correct shape rather than only failing
49
+ * closed. One entry today; the list is the extension point if methodology
50
+ * closes another candidate-side field out of canon the same way.
51
+ */
52
+ const CANDIDATE_ONLY_FIELDS: readonly { field: string; instead: string }[] = [
53
+ {
54
+ field: 'extraction_confidence',
55
+ instead:
56
+ "cite the evidence through 'derived_from' (ELEMENT_PRIMITIVES.md §3) — " +
57
+ 'an OBSERVATION or another Field/Codex artefact; the review flag stays on the ' +
58
+ 'ingest candidate and is not carried across admission',
59
+ },
60
+ ];
61
+
62
+ /** True when the doc is an admitted canon element — `zone: canon` per
63
+ * `ELEMENT_PRIMITIVES.md` §3. Deliberately narrow: a doc with no `zone`
64
+ * (a sidecar) or a non-canon zone (`field`, `codex`) is not admitted canon
65
+ * and this rule says nothing about it. */
66
+ function isAdmittedCanonElement(doc: RepoDoc): boolean {
67
+ return doc.data?.['zone'] === 'canon';
68
+ }
69
+
70
+ /**
71
+ * `ADMIT-009` — a candidate-only field present on an admitted (`zone: canon`)
72
+ * element, of any TYPE. Appends findings; pure, deterministic order.
73
+ */
74
+ export function checkCandidateFields(input: RepoModelInput, findings: RepoFinding[]): void {
75
+ for (const doc of input.elements) {
76
+ if (!doc.data) continue;
77
+ if (!isAdmittedCanonElement(doc)) continue;
78
+
79
+ const id = docId(doc) ?? '';
80
+ for (const { field, instead } of CANDIDATE_ONLY_FIELDS) {
81
+ if (doc.data[field] === undefined) continue;
82
+ findings.push({
83
+ scope: PScope,
84
+ id,
85
+ ruleId: 'ADMIT-009',
86
+ message:
87
+ `ADMIT-009: admitted element '${id || doc.path}' carries '${field}', ` +
88
+ `which is an ingest-candidate review flag and is never persisted into canon ` +
89
+ `(ELEMENT_PRIMITIVES.md §7.29). Remove it and ${instead}.`,
90
+ });
91
+ }
92
+ }
93
+ }
@@ -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,8 @@
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';
33
+ import { checkCandidateFields } from './check-candidate-fields.js';
32
34
  import type { RepoDoc, RepoFinding, RepoModelInput } from './types.js';
33
35
 
34
36
  const PScope: RepoFinding['scope'] = 'repo';
@@ -428,5 +430,10 @@ export function validateRepoModel(input: RepoModelInput): RepoFinding[] {
428
430
  // Phase 8 — standalone-element envelope rules ported from DSM's Go
429
431
  // Validate*Element functions (GOAL-ELEM-001..003, ACTION-001/002/005).
430
432
  checkElementHygiene(input, findings);
433
+ // Phase 9 — versioned-attribute sidecar rules (CONTRACT.md §9, VERSIONED-001..005).
434
+ checkVersionedAttributes(input, findings);
435
+ // Phase 10 — candidate-only fields on admitted canon (ELEMENT_PRIMITIVES.md
436
+ // §7.29, ADMIT-009).
437
+ checkCandidateFields(input, findings);
431
438
  return findings;
432
439
  }