@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,712 @@
1
+ /**
2
+ * Rosetta Stone — Transactional Translation Registry
3
+ *
4
+ * Provides `TranslationRegistryBuilder` for atomic format contract registration
5
+ * and `TranslationRegistrySnapshot` as a deeply-frozen immutable query surface.
6
+ *
7
+ * CONSTRAINTS:
8
+ * - NO filesystem, process, clock, random, Git, or network imports
9
+ * - ALL public data shapes remain owned by `src/schemas.ts` — only re-export, never redefine
10
+ * - Pure functions only
11
+ *
12
+ * Requirements: 2.1, 2.2, 2.3, 2.5, 2.7, 15.1, 15.4, 15.5
13
+ */
14
+
15
+ import type {
16
+ Direction,
17
+ FormatContract,
18
+ FormatIdentifier,
19
+ JsonValue,
20
+ LifecycleStatus,
21
+ NormalizedRelativePath,
22
+ RegistryFailure,
23
+ SourceDocument,
24
+ TranslationDiagnostic,
25
+ } from "../schemas";
26
+
27
+ import { codePointCompare, deepFreeze } from "./contracts";
28
+ import { createDiagnostic, createRegistryFailure } from "./diagnostics";
29
+ import type { ImmutableTemplateBundle } from "./templates";
30
+
31
+ // ═══════════════════════════════════════════════════════════════════════════════
32
+ // Translator Function Type Aliases
33
+ // ═══════════════════════════════════════════════════════════════════════════════
34
+
35
+ /**
36
+ * Context supplied to source translators during inbound translation.
37
+ */
38
+ export interface SourceTranslatorContext {
39
+ readonly format: FormatContract;
40
+ readonly canonicalSchemaVersion: string;
41
+ readonly options: Readonly<Record<string, JsonValue>>;
42
+ readonly callerContext: Readonly<Record<string, JsonValue>>;
43
+ }
44
+
45
+ /**
46
+ * Output from a source translator: a candidate artifact and diagnostics.
47
+ */
48
+ export interface SourceTranslationOutput {
49
+ candidate?: Record<string, unknown>;
50
+ diagnostics: readonly TranslationDiagnostic[];
51
+ consumedPaths: readonly NormalizedRelativePath[];
52
+ preservedPaths: readonly NormalizedRelativePath[];
53
+ }
54
+
55
+ /**
56
+ * A pure source translator that converts in-memory documents into a canonical candidate.
57
+ */
58
+ export type SourceTranslator = (
59
+ documents: readonly SourceDocument[],
60
+ context: SourceTranslatorContext,
61
+ ) => SourceTranslationOutput;
62
+
63
+ /**
64
+ * Output from a pretty-printer: source-formatted documents and diagnostics.
65
+ */
66
+ export interface SourcePrintOutput {
67
+ documents: readonly SourceDocument[];
68
+ diagnostics: readonly TranslationDiagnostic[];
69
+ }
70
+
71
+ /**
72
+ * A pure pretty-printer that renders a canonical artifact back into source format.
73
+ */
74
+ export type PrettyPrinter = (
75
+ artifact: Record<string, unknown>,
76
+ context: SourceTranslatorContext,
77
+ ) => SourcePrintOutput;
78
+
79
+ /**
80
+ * Context supplied to target translators during outbound translation.
81
+ */
82
+ export interface TargetTranslatorContext {
83
+ readonly format: FormatContract;
84
+ readonly variant: string;
85
+ readonly canonicalSchemaVersion: string;
86
+ readonly options: Readonly<Record<string, JsonValue>>;
87
+ readonly callerContext: Readonly<Record<string, JsonValue>>;
88
+ readonly templates: ImmutableTemplateBundle;
89
+ }
90
+
91
+ /**
92
+ * A pure target translator that converts a canonical artifact into a translation plan.
93
+ */
94
+ export type TargetTranslator = (
95
+ artifact: Record<string, unknown>,
96
+ context: TargetTranslatorContext,
97
+ ) => TargetTranslationOutput;
98
+
99
+ /**
100
+ * Output from a target translator: plan files and diagnostics.
101
+ */
102
+ export interface TargetTranslationOutput {
103
+ plan: Record<string, unknown>;
104
+ diagnostics: readonly TranslationDiagnostic[];
105
+ degradations: readonly Record<string, unknown>[];
106
+ }
107
+
108
+ // ═══════════════════════════════════════════════════════════════════════════════
109
+ // Registry Extension
110
+ // ═══════════════════════════════════════════════════════════════════════════════
111
+
112
+ /**
113
+ * A bundle of a format contract with optional translator implementations
114
+ * registered by trusted host code.
115
+ */
116
+ export interface RegistryExtension {
117
+ readonly contract: FormatContract;
118
+ readonly sourceTranslator?: SourceTranslator;
119
+ readonly prettyPrinter?: PrettyPrinter;
120
+ readonly targetTranslator?: TargetTranslator;
121
+ }
122
+
123
+ // ═══════════════════════════════════════════════════════════════════════════════
124
+ // Registration Result
125
+ // ═══════════════════════════════════════════════════════════════════════════════
126
+
127
+ export type RegistrationResult =
128
+ | { ok: true; contract: FormatContract }
129
+ | { ok: false; diagnostics: TranslationDiagnostic[] }
130
+ | { ok: false; registryFailure: RegistryFailure };
131
+
132
+ // ═══════════════════════════════════════════════════════════════════════════════
133
+ // Registry Query and Resolution Types
134
+ // ═══════════════════════════════════════════════════════════════════════════════
135
+
136
+ /**
137
+ * Requested direction for format resolution. Includes "any" for queries
138
+ * that do not restrict direction.
139
+ */
140
+ export type RequestedDirection = Direction | "any";
141
+
142
+ /**
143
+ * Query parameters for listing contracts from the snapshot.
144
+ */
145
+ export interface RegistryQuery {
146
+ readonly direction?: RequestedDirection;
147
+ readonly harness?: string | null;
148
+ readonly lifecycle?: readonly LifecycleStatus[];
149
+ }
150
+
151
+ /**
152
+ * Result of resolving an identifier or alias against a direction.
153
+ */
154
+ export type FormatResolution =
155
+ | {
156
+ ok: true;
157
+ contract: FormatContract;
158
+ diagnostics: TranslationDiagnostic[];
159
+ }
160
+ | {
161
+ ok: false;
162
+ diagnostics: TranslationDiagnostic[];
163
+ };
164
+
165
+ // ═══════════════════════════════════════════════════════════════════════════════
166
+ // Supported Contract Version
167
+ // ═══════════════════════════════════════════════════════════════════════════════
168
+
169
+ const SUPPORTED_CONTRACT_VERSION = "1.0" as const;
170
+
171
+ // ═══════════════════════════════════════════════════════════════════════════════
172
+ // TranslationRegistryBuilder
173
+ // ═══════════════════════════════════════════════════════════════════════════════
174
+
175
+ /**
176
+ * Mutable builder that registers format contracts atomically and produces
177
+ * an immutable `TranslationRegistrySnapshot` on `freeze()`.
178
+ */
179
+ export interface TranslationRegistryBuilder {
180
+ register(extension: RegistryExtension): RegistrationResult;
181
+ freeze(): TranslationRegistrySnapshot;
182
+ }
183
+
184
+ /**
185
+ * Options for format resolution.
186
+ */
187
+ export interface ResolveOptions {
188
+ /** Allow retired formats to resolve (for migration-only use cases). */
189
+ readonly allowRetired?: boolean;
190
+ }
191
+
192
+ /**
193
+ * Immutable, deeply-frozen snapshot of registered format contracts
194
+ * and their translator implementations.
195
+ */
196
+ export interface TranslationRegistrySnapshot {
197
+ readonly version: string;
198
+ readonly registrationCount: number;
199
+ listContracts(query?: RegistryQuery): readonly FormatContract[];
200
+ resolve(
201
+ identifierOrAlias: string,
202
+ direction: RequestedDirection,
203
+ options?: ResolveOptions,
204
+ ): FormatResolution;
205
+ getSourceTranslator(id: FormatIdentifier): SourceTranslator | undefined;
206
+ getPrettyPrinter(id: FormatIdentifier): PrettyPrinter | undefined;
207
+ getTargetTranslator(id: FormatIdentifier): TargetTranslator | undefined;
208
+ getAliasHistory(): ReadonlyMap<string, FormatIdentifier>;
209
+ }
210
+
211
+ // ═══════════════════════════════════════════════════════════════════════════════
212
+ // Internal Builder State
213
+ // ═══════════════════════════════════════════════════════════════════════════════
214
+
215
+ interface InternalRegistryEntry {
216
+ contract: FormatContract;
217
+ sourceTranslator?: SourceTranslator;
218
+ prettyPrinter?: PrettyPrinter;
219
+ targetTranslator?: TargetTranslator;
220
+ }
221
+
222
+ // ═══════════════════════════════════════════════════════════════════════════════
223
+ // Builder Implementation
224
+ // ═══════════════════════════════════════════════════════════════════════════════
225
+
226
+ /**
227
+ * Create a new `TranslationRegistryBuilder` instance.
228
+ *
229
+ * @param version - The version string for the resulting snapshot (e.g. "1.0.0")
230
+ */
231
+ export function createRegistryBuilder(
232
+ version: string,
233
+ ): TranslationRegistryBuilder {
234
+ // Mutable builder state — only mutated on successful registration
235
+ const entries: Map<FormatIdentifier, InternalRegistryEntry> = new Map();
236
+ const aliasIndex: Map<string, FormatIdentifier> = new Map();
237
+ // Track alias registrations in order for history snapshots
238
+ const aliasHistory: Map<string, FormatIdentifier> = new Map();
239
+ let frozen = false;
240
+
241
+ function register(extension: RegistryExtension): RegistrationResult {
242
+ if (frozen) {
243
+ return {
244
+ ok: false,
245
+ registryFailure: createRegistryFailure(
246
+ "Cannot register after the registry has been frozen.",
247
+ ),
248
+ };
249
+ }
250
+
251
+ // Wrap the entire validation in a try/catch so that if diagnostic
252
+ // construction itself fails, we return a RegistryFailure.
253
+ try {
254
+ return validateAndRegister(extension);
255
+ } catch {
256
+ return {
257
+ ok: false,
258
+ registryFailure: createRegistryFailure(
259
+ "Registration failed: diagnostic construction unavailable.",
260
+ ),
261
+ };
262
+ }
263
+ }
264
+
265
+ function validateAndRegister(
266
+ extension: RegistryExtension,
267
+ ): RegistrationResult {
268
+ const { contract } = extension;
269
+ const diagnostics: TranslationDiagnostic[] = [];
270
+
271
+ // --- Contract version check ---
272
+ if (contract.contractVersion !== SUPPORTED_CONTRACT_VERSION) {
273
+ diagnostics.push(
274
+ createDiagnostic("RS_INVALID_CONTRACT", {
275
+ formatId: contract.id,
276
+ message: `Unsupported contract version "${contract.contractVersion}". Only "${SUPPORTED_CONTRACT_VERSION}" is supported.`,
277
+ }),
278
+ );
279
+ }
280
+
281
+ // --- Duplicate identifier check ---
282
+ if (entries.has(contract.id)) {
283
+ diagnostics.push(
284
+ createDiagnostic("RS_REGISTRATION_FAILED", {
285
+ formatId: contract.id,
286
+ message: `Duplicate format identifier "${contract.id}" is already registered.`,
287
+ }),
288
+ );
289
+ }
290
+
291
+ // --- Internal alias uniqueness (no duplicate aliases within same registration) ---
292
+ const seenAliases = new Set<string>();
293
+ for (const alias of contract.aliases) {
294
+ if (seenAliases.has(alias)) {
295
+ diagnostics.push(
296
+ createDiagnostic("RS_INVALID_CONTRACT", {
297
+ formatId: contract.id,
298
+ message: `Duplicate alias "${alias}" within the same registration.`,
299
+ }),
300
+ );
301
+ }
302
+ seenAliases.add(alias);
303
+ }
304
+
305
+ // --- Alias checks: duplicates against existing identifiers and aliases ---
306
+ for (const alias of contract.aliases) {
307
+ if (entries.has(alias as FormatIdentifier)) {
308
+ diagnostics.push(
309
+ createDiagnostic("RS_REGISTRATION_FAILED", {
310
+ formatId: contract.id,
311
+ message: `Alias "${alias}" conflicts with an existing format identifier.`,
312
+ }),
313
+ );
314
+ } else if (aliasIndex.has(alias)) {
315
+ const existingOwner = aliasIndex.get(alias) ?? alias;
316
+ diagnostics.push(
317
+ createDiagnostic("RS_REGISTRATION_FAILED", {
318
+ formatId: contract.id,
319
+ message: `Alias "${alias}" is already registered by format "${existingOwner}".`,
320
+ }),
321
+ );
322
+ }
323
+ // An alias that matches the contract's own id is a self-reference (valid)
324
+ // but an alias that matches another alias in the same registration is fine too.
325
+ }
326
+
327
+ // --- Check alias does not duplicate the registrant's own id across existing aliases ---
328
+ if (aliasIndex.has(contract.id)) {
329
+ const existingOwner = aliasIndex.get(contract.id) ?? contract.id;
330
+ diagnostics.push(
331
+ createDiagnostic("RS_REGISTRATION_FAILED", {
332
+ formatId: contract.id,
333
+ message: `Format identifier "${contract.id}" conflicts with an alias registered by "${existingOwner}".`,
334
+ }),
335
+ );
336
+ }
337
+
338
+ // --- Default variant validation ---
339
+ if (contract.defaultVariant !== undefined) {
340
+ const variantKeys = Object.keys(contract.variants);
341
+ if (!variantKeys.includes(contract.defaultVariant)) {
342
+ diagnostics.push(
343
+ createDiagnostic("RS_INVALID_CONTRACT", {
344
+ formatId: contract.id,
345
+ message: `Default variant "${contract.defaultVariant}" is not declared in variants.`,
346
+ }),
347
+ );
348
+ }
349
+ }
350
+
351
+ // --- Variant ID consistency: each variant's id must match its key ---
352
+ for (const [key, variant] of Object.entries(contract.variants)) {
353
+ if (variant.id !== key) {
354
+ diagnostics.push(
355
+ createDiagnostic("RS_INVALID_CONTRACT", {
356
+ formatId: contract.id,
357
+ message: `Variant key "${key}" does not match its declared id "${variant.id}".`,
358
+ }),
359
+ );
360
+ }
361
+ }
362
+
363
+ // --- Detection rules non-empty for source-capable contracts ---
364
+ const direction = contract.direction;
365
+ if (
366
+ (direction === "source" || direction === "bidirectional") &&
367
+ contract.detection.rules.length === 0
368
+ ) {
369
+ diagnostics.push(
370
+ createDiagnostic("RS_INVALID_CONTRACT", {
371
+ formatId: contract.id,
372
+ message: `Source-capable contract must have at least one detection rule.`,
373
+ }),
374
+ );
375
+ }
376
+
377
+ // --- Normalization rules must have unique IDs ---
378
+ const normRuleIds = new Set<string>();
379
+ for (const rule of contract.normalizationRules) {
380
+ if (normRuleIds.has(rule.id)) {
381
+ diagnostics.push(
382
+ createDiagnostic("RS_INVALID_CONTRACT", {
383
+ formatId: contract.id,
384
+ message: `Duplicate normalization rule id "${rule.id}".`,
385
+ }),
386
+ );
387
+ }
388
+ normRuleIds.add(rule.id);
389
+ }
390
+
391
+ // --- Option definitions: enum values must be non-empty when type is "enum" ---
392
+ for (const [optionKey, optionDef] of Object.entries(
393
+ contract.optionDefinitions,
394
+ )) {
395
+ if (optionDef.type === "enum") {
396
+ if (!optionDef.enumValues || optionDef.enumValues.length === 0) {
397
+ diagnostics.push(
398
+ createDiagnostic("RS_INVALID_CONTRACT", {
399
+ formatId: contract.id,
400
+ message: `Option "${optionKey}" has type "enum" but declares no enum values.`,
401
+ }),
402
+ );
403
+ }
404
+ }
405
+ }
406
+
407
+ // --- Compatibility profile completeness is already enforced by Zod schema ---
408
+ // (RosettaCompatibilityProfileSchema has a refinement checking every capability)
409
+
410
+ // --- Direction-implied translator presence ---
411
+ if (
412
+ (direction === "source" || direction === "bidirectional") &&
413
+ !extension.sourceTranslator
414
+ ) {
415
+ diagnostics.push(
416
+ createDiagnostic("RS_INVALID_CONTRACT", {
417
+ formatId: contract.id,
418
+ message: `Direction "${direction}" requires a source translator, but none was provided.`,
419
+ }),
420
+ );
421
+ }
422
+ if (
423
+ (direction === "target" || direction === "bidirectional") &&
424
+ !extension.targetTranslator
425
+ ) {
426
+ diagnostics.push(
427
+ createDiagnostic("RS_INVALID_CONTRACT", {
428
+ formatId: contract.id,
429
+ message: `Direction "${direction}" requires a target translator, but none was provided.`,
430
+ }),
431
+ );
432
+ }
433
+
434
+ // --- If any diagnostics were collected, reject without mutating state ---
435
+ if (diagnostics.length > 0) {
436
+ return { ok: false, diagnostics };
437
+ }
438
+
439
+ // --- Atomic commit: all checks passed, now mutate builder state ---
440
+ entries.set(contract.id, {
441
+ contract,
442
+ sourceTranslator: extension.sourceTranslator,
443
+ prettyPrinter: extension.prettyPrinter,
444
+ targetTranslator: extension.targetTranslator,
445
+ });
446
+
447
+ for (const alias of contract.aliases) {
448
+ aliasIndex.set(alias, contract.id);
449
+ aliasHistory.set(alias, contract.id);
450
+ }
451
+
452
+ return { ok: true, contract };
453
+ }
454
+
455
+ function freeze(): TranslationRegistrySnapshot {
456
+ frozen = true;
457
+ return createSnapshot(version, entries, aliasIndex, aliasHistory);
458
+ }
459
+
460
+ return { register, freeze };
461
+ }
462
+
463
+ // ═══════════════════════════════════════════════════════════════════════════════
464
+ // Snapshot Implementation
465
+ // ═══════════════════════════════════════════════════════════════════════════════
466
+
467
+ function createSnapshot(
468
+ version: string,
469
+ entries: Map<FormatIdentifier, InternalRegistryEntry>,
470
+ aliasIndex: Map<string, FormatIdentifier>,
471
+ aliasHistory: Map<string, FormatIdentifier>,
472
+ ): TranslationRegistrySnapshot {
473
+ // Build sorted contract list once (deterministic by FormatIdentifier)
474
+ const sortedIds = [...entries.keys()].sort(codePointCompare);
475
+ const frozenContracts: FormatContract[] = sortedIds
476
+ .map((id) => entries.get(id))
477
+ .filter((entry): entry is InternalRegistryEntry => entry !== undefined)
478
+ .map((entry) => deepFreeze(structuredClone(entry.contract)));
479
+
480
+ // Build lookup maps for translators (not frozen — functions can't be frozen)
481
+ const sourceTranslators = new Map<FormatIdentifier, SourceTranslator>();
482
+ const prettyPrinters = new Map<FormatIdentifier, PrettyPrinter>();
483
+ const targetTranslators = new Map<FormatIdentifier, TargetTranslator>();
484
+
485
+ for (const [id, entry] of entries) {
486
+ if (entry.sourceTranslator) {
487
+ sourceTranslators.set(id, entry.sourceTranslator);
488
+ }
489
+ if (entry.prettyPrinter) {
490
+ prettyPrinters.set(id, entry.prettyPrinter);
491
+ }
492
+ if (entry.targetTranslator) {
493
+ targetTranslators.set(id, entry.targetTranslator);
494
+ }
495
+ }
496
+
497
+ // Freeze alias index into a plain object for lookup
498
+ const frozenAliasMap = new Map<string, FormatIdentifier>(aliasIndex);
499
+
500
+ // Freeze alias history for snapshot reporting
501
+ const frozenAliasHistory = new Map<string, FormatIdentifier>(aliasHistory);
502
+
503
+ // Registration count is the number of successfully registered contracts
504
+ const registrationCount = entries.size;
505
+
506
+ function listContracts(query?: RegistryQuery): readonly FormatContract[] {
507
+ if (!query) {
508
+ return frozenContracts;
509
+ }
510
+
511
+ return frozenContracts.filter((contract) => {
512
+ // Direction filter
513
+ if (query.direction && query.direction !== "any") {
514
+ if (!directionMatches(contract.direction, query.direction)) {
515
+ return false;
516
+ }
517
+ }
518
+
519
+ // Harness filter
520
+ if (query.harness !== undefined) {
521
+ if (query.harness === null) {
522
+ if (contract.harness !== null) return false;
523
+ } else {
524
+ if (contract.harness !== query.harness) return false;
525
+ }
526
+ }
527
+
528
+ // Lifecycle filter
529
+ if (query.lifecycle && query.lifecycle.length > 0) {
530
+ if (!query.lifecycle.includes(contract.lifecycle.status)) {
531
+ return false;
532
+ }
533
+ }
534
+
535
+ return true;
536
+ });
537
+ }
538
+
539
+ function resolve(
540
+ identifierOrAlias: string,
541
+ direction: RequestedDirection,
542
+ options?: ResolveOptions,
543
+ ): FormatResolution {
544
+ // Resolve the identifier: direct lookup first, then alias lookup
545
+ let formatId: FormatIdentifier | undefined;
546
+
547
+ if (entries.has(identifierOrAlias as FormatIdentifier)) {
548
+ formatId = identifierOrAlias as FormatIdentifier;
549
+ } else if (frozenAliasMap.has(identifierOrAlias)) {
550
+ formatId = frozenAliasMap.get(identifierOrAlias);
551
+ }
552
+
553
+ if (!formatId) {
554
+ return {
555
+ ok: false,
556
+ diagnostics: [
557
+ createDiagnostic("RS_REGISTRATION_FAILED", {
558
+ formatId: identifierOrAlias,
559
+ message: `Format "${identifierOrAlias}" is not registered.`,
560
+ }),
561
+ ],
562
+ };
563
+ }
564
+
565
+ const entry = entries.get(formatId);
566
+ if (!entry) {
567
+ return {
568
+ ok: false,
569
+ diagnostics: [
570
+ createDiagnostic("RS_REGISTRATION_FAILED", {
571
+ formatId,
572
+ message: `Format "${formatId}" could not be resolved.`,
573
+ }),
574
+ ],
575
+ };
576
+ }
577
+ const contract = entry.contract;
578
+ const diagnostics: TranslationDiagnostic[] = [];
579
+
580
+ // Lifecycle checks
581
+ const lifecycle = contract.lifecycle;
582
+ if (lifecycle.status === "retired") {
583
+ if (options?.allowRetired) {
584
+ // Migration-only: resolve with a warning instead of an error
585
+ diagnostics.push(
586
+ createDiagnostic("RS_LIFECYCLE_DEPRECATED", {
587
+ formatId: contract.id,
588
+ message: `Format "${contract.id}" has lifecycle status "retired" (resolved with allowRetired).`,
589
+ remediation: lifecycle.replacement
590
+ ? `Migrate to "${lifecycle.replacement}".`
591
+ : "No replacement is declared.",
592
+ }),
593
+ );
594
+ } else {
595
+ return {
596
+ ok: false,
597
+ diagnostics: [
598
+ createDiagnostic("RS_REGISTRATION_FAILED", {
599
+ formatId: contract.id,
600
+ message: `Format "${contract.id}" has lifecycle status "retired" and is not selectable.`,
601
+ remediation: lifecycle.replacement
602
+ ? `Migrate to "${lifecycle.replacement}".`
603
+ : "No replacement is declared.",
604
+ }),
605
+ ],
606
+ };
607
+ }
608
+ }
609
+
610
+ if (lifecycle.status === "deprecated") {
611
+ diagnostics.push(
612
+ createDiagnostic("RS_LIFECYCLE_DEPRECATED", {
613
+ formatId: contract.id,
614
+ message: `Format "${contract.id}" is deprecated.`,
615
+ remediation: lifecycle.replacement
616
+ ? `Migrate to "${lifecycle.replacement}".`
617
+ : "No replacement is declared.",
618
+ }),
619
+ );
620
+ }
621
+
622
+ if (lifecycle.status === "experimental") {
623
+ diagnostics.push(
624
+ createDiagnostic("RS_LIFECYCLE_DEPRECATED", {
625
+ formatId: contract.id,
626
+ message: `Format "${contract.id}" has lifecycle status "experimental".`,
627
+ severityOverride: "info",
628
+ }),
629
+ );
630
+ }
631
+
632
+ // Direction check
633
+ if (direction !== "any") {
634
+ if (!directionMatches(contract.direction, direction)) {
635
+ return {
636
+ ok: false,
637
+ diagnostics: [
638
+ createDiagnostic("RS_DIRECTION_MISMATCH", {
639
+ formatId: contract.id,
640
+ message: `Format "${contract.id}" declares direction "${contract.direction}" but "${direction}" was requested.`,
641
+ }),
642
+ ],
643
+ };
644
+ }
645
+ }
646
+
647
+ // Return frozen clone of contract
648
+ const frozenContract = frozenContracts.find((c) => c.id === formatId);
649
+ if (!frozenContract) {
650
+ return {
651
+ ok: false,
652
+ diagnostics: [
653
+ createDiagnostic("RS_REGISTRATION_FAILED", {
654
+ formatId,
655
+ message: `Format "${formatId}" could not be resolved from snapshot.`,
656
+ }),
657
+ ],
658
+ };
659
+ }
660
+
661
+ return { ok: true, contract: frozenContract, diagnostics };
662
+ }
663
+
664
+ function getSourceTranslator(
665
+ id: FormatIdentifier,
666
+ ): SourceTranslator | undefined {
667
+ return sourceTranslators.get(id);
668
+ }
669
+
670
+ function getPrettyPrinter(id: FormatIdentifier): PrettyPrinter | undefined {
671
+ return prettyPrinters.get(id);
672
+ }
673
+
674
+ function getTargetTranslator(
675
+ id: FormatIdentifier,
676
+ ): TargetTranslator | undefined {
677
+ return targetTranslators.get(id);
678
+ }
679
+
680
+ function getAliasHistory(): ReadonlyMap<string, FormatIdentifier> {
681
+ return frozenAliasHistory;
682
+ }
683
+
684
+ const snapshot: TranslationRegistrySnapshot = {
685
+ version,
686
+ registrationCount,
687
+ listContracts,
688
+ resolve,
689
+ getSourceTranslator,
690
+ getPrettyPrinter,
691
+ getTargetTranslator,
692
+ getAliasHistory,
693
+ };
694
+
695
+ return Object.freeze(snapshot);
696
+ }
697
+
698
+ // ═══════════════════════════════════════════════════════════════════════════════
699
+ // Direction Matching Helper
700
+ // ═══════════════════════════════════════════════════════════════════════════════
701
+
702
+ /**
703
+ * Check whether a contract's declared direction satisfies a requested direction.
704
+ * "bidirectional" satisfies both "source" and "target" requests.
705
+ */
706
+ function directionMatches(
707
+ contractDirection: Direction,
708
+ requested: Direction,
709
+ ): boolean {
710
+ if (contractDirection === "bidirectional") return true;
711
+ return contractDirection === requested;
712
+ }