@thinkingsage/kanon 0.8.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 (199) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -0
  4. package/bridge/mcp-server.cjs +14171 -0
  5. package/package.json +98 -0
  6. package/src/adapters/capabilities.ts +178 -0
  7. package/src/adapters/claude-code.ts +110 -0
  8. package/src/adapters/cline.ts +98 -0
  9. package/src/adapters/codex.ts +173 -0
  10. package/src/adapters/copilot.ts +106 -0
  11. package/src/adapters/cursor.ts +97 -0
  12. package/src/adapters/degradation.ts +95 -0
  13. package/src/adapters/index.ts +324 -0
  14. package/src/adapters/kiro-frontmatter.ts +139 -0
  15. package/src/adapters/kiro-inclusion.ts +86 -0
  16. package/src/adapters/kiro.ts +412 -0
  17. package/src/adapters/qdeveloper.ts +115 -0
  18. package/src/adapters/types.ts +81 -0
  19. package/src/adapters/windsurf.ts +96 -0
  20. package/src/admin.ts +283 -0
  21. package/src/asset-conventions.ts +118 -0
  22. package/src/attribution-backfill.ts +319 -0
  23. package/src/attribution-report.ts +95 -0
  24. package/src/attribution.ts +239 -0
  25. package/src/backends/github.ts +194 -0
  26. package/src/backends/http.ts +122 -0
  27. package/src/backends/index.ts +39 -0
  28. package/src/backends/local.ts +47 -0
  29. package/src/backends/s3.ts +157 -0
  30. package/src/backends/types.ts +59 -0
  31. package/src/base-cache.ts +270 -0
  32. package/src/browse-ui.ts +3754 -0
  33. package/src/browse.ts +1038 -0
  34. package/src/build.ts +1108 -0
  35. package/src/catalog.ts +204 -0
  36. package/src/cli-deprecated.ts +29 -0
  37. package/src/cli.ts +773 -0
  38. package/src/collection-admin.ts +287 -0
  39. package/src/collection-builder.ts +464 -0
  40. package/src/collections.ts +116 -0
  41. package/src/compatibility.ts +105 -0
  42. package/src/config.ts +743 -0
  43. package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
  44. package/src/eval.ts +1169 -0
  45. package/src/file-writer.ts +61 -0
  46. package/src/format-registry.ts +141 -0
  47. package/src/guild/auto-updater.ts +163 -0
  48. package/src/guild/backend-resolver.ts +49 -0
  49. package/src/guild/cli.ts +592 -0
  50. package/src/guild/collection-expander.ts +47 -0
  51. package/src/guild/global-cache.ts +247 -0
  52. package/src/guild/hook-generator.ts +100 -0
  53. package/src/guild/manifest.ts +154 -0
  54. package/src/guild/path-utils.ts +12 -0
  55. package/src/guild/sync.ts +622 -0
  56. package/src/guild/version-resolver.ts +42 -0
  57. package/src/help/metadata.ts +445 -0
  58. package/src/help/renderer.ts +265 -0
  59. package/src/help/typo-suggester.ts +25 -0
  60. package/src/hooks/expression.ts +493 -0
  61. package/src/hooks/pipeline.ts +141 -0
  62. package/src/import.ts +773 -0
  63. package/src/importers/claude-code.ts +134 -0
  64. package/src/importers/cline.ts +103 -0
  65. package/src/importers/codex.ts +140 -0
  66. package/src/importers/copilot.ts +103 -0
  67. package/src/importers/cursor.ts +105 -0
  68. package/src/importers/index.ts +390 -0
  69. package/src/importers/kiro.ts +110 -0
  70. package/src/importers/qdeveloper.ts +103 -0
  71. package/src/importers/types.ts +54 -0
  72. package/src/importers/windsurf.ts +104 -0
  73. package/src/install.ts +1005 -0
  74. package/src/manifest-admin.ts +306 -0
  75. package/src/mcp-bridge.ts +240 -0
  76. package/src/mutation/delta.ts +50 -0
  77. package/src/mutation/history.ts +66 -0
  78. package/src/mutation/operators.ts +524 -0
  79. package/src/mutation/runner.ts +332 -0
  80. package/src/new.ts +106 -0
  81. package/src/outcomes/collision.ts +127 -0
  82. package/src/outcomes/normalize.ts +208 -0
  83. package/src/outcomes/registry.ts +173 -0
  84. package/src/parser.ts +446 -0
  85. package/src/provenance-backfill-cli.ts +319 -0
  86. package/src/provenance-backfill.ts +520 -0
  87. package/src/publish.ts +354 -0
  88. package/src/reconcile-orchestrator.ts +502 -0
  89. package/src/reconcile-report-renderer.ts +176 -0
  90. package/src/resolve-body.ts +15 -0
  91. package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
  92. package/src/rosetta/builtins/contracts.ts +1033 -0
  93. package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
  94. package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
  95. package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
  96. package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
  97. package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
  98. package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
  99. package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
  100. package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
  101. package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
  102. package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
  103. package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
  104. package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
  105. package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
  106. package/src/rosetta/builtins/sources/cline-native.ts +176 -0
  107. package/src/rosetta/builtins/sources/codex-native.ts +343 -0
  108. package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
  109. package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
  110. package/src/rosetta/builtins/sources/index.ts +95 -0
  111. package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
  112. package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
  113. package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
  114. package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
  115. package/src/rosetta/builtins/sources/superpowers.ts +240 -0
  116. package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
  117. package/src/rosetta/builtins/targets/claude-code.ts +181 -0
  118. package/src/rosetta/builtins/targets/cline.ts +87 -0
  119. package/src/rosetta/builtins/targets/codex.ts +226 -0
  120. package/src/rosetta/builtins/targets/copilot.ts +103 -0
  121. package/src/rosetta/builtins/targets/cursor.ts +87 -0
  122. package/src/rosetta/builtins/targets/index.ts +60 -0
  123. package/src/rosetta/builtins/targets/kiro.ts +278 -0
  124. package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
  125. package/src/rosetta/builtins/targets/windsurf.ts +87 -0
  126. package/src/rosetta/canonical.ts +729 -0
  127. package/src/rosetta/compatibility.ts +432 -0
  128. package/src/rosetta/contracts.ts +329 -0
  129. package/src/rosetta/detector.ts +724 -0
  130. package/src/rosetta/diagnostics.ts +630 -0
  131. package/src/rosetta/engine-bootstrap.ts +103 -0
  132. package/src/rosetta/engine.ts +744 -0
  133. package/src/rosetta/index.ts +381 -0
  134. package/src/rosetta/inspection.ts +530 -0
  135. package/src/rosetta/plan.ts +448 -0
  136. package/src/rosetta/provenance-digest.ts +369 -0
  137. package/src/rosetta/reconcile.ts +812 -0
  138. package/src/rosetta/redaction.ts +467 -0
  139. package/src/rosetta/registry.ts +712 -0
  140. package/src/rosetta/renderers.ts +571 -0
  141. package/src/rosetta/request-guard.ts +335 -0
  142. package/src/rosetta/resolution.ts +419 -0
  143. package/src/rosetta/source-accounting.ts +233 -0
  144. package/src/rosetta/templates.ts +129 -0
  145. package/src/rosetta-cli.ts +717 -0
  146. package/src/rosetta-docs-generator.ts +793 -0
  147. package/src/rosetta-profiles-cli.ts +367 -0
  148. package/src/schemas.ts +1712 -0
  149. package/src/spec-coordination.ts +1141 -0
  150. package/src/temper.ts +747 -0
  151. package/src/template-bundle-loader.ts +312 -0
  152. package/src/template-engine.ts +53 -0
  153. package/src/translation-application-policy.ts +496 -0
  154. package/src/translation-orchestrator.ts +1013 -0
  155. package/src/translation-plan-applier.ts +473 -0
  156. package/src/tutorial.ts +305 -0
  157. package/src/validate.ts +1093 -0
  158. package/src/versioning.ts +553 -0
  159. package/src/wizard.ts +660 -0
  160. package/src/workspace.ts +237 -0
  161. package/templates/eval-contexts/claude-code.md.njk +6 -0
  162. package/templates/eval-contexts/cline.md.njk +6 -0
  163. package/templates/eval-contexts/copilot.md.njk +6 -0
  164. package/templates/eval-contexts/cursor.md.njk +6 -0
  165. package/templates/eval-contexts/kiro.md.njk +10 -0
  166. package/templates/eval-contexts/qdeveloper.md.njk +6 -0
  167. package/templates/eval-contexts/windsurf.md.njk +6 -0
  168. package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
  169. package/templates/harness-adapters/_base/base.md.njk +16 -0
  170. package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
  171. package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
  172. package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
  173. package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
  174. package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
  175. package/templates/harness-adapters/cline/hook.sh.njk +4 -0
  176. package/templates/harness-adapters/cline/mcp.json.njk +1 -0
  177. package/templates/harness-adapters/cline/rule.md.njk +1 -0
  178. package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
  179. package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
  180. package/templates/harness-adapters/codex/skill.md.njk +27 -0
  181. package/templates/harness-adapters/copilot/agents.md.njk +1 -0
  182. package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
  183. package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
  184. package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
  185. package/templates/harness-adapters/cursor/rule.md.njk +6 -0
  186. package/templates/harness-adapters/kiro/hook.json.njk +1 -0
  187. package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
  188. package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
  189. package/templates/harness-adapters/kiro/power.md.njk +12 -0
  190. package/templates/harness-adapters/kiro/steering.md.njk +16 -0
  191. package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
  192. package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
  193. package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
  194. package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
  195. package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
  196. package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
  197. package/templates/knowledge/hooks.yaml.njk +4 -0
  198. package/templates/knowledge/knowledge.md.njk +53 -0
  199. package/templates/knowledge/mcp-servers.yaml.njk +2 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Outcomes registry — pure shape normalization.
3
+ *
4
+ * Canonicalizes type-shape strings so that two shapes compare equal when they
5
+ * are spec-level equivalent. Pure, deterministic, and idempotent: there is no
6
+ * I/O and no shared state, and `normalizeShape(normalizeShape(s))` always equals
7
+ * `normalizeShape(s)`.
8
+ *
9
+ * See ADR-0041 for the architectural rationale.
10
+ */
11
+
12
+ /** Characters that may appear inside an identifier (post-lowercasing). */
13
+ const IDENT_CHAR = /[a-z0-9_$]/;
14
+
15
+ /**
16
+ * Canonicalize a type-shape string for comparison in the outcomes registry.
17
+ *
18
+ * Sub-steps are applied in a fixed order (Requirement 2D.1):
19
+ * 1. Trim leading/trailing whitespace.
20
+ * 2. Collapse internal runs of whitespace to a single space.
21
+ * 3. Lowercase everything.
22
+ * 4. Normalize `Array<T>` to `T[]` (before union sorting so members compare canonically).
23
+ * 5. Strip parameter names inside tuples: `(name: string, age: number)` -> `(string, number)`.
24
+ * 6. Sort top-level union members alphabetically and rejoin with ` | `.
25
+ *
26
+ * It deliberately does NOT (Requirement 2D.2):
27
+ * - resolve type aliases (`Path` stays distinct from `string`),
28
+ * - erase generic type parameters (`Result<T, E>` stays distinct from `Result<string, Error>`),
29
+ * - structurally compare object shapes (`{name: string}` stays distinct from `UserRecord`).
30
+ *
31
+ * Union sorting operates on top-level union members only; nested generics are
32
+ * left structurally intact.
33
+ */
34
+ export function normalizeShape(shape: string): string {
35
+ // Steps 1 & 2: trim, then collapse internal whitespace runs to a single space.
36
+ let result = shape.trim().replace(/\s+/g, " ");
37
+ // Step 3: lowercase.
38
+ result = result.toLowerCase();
39
+ // Step 4: Array<T> -> T[].
40
+ result = normalizeArraySyntax(result);
41
+ // Step 5: strip tuple parameter names.
42
+ result = stripTupleParamNames(result);
43
+ // Step 6: sort top-level union members.
44
+ result = sortTopLevelUnion(result);
45
+ return result;
46
+ }
47
+
48
+ /**
49
+ * Find the index of the close delimiter matching the open delimiter at
50
+ * `openIdx`. Counts nested occurrences of the same delimiter type. For angle
51
+ * brackets, a `>` that is part of an arrow (`=>`) is not treated as a closer.
52
+ * Returns -1 when no matching close delimiter is found.
53
+ */
54
+ function findMatchingDelimiter(s: string, openIdx: number): number {
55
+ const open = s[openIdx];
56
+ const close =
57
+ open === "<" ? ">" : open === "(" ? ")" : open === "[" ? "]" : "}";
58
+ let depth = 0;
59
+ for (let i = openIdx; i < s.length; i++) {
60
+ const ch = s[i];
61
+ // Skip the `>` in an arrow `=>` when scanning angle brackets.
62
+ if (open === "<" && ch === ">" && s[i - 1] === "=") {
63
+ continue;
64
+ }
65
+ if (ch === open) {
66
+ depth++;
67
+ } else if (ch === close) {
68
+ depth--;
69
+ if (depth === 0) {
70
+ return i;
71
+ }
72
+ }
73
+ }
74
+ return -1;
75
+ }
76
+
77
+ /**
78
+ * Split `s` on `sep` at the top level only, respecting nesting of `<>`, `()`,
79
+ * `[]`, and `{}`. A `>` that is part of an arrow (`=>`) does not decrement depth.
80
+ */
81
+ function splitTopLevel(s: string, sep: string): string[] {
82
+ const parts: string[] = [];
83
+ let depth = 0;
84
+ let current = "";
85
+ for (let i = 0; i < s.length; i++) {
86
+ const ch = s[i];
87
+ if (ch === "<" || ch === "(" || ch === "[" || ch === "{") {
88
+ depth++;
89
+ } else if (ch === ">" || ch === ")" || ch === "]" || ch === "}") {
90
+ // Don't count the `>` of an arrow `=>` as a closing delimiter.
91
+ if (!(ch === ">" && s[i - 1] === "=")) {
92
+ depth = Math.max(0, depth - 1);
93
+ }
94
+ }
95
+ if (ch === sep && depth === 0) {
96
+ parts.push(current);
97
+ current = "";
98
+ } else {
99
+ current += ch;
100
+ }
101
+ }
102
+ parts.push(current);
103
+ return parts;
104
+ }
105
+
106
+ /**
107
+ * Rewrite every `array<T>` to `T[]`, repeatedly, so nested arrays
108
+ * (`array<array<t>>` -> `t[][]`) are fully normalized. Only matches the
109
+ * `array` keyword when it stands on its own (not a suffix like `bytearray<...>`).
110
+ */
111
+ function normalizeArraySyntax(input: string): string {
112
+ let s = input;
113
+ while (true) {
114
+ const idx = findArrayKeyword(s);
115
+ if (idx === -1) {
116
+ break;
117
+ }
118
+ const openAngle = idx + "array".length; // position of '<'
119
+ if (s[openAngle] !== "<") {
120
+ break;
121
+ }
122
+ const close = findMatchingDelimiter(s, openAngle);
123
+ if (close === -1) {
124
+ break; // unbalanced; leave the rest untouched.
125
+ }
126
+ const inner = s.slice(openAngle + 1, close);
127
+ s = `${s.slice(0, idx)}${inner}[]${s.slice(close + 1)}`;
128
+ }
129
+ return s;
130
+ }
131
+
132
+ /**
133
+ * Locate the next standalone `array<` keyword (one not preceded by an
134
+ * identifier character). Returns -1 when none is found.
135
+ */
136
+ function findArrayKeyword(s: string): number {
137
+ let from = 0;
138
+ while (true) {
139
+ const idx = s.indexOf("array<", from);
140
+ if (idx === -1) {
141
+ return -1;
142
+ }
143
+ const prev = idx > 0 ? s[idx - 1] : "";
144
+ if (prev === "" || !IDENT_CHAR.test(prev)) {
145
+ return idx;
146
+ }
147
+ from = idx + 1;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Strip parameter names from tuple members within every parenthesized group,
153
+ * recursing into nested groups. `(name: string, age: number)` becomes
154
+ * `(string, number)`. Object shapes (`{...}`) are intentionally left untouched.
155
+ */
156
+ function stripTupleParamNames(input: string): string {
157
+ let result = "";
158
+ let i = 0;
159
+ while (i < input.length) {
160
+ const ch = input[i];
161
+ if (ch === "(") {
162
+ const close = findMatchingDelimiter(input, i);
163
+ if (close === -1) {
164
+ result += ch;
165
+ i++;
166
+ continue;
167
+ }
168
+ const inner = input.slice(i + 1, close);
169
+ // Recurse first so nested parenthesized groups are normalized too.
170
+ const processedInner = stripTupleParamNames(inner);
171
+ const stripped = splitTopLevel(processedInner, ",")
172
+ .map((part) => stripLeadingLabel(part.trim()))
173
+ .join(", ");
174
+ result += `(${stripped})`;
175
+ i = close + 1;
176
+ } else {
177
+ result += ch;
178
+ i++;
179
+ }
180
+ }
181
+ return result;
182
+ }
183
+
184
+ /**
185
+ * Remove a leading `name:` label from a single tuple element, returning the bare
186
+ * type. Elements that start with a bracket (e.g. object shapes) or that have no
187
+ * leading label are returned unchanged.
188
+ */
189
+ function stripLeadingLabel(element: string): string {
190
+ const match = element.match(/^[a-z_$][a-z0-9_$]*\s*:\s*(.*)$/);
191
+ return match ? match[1].trim() : element;
192
+ }
193
+
194
+ /**
195
+ * Sort top-level union members alphabetically and rejoin with ` | `. Members
196
+ * nested inside generics, tuples, or object shapes are not split. A shape with a
197
+ * single member (no top-level `|`) is returned trimmed and unchanged.
198
+ */
199
+ function sortTopLevelUnion(s: string): string {
200
+ const members = splitTopLevel(s, "|")
201
+ .map((member) => member.trim())
202
+ .filter((member) => member.length > 0);
203
+ if (members.length <= 1) {
204
+ return s.trim();
205
+ }
206
+ members.sort();
207
+ return members.join(" | ");
208
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Outcomes registry — pure aggregation + registry check.
3
+ *
4
+ * This module is the single deterministic entry point for the outcomes
5
+ * registry (Requirement 2F). It flattens outcomes declared across a set of
6
+ * artifacts into attributed references, then runs duplicate-id detection
7
+ * followed by pairwise two-tier collision detection over those references.
8
+ *
9
+ * Everything here is pure, deterministic, and free of I/O or shared state, so
10
+ * the cross-artifact comparison logic can be verified exhaustively with
11
+ * property-based tests. The same `runRegistryCheck` function backs both
12
+ * `kanon validate` (cross-artifact) and `kanon guild sync` (manifest-resolved
13
+ * artifacts); only the surrounding I/O and exit-code policy differ.
14
+ *
15
+ * Detection order and policy:
16
+ * 1. Duplicate ids first — outcome ids are globally unique (Req 2F.4); any
17
+ * repeated id yields a `duplicate-id` finding (an error).
18
+ * 2. Each unordered pair is evaluated exactly once via `computeVerdict`:
19
+ * - COLLISION + mutually acknowledged (`related`) -> `acknowledged-overlap`
20
+ * (Req 2E.4) — not an error.
21
+ * - COLLISION non-acknowledged -> `collision` (error), with both
22
+ * normalized shapes and the keyword Jaccard score attached (Req 2F.2).
23
+ * - AMBIGUOUS -> `ambiguous` (warning only).
24
+ * - CLEAN -> no finding.
25
+ *
26
+ * `hasErrors` is true when any `collision` or `duplicate-id` finding exists.
27
+ *
28
+ * See ADR-0041 for the architectural rationale.
29
+ */
30
+
31
+ import type { Outcome } from "../schemas";
32
+ import {
33
+ computeVerdict,
34
+ DEFAULT_KEYWORD_THRESHOLD,
35
+ isAcknowledged,
36
+ jaccardSimilarity,
37
+ tokenizeKeywords,
38
+ } from "./collision";
39
+ import { normalizeShape } from "./normalize";
40
+
41
+ /** A single outcome paired with the name of the artifact that declared it. */
42
+ export interface OutcomeRef {
43
+ outcome: Outcome;
44
+ artifactName: string;
45
+ }
46
+
47
+ /** The kinds of finding the registry check can produce. */
48
+ export type CollisionKind =
49
+ | "collision"
50
+ | "ambiguous"
51
+ | "duplicate-id"
52
+ | "acknowledged-overlap";
53
+
54
+ /**
55
+ * A finding about a pair of outcome references. `collision` and `duplicate-id`
56
+ * are errors; `ambiguous` is a warning; `acknowledged-overlap` is informational.
57
+ * The normalized shapes and Jaccard score are attached for `collision` and
58
+ * `ambiguous` findings to support actionable diagnostics (Req 2F.2).
59
+ */
60
+ export interface CollisionFinding {
61
+ kind: CollisionKind;
62
+ a: OutcomeRef;
63
+ b: OutcomeRef;
64
+ /** Normalized input shape, for collision/ambiguous findings. */
65
+ inputShape?: string;
66
+ /** Normalized output shape, for collision/ambiguous findings. */
67
+ outputShape?: string;
68
+ /** Keyword Jaccard similarity, for collision/ambiguous findings. */
69
+ jaccard?: number;
70
+ }
71
+
72
+ /** The outcome of a full registry check over a set of outcome references. */
73
+ export interface RegistryReport {
74
+ findings: CollisionFinding[];
75
+ /** True when any non-acknowledged collision or duplicate id was found. */
76
+ hasErrors: boolean;
77
+ }
78
+
79
+ /**
80
+ * Flatten the outcomes declared by a set of artifacts into attributed
81
+ * references (Req 2F.1 / 2G.1). Preserves artifact order and, within each
82
+ * artifact, outcome declaration order. Pure.
83
+ */
84
+ export function aggregateOutcomes(
85
+ artifacts: Array<{ name: string; outcomes: Outcome[] }>,
86
+ ): OutcomeRef[] {
87
+ const refs: OutcomeRef[] = [];
88
+ for (const artifact of artifacts) {
89
+ for (const outcome of artifact.outcomes) {
90
+ refs.push({ outcome, artifactName: artifact.name });
91
+ }
92
+ }
93
+ return refs;
94
+ }
95
+
96
+ /** Keyword Jaccard score between two outcomes (mirrors Tier-2 input). */
97
+ function keywordJaccard(a: Outcome, b: Outcome): number {
98
+ return jaccardSimilarity(
99
+ tokenizeKeywords(a.keywords),
100
+ tokenizeKeywords(b.keywords),
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Run duplicate-id detection and pairwise collision detection over all refs.
106
+ * Pure and deterministic (Req 2F). Duplicate ids are detected first (globally
107
+ * unique ids, Req 2F.4); then each unordered pair is evaluated exactly once.
108
+ *
109
+ * @param refs attributed outcome references (see `aggregateOutcomes`).
110
+ * @param threshold Tier-2 keyword Jaccard threshold (default 0.4).
111
+ */
112
+ export function runRegistryCheck(
113
+ refs: OutcomeRef[],
114
+ threshold: number = DEFAULT_KEYWORD_THRESHOLD,
115
+ ): RegistryReport {
116
+ const findings: CollisionFinding[] = [];
117
+ let hasErrors = false;
118
+
119
+ // (1) Duplicate-id detection first (Req 2F.4). IDs are globally unique, so
120
+ // any two refs sharing an id are reported as a duplicate-id error. Every
121
+ // unordered pair of refs with the same id is reported once.
122
+ for (let i = 0; i < refs.length; i++) {
123
+ for (let j = i + 1; j < refs.length; j++) {
124
+ if (refs[i].outcome.id === refs[j].outcome.id) {
125
+ findings.push({ kind: "duplicate-id", a: refs[i], b: refs[j] });
126
+ hasErrors = true;
127
+ }
128
+ }
129
+ }
130
+
131
+ // (2) Pairwise collision detection over each unordered pair, evaluated once.
132
+ for (let i = 0; i < refs.length; i++) {
133
+ for (let j = i + 1; j < refs.length; j++) {
134
+ const a = refs[i].outcome;
135
+ const b = refs[j].outcome;
136
+ const verdict = computeVerdict(a, b, threshold);
137
+
138
+ if (verdict === "COLLISION") {
139
+ if (isAcknowledged(a, b)) {
140
+ // Mutually acknowledged overlap is intentional (Req 2E.4) — not
141
+ // an error.
142
+ findings.push({
143
+ kind: "acknowledged-overlap",
144
+ a: refs[i],
145
+ b: refs[j],
146
+ });
147
+ } else {
148
+ findings.push({
149
+ kind: "collision",
150
+ a: refs[i],
151
+ b: refs[j],
152
+ inputShape: normalizeShape(a.inputShape),
153
+ outputShape: normalizeShape(a.outputShape),
154
+ jaccard: keywordJaccard(a, b),
155
+ });
156
+ hasErrors = true;
157
+ }
158
+ } else if (verdict === "AMBIGUOUS") {
159
+ findings.push({
160
+ kind: "ambiguous",
161
+ a: refs[i],
162
+ b: refs[j],
163
+ inputShape: normalizeShape(a.inputShape),
164
+ outputShape: normalizeShape(a.outputShape),
165
+ jaccard: keywordJaccard(a, b),
166
+ });
167
+ }
168
+ // CLEAN -> no finding.
169
+ }
170
+ }
171
+
172
+ return { findings, hasErrors };
173
+ }