@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
@@ -37,7 +37,7 @@ export interface FGCAPreviewLayoutOptions {
37
37
  colGap?: number;
38
38
  /** Vertical gap (px) between stacked nodes within a column. Default matches the historical hardcoded value. */
39
39
  rowGap?: number;
40
- /** Trim to a level cap or a single root goal (vkgeorgia/strategy#77). Defaults to 'all'. */
40
+ /** Trim to a level cap or a single root goal. Defaults to 'all'. */
41
41
  scope?: Scope;
42
42
  /** Entity node width (px). Default {@link FGCA_NODE_W}. */
43
43
  nodeWidth?: number;
@@ -15,5 +15,5 @@ export interface GoalNodeData {
15
15
  }
16
16
  declare const GoalNode: React.MemoExoticComponent<({ data }: {
17
17
  data: GoalNodeData;
18
- }) => import("react/jsx-runtime").JSX.Element>;
18
+ }) => React.JSX.Element>;
19
19
  export default GoalNode;
@@ -1,7 +1,7 @@
1
1
  import type { Goal, GoalTree, LayoutOptions, GoalTreeLayout } from './types.js';
2
2
  import type { Scope } from '../scope.js';
3
3
  /**
4
- * Trims a flat goal list to a scope (vkgeorgia/strategy#77).
4
+ * Trims a flat goal list to a scope.
5
5
  * - 'level' → goals with `level <= maxLevel`.
6
6
  * - 'root' → the root goal (matched by string id) plus all its descendants
7
7
  * via `parent_id`; an empty list when the root is absent.
@@ -4,7 +4,7 @@ const DEFAULT_NODE_HEIGHT = 80;
4
4
  const DEFAULT_RANK_SEP = 80;
5
5
  const DEFAULT_NODE_SEP = 40;
6
6
  /**
7
- * Trims a flat goal list to a scope (vkgeorgia/strategy#77).
7
+ * Trims a flat goal list to a scope.
8
8
  * - 'level' → goals with `level <= maxLevel`.
9
9
  * - 'root' → the root goal (matched by string id) plus all its descendants
10
10
  * 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
- * Strategy hub #63: this lives at the input boundary so the library's
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.
@@ -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
- * Strategy hub #63: this lives at the input boundary so the library's
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.
@@ -44,7 +44,7 @@ export interface LayoutOptions {
44
44
  nodeSep?: number;
45
45
  hideCollapsed?: number[];
46
46
  viewDepth?: number | null;
47
- /** Trim the tree to a level cap or a subtree root (vkgeorgia/strategy#77). Defaults to 'all'. */
47
+ /** Trim the tree to a level cap or a subtree root. Defaults to 'all'. */
48
48
  scope?: Scope;
49
49
  }
50
50
  export interface LaidOutNode {
@@ -15,7 +15,7 @@ export function validateGoalTree(input) {
15
15
  return { valid: false, errors, warnings };
16
16
  const tree = raw;
17
17
  // GOALS-013 — goal_types[].level values must be contiguous starting from 0.
18
- // Per the N+1 rule (decision in vkgeorgia/strategy#66), the level set must
18
+ // Per the N+1 rule, the level set must
19
19
  // be exactly {0, 1, …, N}; a gap breaks the parent→child level invariant
20
20
  // and means GOALS-012 cannot be enforced consistently.
21
21
  const levels = tree.goal_types
@@ -84,7 +84,7 @@ export function validateGoalTree(input) {
84
84
  continue;
85
85
  }
86
86
  // GOALS-012 — parent must be exactly one level above the child (N+1
87
- // hierarchy, decision in vkgeorgia/strategy#66). Only enforced when the
87
+ // hierarchy). Only enforced when the
88
88
  // parent is resolvable; a missing parent is covered by BROKEN_PARENT_REF.
89
89
  if (g.parent_id !== 0) {
90
90
  const parentLevel = levelById.get(g.parent_id);
@@ -0,0 +1,43 @@
1
+ export declare const ENVELOPE_FIELDS: string[];
2
+ /** Every non-blank, non-comment line of the file, except those belonging to
3
+ * an `ENVELOPE_FIELDS` block (the key line and its more-indented
4
+ * continuation lines), whitespace-normalised and sorted —
5
+ * order-independent, formatting-independent, comment-independent. Not a
6
+ * general YAML parser — a flat, line-oriented pass over the already-known
7
+ * envelope field names, the same posture the methodology reference script
8
+ * takes (its own header comment cites `baseline-manifest.mjs` /
9
+ * `check-agreement.mjs` for precedent). */
10
+ export declare function statementLines(text: string): string[];
11
+ export interface LineEdit {
12
+ from: string;
13
+ to: string;
14
+ }
15
+ /** A parsed `migrations/<slug>/TRANSFORM.yaml` manifest (§16.3). */
16
+ export interface MigrationManifest {
17
+ slug: string;
18
+ mechanical: boolean;
19
+ appliesTo: string[];
20
+ lineEdits: LineEdit[];
21
+ }
22
+ /** Minimal parse of the fixed `TRANSFORM.yaml` shape — `mechanical: true`,
23
+ * `applies_to: [exact relative paths]`, `line_edits: [{from, to}]`. Not a
24
+ * general YAML parser, same posture as {@link statementLines}. */
25
+ export declare function parseMigrationManifest(text: string): Omit<MigrationManifest, 'slug'>;
26
+ export interface SuspicionResult {
27
+ suspicious: boolean;
28
+ /** True when a manifest declared `mechanical: true` for this target but
29
+ * its replay did not fully explain the change — the hatch does not
30
+ * self-grant; suspicion stands regardless of the flag (§16.3). */
31
+ hatchRefused?: boolean;
32
+ }
33
+ /**
34
+ * §16.2: *Suspicious* ⟺ the target's content identity at the anchor commit
35
+ * differs from its content identity now, **and** §16.3's hatch does not
36
+ * explain the difference.
37
+ *
38
+ * `beforeText` / `afterText` are already-fetched (git IO lives in the
39
+ * caller); `undefined` for either (no anchor, or the target no longer
40
+ * exists) means nothing to compare — not suspicious, per the reference
41
+ * script's `target-absent-at-anchor` / `target-absent-now` cases.
42
+ */
43
+ export declare function checkSuspicion(beforeText: string | undefined, afterText: string | undefined, applicableManifests?: Array<Omit<MigrationManifest, 'slug'>>): SuspicionResult;
@@ -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,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,2 @@
1
+ export { METRIC_DIRECTIONS_OF_GOOD } from './types.js';
2
+ export { validateMetric } from './validate.js';
@@ -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
+ // METRIC — motivation-layer managed indicator (no ArchiMate counterpart).
2
+ // Schema: methodology notations/ELEMENT_PRIMITIVES.md §7.27.
3
+ export const METRIC_DIRECTIONS_OF_GOOD = [
4
+ 'higher_is_better',
5
+ 'lower_is_better',
6
+ 'on_target',
7
+ ];
@@ -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,3 @@
1
+ export type { Need } from './types.js';
2
+ export { validateNeed } from './validate.js';
3
+ export type { NeedValidateOptions } from './validate.js';
@@ -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,3 @@
1
+ // NEED — motivation-layer stakeholder/user need (no ArchiMate counterpart).
2
+ // Upstream of REQUIREMENT — schema: methodology notations/ELEMENT_PRIMITIVES.md §7.28.
3
+ export {};
@@ -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,6 @@
1
+ import type { RepoFinding, RepoModelInput } from './types.js';
2
+ /**
3
+ * `ADMIT-009` — a candidate-only field present on an admitted (`zone: canon`)
4
+ * element, of any TYPE. Appends findings; pure, deterministic order.
5
+ */
6
+ export declare function checkCandidateFields(input: RepoModelInput, findings: RepoFinding[]): void;
@@ -0,0 +1,87 @@
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
+ import { docId } from './validate-repo.js';
40
+ const PScope = 'repo';
41
+ /**
42
+ * Fields that belong to an ingest candidate and are never persisted into
43
+ * canon. Each entry names the field and the provenance pattern that replaces
44
+ * it, so the finding points at the correct shape rather than only failing
45
+ * closed. One entry today; the list is the extension point if methodology
46
+ * closes another candidate-side field out of canon the same way.
47
+ */
48
+ const CANDIDATE_ONLY_FIELDS = [
49
+ {
50
+ field: 'extraction_confidence',
51
+ instead: "cite the evidence through 'derived_from' (ELEMENT_PRIMITIVES.md §3) — " +
52
+ 'an OBSERVATION or another Field/Codex artefact; the review flag stays on the ' +
53
+ 'ingest candidate and is not carried across admission',
54
+ },
55
+ ];
56
+ /** True when the doc is an admitted canon element — `zone: canon` per
57
+ * `ELEMENT_PRIMITIVES.md` §3. Deliberately narrow: a doc with no `zone`
58
+ * (a sidecar) or a non-canon zone (`field`, `codex`) is not admitted canon
59
+ * and this rule says nothing about it. */
60
+ function isAdmittedCanonElement(doc) {
61
+ return doc.data?.['zone'] === 'canon';
62
+ }
63
+ /**
64
+ * `ADMIT-009` — a candidate-only field present on an admitted (`zone: canon`)
65
+ * element, of any TYPE. Appends findings; pure, deterministic order.
66
+ */
67
+ export function checkCandidateFields(input, findings) {
68
+ for (const doc of input.elements) {
69
+ if (!doc.data)
70
+ continue;
71
+ if (!isAdmittedCanonElement(doc))
72
+ continue;
73
+ const id = docId(doc) ?? '';
74
+ for (const { field, instead } of CANDIDATE_ONLY_FIELDS) {
75
+ if (doc.data[field] === undefined)
76
+ continue;
77
+ findings.push({
78
+ scope: PScope,
79
+ id,
80
+ ruleId: 'ADMIT-009',
81
+ message: `ADMIT-009: admitted element '${id || doc.path}' carries '${field}', ` +
82
+ `which is an ingest-candidate review flag and is never persisted into canon ` +
83
+ `(ELEMENT_PRIMITIVES.md §7.29). Remove it and ${instead}.`,
84
+ });
85
+ }
86
+ }
87
+ }
@@ -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;