@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,717 @@
1
+ /**
2
+ * Rosetta Stone — CLI Command Handlers
3
+ *
4
+ * Commander-based handlers for the `kanon rosetta` command namespace:
5
+ * - formats: List all registered format contracts
6
+ * - detect: Detect source format for a given path
7
+ * - inspect: Dry-run inspection of a translation
8
+ * - translate: Execute a translation
9
+ *
10
+ * Each handler validates direction, supports named translation profiles,
11
+ * routes to inbound/outbound/transcode mode, and supports --dry-run,
12
+ * --strict, --variant, and --json flags.
13
+ *
14
+ * Requirements: 2.8, 10.1, 10.2, 10.3
15
+ */
16
+
17
+ import { dirname, resolve } from "node:path";
18
+ import chalk from "chalk";
19
+ import type { Command } from "commander";
20
+ import { loadForgeConfig } from "./config";
21
+ import { registerBackfillCommand } from "./provenance-backfill-cli";
22
+ import { getSharedEngine, getSharedRegistry } from "./rosetta/engine-bootstrap";
23
+ import {
24
+ type JsonRenderOptions,
25
+ renderHuman,
26
+ renderJson,
27
+ type TranslationRegistrySnapshot,
28
+ } from "./rosetta/index";
29
+ import { registerProfilesCommands } from "./rosetta-profiles-cli";
30
+ import type {
31
+ FormatContract,
32
+ SourceDocument,
33
+ TranslationProfile,
34
+ TranslationRequest,
35
+ } from "./schemas";
36
+ import {
37
+ readArtifactDocuments,
38
+ resolveAllowedRoot,
39
+ } from "./translation-orchestrator";
40
+
41
+ // ═══════════════════════════════════════════════════════════════════════════════
42
+ // Registry and Engine Bootstrap (lazy, cached — shared with the import facade)
43
+ // ═══════════════════════════════════════════════════════════════════════════════
44
+
45
+ /**
46
+ * Registry accessor. Delegates to the shared engine bootstrap so this command
47
+ * namespace and the `kanon import` facade resolve against one registry snapshot
48
+ * (ADR-0065). Kept as a local alias for the call sites that read
49
+ * `registry.version` / pass `registrySnapshot`.
50
+ */
51
+ function getRegistry(): TranslationRegistrySnapshot {
52
+ return getSharedRegistry();
53
+ }
54
+
55
+ // ═══════════════════════════════════════════════════════════════════════════════
56
+ // Source Document Loading
57
+ // ═══════════════════════════════════════════════════════════════════════════════
58
+
59
+ /**
60
+ * Load source documents from a given path for use with the Rosetta Stone engine.
61
+ * Resolves the path as an allowed root and reads all files in the artifact directory.
62
+ */
63
+ async function loadSourceDocuments(
64
+ sourcePath: string,
65
+ ): Promise<SourceDocument[]> {
66
+ const resolvedPath = resolve(sourcePath);
67
+
68
+ // The parent directory serves as the allowed root for containment
69
+ const parentDir = dirname(resolvedPath);
70
+ const root = await resolveAllowedRoot(parentDir, "cli-source");
71
+
72
+ const group = await readArtifactDocuments(resolvedPath, root);
73
+ return group.documents;
74
+ }
75
+
76
+ // ═══════════════════════════════════════════════════════════════════════════════
77
+ // Direction Inference
78
+ // ═══════════════════════════════════════════════════════════════════════════════
79
+
80
+ interface DirectionResult {
81
+ mode: "inbound" | "outbound" | "transcode";
82
+ sourceFormatId?: string;
83
+ targetFormatId?: string;
84
+ }
85
+
86
+ /**
87
+ * Determine translation direction from --from and --to flags.
88
+ * - --from only → inbound
89
+ * - --to only → outbound
90
+ * - both --from and --to → transcode
91
+ * - neither → error
92
+ */
93
+ function resolveDirection(
94
+ from?: string,
95
+ to?: string,
96
+ ): DirectionResult | string {
97
+ if (from && to) {
98
+ return { mode: "transcode", sourceFormatId: from, targetFormatId: to };
99
+ }
100
+ if (from && !to) {
101
+ return { mode: "inbound", sourceFormatId: from };
102
+ }
103
+ if (!from && to) {
104
+ return { mode: "outbound", targetFormatId: to };
105
+ }
106
+ return "Direction is required: use --from for inbound, --to for outbound, or both for transcode.";
107
+ }
108
+
109
+ // ═══════════════════════════════════════════════════════════════════════════════
110
+ // Profile Resolution
111
+ // ═══════════════════════════════════════════════════════════════════════════════
112
+
113
+ interface ResolvedOptions {
114
+ from?: string;
115
+ to?: string;
116
+ variant?: string;
117
+ strict: boolean;
118
+ options: Record<string, unknown>;
119
+ }
120
+
121
+ /**
122
+ * Merge a named translation profile with explicit CLI flags.
123
+ * CLI flags take precedence over profile values.
124
+ */
125
+ function mergeProfileWithFlags(
126
+ profile: TranslationProfile | undefined,
127
+ flags: {
128
+ from?: string;
129
+ to?: string;
130
+ variant?: string;
131
+ strict?: boolean;
132
+ },
133
+ ): ResolvedOptions {
134
+ const resolved: ResolvedOptions = {
135
+ from: flags.from ?? profile?.sourceFormat,
136
+ to: flags.to ?? profile?.targetFormat,
137
+ variant: flags.variant ?? profile?.targetVariant,
138
+ strict: flags.strict ?? profile?.strict ?? false,
139
+ options: profile?.options ?? {},
140
+ };
141
+ return resolved;
142
+ }
143
+
144
+ // ═══════════════════════════════════════════════════════════════════════════════
145
+ // kanon rosetta formats
146
+ // ═══════════════════════════════════════════════════════════════════════════════
147
+
148
+ interface FormatsOptions {
149
+ json?: boolean;
150
+ }
151
+
152
+ /**
153
+ * Handler for `kanon rosetta formats`.
154
+ * Lists all registered format contracts with identifier, direction, harness,
155
+ * aliases, lifecycle, and supported canonical schema versions.
156
+ */
157
+ async function formatsCommand(options: FormatsOptions): Promise<void> {
158
+ const registry = getRegistry();
159
+ const contracts = registry.listContracts();
160
+
161
+ if (options.json) {
162
+ const output = contracts.map(formatContractToJson);
163
+ console.log(JSON.stringify(output, null, 2));
164
+ return;
165
+ }
166
+
167
+ // Human-readable output
168
+ console.log(chalk.bold.cyan("Registered Format Contracts"));
169
+ console.log(chalk.dim("─".repeat(50)));
170
+ console.log();
171
+
172
+ for (const contract of contracts) {
173
+ const lifecycle = contract.lifecycle.status;
174
+ const lifecycleLabel =
175
+ lifecycle === "active"
176
+ ? chalk.green(lifecycle)
177
+ : lifecycle === "deprecated"
178
+ ? chalk.yellow(lifecycle)
179
+ : lifecycle === "experimental"
180
+ ? chalk.blue(lifecycle)
181
+ : chalk.red(lifecycle);
182
+
183
+ console.log(
184
+ ` ${chalk.bold(contract.id)} ${chalk.dim(contract.direction)} ${lifecycleLabel}`,
185
+ );
186
+ if (contract.harness) {
187
+ console.log(` Harness: ${contract.harness}`);
188
+ }
189
+ if (contract.aliases.length > 0) {
190
+ console.log(` Aliases: ${contract.aliases.join(", ")}`);
191
+ }
192
+ const variantIds = Object.keys(contract.variants);
193
+ if (variantIds.length > 0) {
194
+ const defaultVariant = contract.defaultVariant;
195
+ const display = variantIds.map((v) =>
196
+ v === defaultVariant ? `${v} (default)` : v,
197
+ );
198
+ console.log(` Variants: ${display.join(", ")}`);
199
+ }
200
+ console.log(` Schema versions: ${contract.canonicalVersions}`);
201
+ console.log();
202
+ }
203
+ }
204
+
205
+ function formatContractToJson(
206
+ contract: FormatContract,
207
+ ): Record<string, unknown> {
208
+ return {
209
+ formatId: contract.id,
210
+ direction: contract.direction,
211
+ harness: contract.harness ?? null,
212
+ aliases: contract.aliases,
213
+ lifecycle: contract.lifecycle.status,
214
+ contractVersion: contract.contractVersion,
215
+ canonicalVersions: contract.canonicalVersions,
216
+ variants: Object.keys(contract.variants),
217
+ defaultVariant: contract.defaultVariant ?? null,
218
+ };
219
+ }
220
+
221
+ // ═══════════════════════════════════════════════════════════════════════════════
222
+ // kanon rosetta detect
223
+ // ═══════════════════════════════════════════════════════════════════════════════
224
+
225
+ interface DetectOptions {
226
+ format?: string;
227
+ json?: boolean;
228
+ }
229
+
230
+ /**
231
+ * Handler for `kanon rosetta detect <path>`.
232
+ * Detects the source format of documents at the given path.
233
+ */
234
+ async function detectCommand(
235
+ sourcePath: string,
236
+ options: DetectOptions,
237
+ ): Promise<void> {
238
+ const registry = getRegistry();
239
+ const engine = getSharedEngine();
240
+
241
+ const documents = await loadSourceDocuments(sourcePath);
242
+
243
+ if (documents.length === 0) {
244
+ console.error(
245
+ chalk.red("Error: No documents found at the specified path."),
246
+ );
247
+ process.exit(1);
248
+ }
249
+
250
+ const result = engine.detect({
251
+ documents,
252
+ registrySnapshot: registry,
253
+ explicitFormatId: options.format,
254
+ direction: "source",
255
+ });
256
+
257
+ if (options.json) {
258
+ const output = {
259
+ ok: result.ok,
260
+ candidates: result.candidates.map((c) => ({
261
+ formatId: c.formatId,
262
+ confidence: c.confidence,
263
+ evidence: c.evidence,
264
+ })),
265
+ selected: result.ok ? result.selected : null,
266
+ diagnostics: result.diagnostics,
267
+ };
268
+ console.log(JSON.stringify(output, null, 2));
269
+ return;
270
+ }
271
+
272
+ // Human-readable output
273
+ console.log(chalk.bold.cyan("Format Detection"));
274
+ console.log(chalk.dim("─".repeat(40)));
275
+ console.log();
276
+
277
+ if (result.ok) {
278
+ console.log(` Selected: ${chalk.green.bold(result.selected)}`);
279
+ } else {
280
+ console.log(chalk.red(" No unique format match found."));
281
+ }
282
+ console.log();
283
+
284
+ if (result.candidates.length > 0) {
285
+ console.log(chalk.bold(" Candidates:"));
286
+ for (const candidate of result.candidates) {
287
+ const conf = (candidate.confidence * 100).toFixed(0);
288
+ console.log(
289
+ ` ${candidate.formatId} (${conf}% confidence, ${candidate.evidence.length} evidence)`,
290
+ );
291
+ }
292
+ console.log();
293
+ }
294
+
295
+ if (result.diagnostics.length > 0) {
296
+ console.log(chalk.bold(" Diagnostics:"));
297
+ for (const diag of result.diagnostics) {
298
+ const icon =
299
+ diag.severity === "error"
300
+ ? "✗"
301
+ : diag.severity === "warning"
302
+ ? "⚠"
303
+ : "ℹ";
304
+ console.log(` ${icon} [${diag.code}] ${diag.message}`);
305
+ }
306
+ }
307
+
308
+ if (!result.ok) {
309
+ process.exit(1);
310
+ }
311
+ }
312
+
313
+ // ═══════════════════════════════════════════════════════════════════════════════
314
+ // kanon rosetta inspect
315
+ // ═══════════════════════════════════════════════════════════════════════════════
316
+
317
+ interface InspectOptions {
318
+ from?: string;
319
+ to?: string;
320
+ variant?: string;
321
+ strict?: boolean;
322
+ json?: boolean;
323
+ profile?: string;
324
+ }
325
+
326
+ /**
327
+ * Handler for `kanon rosetta inspect <path>`.
328
+ * Inspects a translation without writing (equivalent to --dry-run on translate).
329
+ */
330
+ async function inspectCommand(
331
+ sourcePath: string,
332
+ options: InspectOptions,
333
+ ): Promise<void> {
334
+ const config = await loadForgeConfig();
335
+ const profile = options.profile
336
+ ? config.translations?.[options.profile]
337
+ : undefined;
338
+
339
+ if (options.profile && !profile) {
340
+ console.error(
341
+ chalk.red(
342
+ `Error: Translation profile "${options.profile}" not found in config.`,
343
+ ),
344
+ );
345
+ process.exit(1);
346
+ }
347
+
348
+ const resolved = mergeProfileWithFlags(profile, options);
349
+
350
+ const direction = resolveDirection(resolved.from, resolved.to);
351
+ if (typeof direction === "string") {
352
+ console.error(chalk.red(`Error: ${direction}`));
353
+ process.exit(1);
354
+ }
355
+
356
+ const registry = getRegistry();
357
+ const engine = getSharedEngine();
358
+
359
+ // Validate explicit format identifiers against direction
360
+ if (direction.sourceFormatId) {
361
+ const sourceRes = registry.resolve(direction.sourceFormatId, "source");
362
+ if (!sourceRes.ok) {
363
+ console.error(
364
+ chalk.red(
365
+ `Error: "${direction.sourceFormatId}" is not a valid source format.`,
366
+ ),
367
+ );
368
+ for (const d of sourceRes.diagnostics) {
369
+ console.error(chalk.dim(` ${d.message}`));
370
+ }
371
+ process.exit(1);
372
+ }
373
+ }
374
+
375
+ if (direction.targetFormatId) {
376
+ const targetRes = registry.resolve(direction.targetFormatId, "target");
377
+ if (!targetRes.ok) {
378
+ console.error(
379
+ chalk.red(
380
+ `Error: "${direction.targetFormatId}" is not a valid target format.`,
381
+ ),
382
+ );
383
+ for (const d of targetRes.diagnostics) {
384
+ console.error(chalk.dim(` ${d.message}`));
385
+ }
386
+ process.exit(1);
387
+ }
388
+ }
389
+
390
+ const documents = await loadSourceDocuments(sourcePath);
391
+
392
+ if (documents.length === 0) {
393
+ console.error(
394
+ chalk.red("Error: No documents found at the specified path."),
395
+ );
396
+ process.exit(1);
397
+ }
398
+
399
+ const request = buildTranslationRequest(direction, documents, resolved);
400
+ const report = engine.inspect(request);
401
+
402
+ if (options.json) {
403
+ const jsonOpts: JsonRenderOptions = {
404
+ generatedAt: new Date().toISOString(),
405
+ registryVersion: registry.version,
406
+ request,
407
+ defaults: [],
408
+ normalizations: [],
409
+ degradations: [],
410
+ };
411
+ console.log(renderJson(report, jsonOpts));
412
+ } else {
413
+ console.log(renderHuman(report));
414
+ }
415
+ }
416
+
417
+ // ═══════════════════════════════════════════════════════════════════════════════
418
+ // kanon rosetta translate
419
+ // ═══════════════════════════════════════════════════════════════════════════════
420
+
421
+ interface TranslateOptions {
422
+ from?: string;
423
+ to?: string;
424
+ variant?: string;
425
+ dryRun?: boolean;
426
+ strict?: boolean;
427
+ json?: boolean;
428
+ profile?: string;
429
+ }
430
+
431
+ /**
432
+ * Handler for `kanon rosetta translate <path>`.
433
+ * Executes a translation operation. With --dry-run, produces inspection
434
+ * output without writing files.
435
+ */
436
+ async function translateCommand(
437
+ sourcePath: string,
438
+ options: TranslateOptions,
439
+ ): Promise<void> {
440
+ const config = await loadForgeConfig();
441
+ const profile = options.profile
442
+ ? config.translations?.[options.profile]
443
+ : undefined;
444
+
445
+ if (options.profile && !profile) {
446
+ console.error(
447
+ chalk.red(
448
+ `Error: Translation profile "${options.profile}" not found in config.`,
449
+ ),
450
+ );
451
+ process.exit(1);
452
+ }
453
+
454
+ const resolved = mergeProfileWithFlags(profile, options);
455
+
456
+ const direction = resolveDirection(resolved.from, resolved.to);
457
+ if (typeof direction === "string") {
458
+ console.error(chalk.red(`Error: ${direction}`));
459
+ process.exit(1);
460
+ }
461
+
462
+ const registry = getRegistry();
463
+ const engine = getSharedEngine();
464
+
465
+ // Validate explicit format identifiers against direction
466
+ if (direction.sourceFormatId) {
467
+ const sourceRes = registry.resolve(direction.sourceFormatId, "source");
468
+ if (!sourceRes.ok) {
469
+ console.error(
470
+ chalk.red(
471
+ `Error: "${direction.sourceFormatId}" is not a valid source format.`,
472
+ ),
473
+ );
474
+ for (const d of sourceRes.diagnostics) {
475
+ console.error(chalk.dim(` ${d.message}`));
476
+ }
477
+ process.exit(1);
478
+ }
479
+ }
480
+
481
+ if (direction.targetFormatId) {
482
+ const targetRes = registry.resolve(direction.targetFormatId, "target");
483
+ if (!targetRes.ok) {
484
+ console.error(
485
+ chalk.red(
486
+ `Error: "${direction.targetFormatId}" is not a valid target format.`,
487
+ ),
488
+ );
489
+ for (const d of targetRes.diagnostics) {
490
+ console.error(chalk.dim(` ${d.message}`));
491
+ }
492
+ process.exit(1);
493
+ }
494
+ }
495
+
496
+ const documents = await loadSourceDocuments(sourcePath);
497
+
498
+ if (documents.length === 0) {
499
+ console.error(
500
+ chalk.red("Error: No documents found at the specified path."),
501
+ );
502
+ process.exit(1);
503
+ }
504
+
505
+ const request = buildTranslationRequest(direction, documents, resolved);
506
+
507
+ if (options.dryRun) {
508
+ // Dry-run: produce inspection without writing
509
+ const report = engine.inspect(request);
510
+
511
+ if (options.json) {
512
+ const jsonOpts: JsonRenderOptions = {
513
+ generatedAt: new Date().toISOString(),
514
+ registryVersion: registry.version,
515
+ request,
516
+ defaults: [],
517
+ normalizations: [],
518
+ degradations: [],
519
+ };
520
+ console.log(renderJson(report, jsonOpts));
521
+ } else {
522
+ console.log(renderHuman(report));
523
+ }
524
+ return;
525
+ }
526
+
527
+ // Execute translation
528
+ const result = engine.translate(request);
529
+
530
+ if (options.json) {
531
+ const output = {
532
+ status: result.status,
533
+ diagnostics: result.diagnostics,
534
+ plan: result.plan
535
+ ? {
536
+ operationCount: result.plan.outputFiles?.length ?? 0,
537
+ outputFiles:
538
+ result.plan.outputFiles?.map((f) => f.relativePath) ?? [],
539
+ }
540
+ : null,
541
+ };
542
+ console.log(JSON.stringify(output, null, 2));
543
+ } else {
544
+ // Human-readable result
545
+ if (result.status === "success") {
546
+ console.log(chalk.green("Translation successful."));
547
+ } else if (result.status === "partial") {
548
+ console.log(chalk.yellow("Translation completed with warnings."));
549
+ } else {
550
+ console.log(chalk.red("Translation failed."));
551
+ }
552
+
553
+ if (result.diagnostics.length > 0) {
554
+ console.log();
555
+ console.log(chalk.bold("Diagnostics:"));
556
+ for (const diag of result.diagnostics) {
557
+ const icon =
558
+ diag.severity === "error"
559
+ ? chalk.red("✗")
560
+ : diag.severity === "warning"
561
+ ? chalk.yellow("⚠")
562
+ : chalk.blue("ℹ");
563
+ console.log(` ${icon} [${diag.code}] ${diag.message}`);
564
+ if (diag.remediation) {
565
+ console.log(chalk.dim(` Remediation: ${diag.remediation}`));
566
+ }
567
+ }
568
+ }
569
+
570
+ if (result.plan?.outputFiles && result.plan.outputFiles.length > 0) {
571
+ console.log();
572
+ console.log(chalk.bold("Output files:"));
573
+ for (const file of result.plan.outputFiles) {
574
+ console.log(` ${file.relativePath}`);
575
+ }
576
+ }
577
+ }
578
+
579
+ if (result.status === "failure") {
580
+ process.exit(1);
581
+ }
582
+ }
583
+
584
+ // ═══════════════════════════════════════════════════════════════════════════════
585
+ // Translation Request Builder
586
+ // ═══════════════════════════════════════════════════════════════════════════════
587
+
588
+ function buildTranslationRequest(
589
+ direction: DirectionResult,
590
+ documents: readonly SourceDocument[],
591
+ resolved: ResolvedOptions,
592
+ ): TranslationRequest {
593
+ // Documents are already in SourceDocument shape from the orchestrator
594
+ const sourceDocuments = documents.map((d) => ({
595
+ path: d.path,
596
+ content: typeof d.content === "string" ? d.content : d.content,
597
+ executable: d.executable,
598
+ ...(d.mediaType ? { mediaType: d.mediaType } : {}),
599
+ }));
600
+
601
+ switch (direction.mode) {
602
+ case "inbound":
603
+ return {
604
+ mode: "inbound",
605
+ sourceDocuments,
606
+ source: {
607
+ formatId: direction.sourceFormatId,
608
+ variant: resolved.variant,
609
+ options: resolved.options as Record<string, never>,
610
+ },
611
+ canonical: {
612
+ emitEmptyAuxiliaryFiles: false,
613
+ },
614
+ canonicalSchemaVersion: "1.0.0",
615
+ strict: resolved.strict,
616
+ callerContext: {},
617
+ };
618
+ case "outbound":
619
+ // For outbound, the source documents represent a canonical artifact directory
620
+ // The engine handles parsing them into a KnowledgeArtifact
621
+ return {
622
+ mode: "transcode",
623
+ sourceDocuments,
624
+ source: {
625
+ formatId: "kanon-canonical",
626
+ options: {},
627
+ },
628
+ target: {
629
+ // biome-ignore lint/style/noNonNullAssertion: targetFormatId guaranteed for outbound
630
+ formatId: direction.targetFormatId!,
631
+ variant: resolved.variant,
632
+ options: resolved.options as Record<string, never>,
633
+ },
634
+ canonicalSchemaVersion: "1.0.0",
635
+ strict: resolved.strict,
636
+ callerContext: {},
637
+ };
638
+ case "transcode":
639
+ return {
640
+ mode: "transcode",
641
+ sourceDocuments,
642
+ source: {
643
+ formatId: direction.sourceFormatId,
644
+ variant: resolved.variant,
645
+ options: {},
646
+ },
647
+ target: {
648
+ // biome-ignore lint/style/noNonNullAssertion: targetFormatId guaranteed for transcode
649
+ formatId: direction.targetFormatId!,
650
+ variant: resolved.variant,
651
+ options: resolved.options as Record<string, never>,
652
+ },
653
+ canonicalSchemaVersion: "1.0.0",
654
+ strict: resolved.strict,
655
+ callerContext: {},
656
+ };
657
+ }
658
+ }
659
+
660
+ // ═══════════════════════════════════════════════════════════════════════════════
661
+ // Command Registration
662
+ // ═══════════════════════════════════════════════════════════════════════════════
663
+
664
+ /**
665
+ * Register the `kanon rosetta` command group on a Commander program.
666
+ */
667
+ export function registerRosettaCommands(program: Command): void {
668
+ const rosettaCmd = program
669
+ .command("rosetta")
670
+ .description(
671
+ "Rosetta Stone — format discovery, detection, inspection, and translation",
672
+ );
673
+
674
+ // Register profiles subcommand group
675
+ registerProfilesCommands(rosettaCmd);
676
+
677
+ // Register the provenance backfill command
678
+ registerBackfillCommand(rosettaCmd);
679
+
680
+ rosettaCmd
681
+ .command("formats")
682
+ .description("List all registered format contracts")
683
+ .option("--json", "Output as JSON")
684
+ .action((opts: FormatsOptions) => formatsCommand(opts));
685
+
686
+ rosettaCmd
687
+ .command("detect <path>")
688
+ .description("Detect the source format of documents at a path")
689
+ .option("--format <id>", "Explicit format selection (validates only)")
690
+ .option("--json", "Output as JSON")
691
+ .action((path: string, opts: DetectOptions) => detectCommand(path, opts));
692
+
693
+ rosettaCmd
694
+ .command("inspect <path>")
695
+ .description("Inspect a translation without writing files")
696
+ .option("--from <id>", "Source format (inbound direction)")
697
+ .option("--to <id>", "Target format (outbound direction)")
698
+ .option("--variant <name>", "Explicit variant selection")
699
+ .option("--strict", "Promote compatibility diagnostics to errors")
700
+ .option("--json", "Output as versioned JSON")
701
+ .option("--profile <name>", "Named translation profile from config")
702
+ .action((path: string, opts: InspectOptions) => inspectCommand(path, opts));
703
+
704
+ rosettaCmd
705
+ .command("translate <path>")
706
+ .description("Translate artifacts between formats")
707
+ .option("--from <id>", "Source format (inbound direction)")
708
+ .option("--to <id>", "Target format (outbound direction)")
709
+ .option("--variant <name>", "Explicit variant selection")
710
+ .option("--dry-run", "Inspect without writing")
711
+ .option("--strict", "Promote compatibility diagnostics to errors")
712
+ .option("--json", "Output as JSON")
713
+ .option("--profile <name>", "Named translation profile from config")
714
+ .action((path: string, opts: TranslateOptions) =>
715
+ translateCommand(path, opts),
716
+ );
717
+ }