@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,571 @@
1
+ /**
2
+ * Rosetta Stone — Human and Versioned JSON Renderers
3
+ *
4
+ * Both renderers consume the same InspectionReport model and produce
5
+ * different views of the same data:
6
+ * - Human renderer: terminal-friendly text with optional ANSI color codes
7
+ * - JSON renderer: versioned, schema-valid, deterministic JSON without ANSI
8
+ *
9
+ * CONSTRAINTS:
10
+ * - NO filesystem, process, clock, random, Git, or network imports
11
+ * - Pure functions only (timestamp and registryVersion supplied by caller)
12
+ * - JSON output validates through InspectionReportEnvelopeSchema
13
+ * - JSON output NEVER contains ANSI escape sequences
14
+ * - JSON output uses stable field names and deterministic key ordering
15
+ *
16
+ * Requirements: 8.6, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9
17
+ */
18
+
19
+ import type {
20
+ AppliedDefault,
21
+ AppliedNormalization,
22
+ DegradationRecord,
23
+ InspectionReportEnvelope,
24
+ TranslationDiagnostic,
25
+ TranslationRequest,
26
+ } from "../schemas";
27
+ import { InspectionReportEnvelopeSchema } from "../schemas";
28
+ import { codePointCompare, stableJsonStringify } from "./contracts";
29
+ import type { InspectionReport } from "./inspection";
30
+
31
+ // ═══════════════════════════════════════════════════════════════════════════════
32
+ // ANSI Color Helpers (human renderer only)
33
+ // ═══════════════════════════════════════════════════════════════════════════════
34
+
35
+ const ANSI = {
36
+ reset: "\x1b[0m",
37
+ bold: "\x1b[1m",
38
+ dim: "\x1b[2m",
39
+ red: "\x1b[31m",
40
+ green: "\x1b[32m",
41
+ yellow: "\x1b[33m",
42
+ blue: "\x1b[34m",
43
+ magenta: "\x1b[35m",
44
+ cyan: "\x1b[36m",
45
+ gray: "\x1b[90m",
46
+ } as const;
47
+
48
+ function color(text: string, ...codes: string[]): string {
49
+ return `${codes.join("")}${text}${ANSI.reset}`;
50
+ }
51
+
52
+ // ═══════════════════════════════════════════════════════════════════════════════
53
+ // Severity Indicators
54
+ // ═══════════════════════════════════════════════════════════════════════════════
55
+
56
+ const SEVERITY_ICON: Record<string, string> = {
57
+ error: "✗",
58
+ warning: "⚠",
59
+ info: "ℹ",
60
+ };
61
+
62
+ const SEVERITY_COLOR: Record<string, string> = {
63
+ error: ANSI.red,
64
+ warning: ANSI.yellow,
65
+ info: ANSI.blue,
66
+ };
67
+
68
+ // ═══════════════════════════════════════════════════════════════════════════════
69
+ // Human Renderer
70
+ // ═══════════════════════════════════════════════════════════════════════════════
71
+
72
+ /**
73
+ * Renders an InspectionReport as human-readable terminal text.
74
+ * Uses ANSI color codes for severity indicators and section headers.
75
+ */
76
+ export function renderHuman(report: InspectionReport): string {
77
+ const lines: string[] = [];
78
+
79
+ // Header
80
+ lines.push(color("Rosetta Stone Inspection Report", ANSI.bold, ANSI.cyan));
81
+ lines.push(color("─".repeat(40), ANSI.dim));
82
+ lines.push("");
83
+
84
+ // Request summary
85
+ lines.push(color("Request", ANSI.bold));
86
+ lines.push(` Direction: ${report.request.direction}`);
87
+ if (report.request.sourceFormat) {
88
+ lines.push(` Source format: ${report.request.sourceFormat}`);
89
+ }
90
+ if (report.request.targetFormat) {
91
+ lines.push(` Target format: ${report.request.targetFormat}`);
92
+ }
93
+ lines.push(` Strict: ${report.request.strict ? "yes" : "no"}`);
94
+ lines.push(` Dry-run: ${report.request.dryRun ? "yes" : "no"}`);
95
+ lines.push("");
96
+
97
+ // Detection
98
+ if (report.detection) {
99
+ lines.push(color("Detection", ANSI.bold));
100
+ if (report.detection.selectedFormatId) {
101
+ lines.push(
102
+ ` Selected: ${color(report.detection.selectedFormatId, ANSI.green)}`,
103
+ );
104
+ }
105
+ if (report.detection.ambiguous) {
106
+ lines.push(color(" ⚠ Ambiguous detection", ANSI.yellow));
107
+ }
108
+ for (const candidate of report.detection.candidates) {
109
+ const conf = (candidate.confidence * 100).toFixed(0);
110
+ lines.push(
111
+ ` ${candidate.formatId} (${conf}% confidence, ${candidate.evidenceCount} evidence)`,
112
+ );
113
+ }
114
+ lines.push("");
115
+ }
116
+
117
+ // Format
118
+ lines.push(color("Format", ANSI.bold));
119
+ lines.push(` ID: ${report.format.formatId}`);
120
+ if (report.format.variant) {
121
+ lines.push(` Variant: ${report.format.variant}`);
122
+ }
123
+ lines.push(` Contract version: ${report.format.contractVersion}`);
124
+ if (report.format.canonicalVersionRange) {
125
+ lines.push(
126
+ ` Canonical version range: ${report.format.canonicalVersionRange}`,
127
+ );
128
+ }
129
+ lines.push(` Lifecycle: ${report.format.lifecycle}`);
130
+ lines.push("");
131
+
132
+ // Canonical summary
133
+ lines.push(color("Canonical", ANSI.bold));
134
+ lines.push(` Schema version: ${report.canonical.schemaVersion}`);
135
+ if (report.canonical.artifactName) {
136
+ lines.push(` Artifact: ${report.canonical.artifactName}`);
137
+ }
138
+ if (report.canonical.type) {
139
+ lines.push(` Type: ${report.canonical.type}`);
140
+ }
141
+ if (report.canonical.harnesses.length > 0) {
142
+ lines.push(` Harnesses: ${report.canonical.harnesses.join(", ")}`);
143
+ }
144
+ const counts = [
145
+ report.canonical.hookCount > 0 && `${report.canonical.hookCount} hook(s)`,
146
+ report.canonical.mcpServerCount > 0 &&
147
+ `${report.canonical.mcpServerCount} MCP server(s)`,
148
+ report.canonical.workflowCount > 0 &&
149
+ `${report.canonical.workflowCount} workflow(s)`,
150
+ report.canonical.bodyOverrideCount > 0 &&
151
+ `${report.canonical.bodyOverrideCount} body override(s)`,
152
+ ].filter(Boolean);
153
+ if (counts.length > 0) {
154
+ lines.push(` Content: ${counts.join(", ")}`);
155
+ }
156
+ lines.push("");
157
+
158
+ // Compatibility
159
+ lines.push(color("Compatibility", ANSI.bold));
160
+ const totalCaps =
161
+ report.compatibility.fullCount +
162
+ report.compatibility.partialCount +
163
+ report.compatibility.noneCount;
164
+ lines.push(
165
+ ` Full: ${report.compatibility.fullCount} Partial: ${report.compatibility.partialCount} None: ${report.compatibility.noneCount} (${totalCaps} total)`,
166
+ );
167
+ if (report.compatibility.strictPromoted) {
168
+ lines.push(
169
+ color(" Strict mode promoted degradations to errors", ANSI.yellow),
170
+ );
171
+ }
172
+ const degradationEntries = Object.entries(report.compatibility.degradations);
173
+ if (degradationEntries.length > 0) {
174
+ lines.push(" Degradations:");
175
+ for (const [capability, action] of degradationEntries) {
176
+ lines.push(` ${capability}: ${action}`);
177
+ }
178
+ }
179
+ lines.push("");
180
+
181
+ // Options with origins
182
+ const effectiveKeys = Object.keys(report.options.effective);
183
+ if (effectiveKeys.length > 0) {
184
+ lines.push(color("Options", ANSI.bold));
185
+ for (const key of effectiveKeys) {
186
+ const value = report.options.effective[key];
187
+ const origin = report.options.origins[key] ?? "unknown";
188
+ const originLabel = color(`(${origin})`, ANSI.dim);
189
+ lines.push(` ${key}: ${JSON.stringify(value)} ${originLabel}`);
190
+ }
191
+ lines.push("");
192
+ }
193
+
194
+ // Diagnostics
195
+ lines.push(color("Diagnostics", ANSI.bold));
196
+ lines.push(
197
+ ` Errors: ${report.diagnostics.errorCount} Warnings: ${report.diagnostics.warningCount} Info: ${report.diagnostics.infoCount}`,
198
+ );
199
+ if (report.diagnostics.blocking.length > 0) {
200
+ lines.push(
201
+ color(` Blocking: ${report.diagnostics.blocking.join(", ")}`, ANSI.red),
202
+ );
203
+ }
204
+ for (const diag of report.diagnostics.diagnostics) {
205
+ const icon = SEVERITY_ICON[diag.severity] ?? "?";
206
+ const colorCode = SEVERITY_COLOR[diag.severity] ?? "";
207
+ const loc = formatDiagnosticLocation(diag);
208
+ lines.push(
209
+ ` ${color(icon, colorCode)} [${diag.code}] ${diag.message}${loc}`,
210
+ );
211
+ lines.push(color(` Remediation: ${diag.remediation}`, ANSI.dim));
212
+ }
213
+ lines.push("");
214
+
215
+ // Plan summary
216
+ lines.push(color("Plan", ANSI.bold));
217
+ lines.push(` Operations: ${report.plan.operationCount}`);
218
+ lines.push(` Application state: ${report.plan.applicationState}`);
219
+ if (report.plan.outputFilePaths.length > 0) {
220
+ lines.push(" Output files:");
221
+ for (const path of report.plan.outputFilePaths) {
222
+ lines.push(` ${path}`);
223
+ }
224
+ }
225
+ if (report.plan.policyDiagnosticCodes.length > 0) {
226
+ lines.push(
227
+ ` Policy diagnostics: ${report.plan.policyDiagnosticCodes.join(", ")}`,
228
+ );
229
+ }
230
+ lines.push("");
231
+
232
+ // Content preview status
233
+ lines.push(color("Content Preview", ANSI.bold));
234
+ if (report.preview.available) {
235
+ lines.push(color(" Available", ANSI.green));
236
+ } else {
237
+ lines.push(color(" Unavailable", ANSI.yellow));
238
+ if (report.preview.reason) {
239
+ lines.push(` Reason: ${report.preview.reason}`);
240
+ }
241
+ }
242
+ lines.push("");
243
+
244
+ // Collision policy
245
+ lines.push(color("Collision Policy", ANSI.bold));
246
+ lines.push(` Policy: ${report.collision.policy}`);
247
+ lines.push(` Collisions: ${report.collision.collisionCount}`);
248
+ const outcomeEntries = Object.entries(report.collision.outcomes);
249
+ if (outcomeEntries.length > 0) {
250
+ lines.push(" Outcomes:");
251
+ for (const [path, outcome] of outcomeEntries) {
252
+ lines.push(` ${path}: ${outcome}`);
253
+ }
254
+ }
255
+
256
+ return lines.join("\n");
257
+ }
258
+
259
+ /**
260
+ * Format a diagnostic's source/canonical location for human display.
261
+ */
262
+ function formatDiagnosticLocation(diag: TranslationDiagnostic): string {
263
+ if (diag.source?.path) {
264
+ const loc = diag.source;
265
+ const parts = [loc.path];
266
+ if (loc.line !== undefined) {
267
+ parts.push(`:${loc.line}`);
268
+ if (loc.column !== undefined) {
269
+ parts.push(`:${loc.column}`);
270
+ }
271
+ }
272
+ return ` at ${parts.join("")}`;
273
+ }
274
+ if (diag.canonical?.fieldPath) {
275
+ return ` at canonical:${diag.canonical.fieldPath}`;
276
+ }
277
+ return "";
278
+ }
279
+
280
+ // ═══════════════════════════════════════════════════════════════════════════════
281
+ // JSON Renderer
282
+ // ═══════════════════════════════════════════════════════════════════════════════
283
+
284
+ /** Options required by the JSON renderer that come from the impure shell */
285
+ export interface JsonRenderOptions {
286
+ /** ISO-8601 timestamp for the generatedAt field */
287
+ readonly generatedAt: string;
288
+ /** Registry version string */
289
+ readonly registryVersion: string;
290
+ /** The original translation request (for envelope) */
291
+ readonly request: TranslationRequest;
292
+ /** Defaults applied during translation */
293
+ readonly defaults: readonly AppliedDefault[];
294
+ /** Normalizations applied during translation */
295
+ readonly normalizations: readonly AppliedNormalization[];
296
+ /** Degradation records from compatibility */
297
+ readonly degradations: readonly DegradationRecord[];
298
+ }
299
+
300
+ /**
301
+ * Renders an InspectionReport as versioned JSON.
302
+ *
303
+ * Guarantees:
304
+ * - Validates through InspectionReportEnvelopeSchema
305
+ * - Uses stable field names and deterministic key ordering (recursively sorted)
306
+ * - NEVER contains ANSI escape sequences
307
+ * - Includes machineSchemaVersion field
308
+ * - Diagnostics follow severity/phase/code ordering (inherited from report)
309
+ *
310
+ * Returns the serialized JSON string.
311
+ * Throws if the output fails schema validation.
312
+ */
313
+ export function renderJson(
314
+ report: InspectionReport,
315
+ options: JsonRenderOptions,
316
+ ): string {
317
+ // Build the envelope structure from the InspectionReport model
318
+ const envelope: InspectionReportEnvelope = buildEnvelope(report, options);
319
+
320
+ // Validate against the schema
321
+ const result = InspectionReportEnvelopeSchema.safeParse(envelope);
322
+ if (!result.success) {
323
+ const errors = result.error.issues.map(
324
+ (issue) => `${issue.path.join(".")}: ${issue.message}`,
325
+ );
326
+ throw new Error(
327
+ `InspectionReportEnvelope validation failed:\n${errors.join("\n")}`,
328
+ );
329
+ }
330
+
331
+ // Produce deterministic JSON with recursively sorted keys
332
+ return stableJsonStringify(result.data);
333
+ }
334
+
335
+ /**
336
+ * Renders an InspectionReport as a parsed JSON object (not stringified).
337
+ * Useful when the caller needs the envelope for further processing.
338
+ *
339
+ * Same guarantees as renderJson but returns the validated object.
340
+ */
341
+ export function renderJsonObject(
342
+ report: InspectionReport,
343
+ options: JsonRenderOptions,
344
+ ): InspectionReportEnvelope {
345
+ const envelope = buildEnvelope(report, options);
346
+
347
+ const result = InspectionReportEnvelopeSchema.safeParse(envelope);
348
+ if (!result.success) {
349
+ const errors = result.error.issues.map(
350
+ (issue) => `${issue.path.join(".")}: ${issue.message}`,
351
+ );
352
+ throw new Error(
353
+ `InspectionReportEnvelope validation failed:\n${errors.join("\n")}`,
354
+ );
355
+ }
356
+
357
+ return result.data;
358
+ }
359
+
360
+ // ═══════════════════════════════════════════════════════════════════════════════
361
+ // Internal Envelope Builder
362
+ // ═══════════════════════════════════════════════════════════════════════════════
363
+
364
+ function buildEnvelope(
365
+ report: InspectionReport,
366
+ options: JsonRenderOptions,
367
+ ): InspectionReportEnvelope {
368
+ // Determine request mode from the translation request
369
+ const mode = options.request.mode;
370
+
371
+ // Build source/target format summaries from report.format
372
+ const sourceFormat =
373
+ mode === "inbound" || mode === "transcode"
374
+ ? {
375
+ formatId: report.format.formatId,
376
+ ...(report.format.variant !== undefined && {
377
+ variant: report.format.variant,
378
+ }),
379
+ contractVersion: report.format.contractVersion,
380
+ lifecycle: report.format.lifecycle,
381
+ }
382
+ : undefined;
383
+
384
+ const targetFormat =
385
+ mode === "outbound" || mode === "transcode"
386
+ ? {
387
+ formatId: report.format.formatId,
388
+ ...(report.format.variant !== undefined && {
389
+ variant: report.format.variant,
390
+ }),
391
+ contractVersion: report.format.contractVersion,
392
+ lifecycle: report.format.lifecycle,
393
+ }
394
+ : undefined;
395
+
396
+ // Build detection section
397
+ const detection = report.detection
398
+ ? {
399
+ candidates: report.detection.candidates.map((c) => ({
400
+ formatId: c.formatId,
401
+ confidence: c.confidence,
402
+ evidence: [], // Condensed in inspection report; empty array for envelope
403
+ })),
404
+ ...(report.detection.selectedFormatId !== undefined && {
405
+ selected: report.detection.selectedFormatId,
406
+ }),
407
+ }
408
+ : undefined;
409
+
410
+ // Build canonical section
411
+ const canonical = report.canonical.artifactName
412
+ ? {
413
+ artifactName: report.canonical.artifactName,
414
+ fieldCount: countCanonicalFields(report),
415
+ }
416
+ : {
417
+ fieldCount: countCanonicalFields(report),
418
+ };
419
+
420
+ // Build compatibility section from report degradation data
421
+ const compatibilityEntries = Object.entries(
422
+ report.compatibility.degradations,
423
+ );
424
+ const compatibility =
425
+ compatibilityEntries.length > 0 ||
426
+ report.compatibility.fullCount > 0 ||
427
+ report.compatibility.partialCount > 0 ||
428
+ report.compatibility.noneCount > 0
429
+ ? {
430
+ counts: buildCompatibilityCounts(report),
431
+ }
432
+ : undefined;
433
+
434
+ // Build plan section
435
+ const plan =
436
+ report.plan.operationCount > 0
437
+ ? {
438
+ fileCount: report.plan.outputFilePaths.length,
439
+ paths: [...report.plan.outputFilePaths].sort(codePointCompare),
440
+ }
441
+ : undefined;
442
+
443
+ // Safe diagnostics — strip any ANSI from messages/remediations
444
+ const diagnostics = report.diagnostics.diagnostics.map(
445
+ stripAnsiFromDiagnostic,
446
+ );
447
+
448
+ // Sort defaults deterministically
449
+ const defaults = [...options.defaults].sort((a, b) =>
450
+ codePointCompare(a.field, b.field),
451
+ );
452
+
453
+ // Sort normalizations deterministically
454
+ const normalizations = [...options.normalizations].sort((a, b) =>
455
+ codePointCompare(a.field, b.field),
456
+ );
457
+
458
+ // Sort degradations deterministically
459
+ const degradations = [...options.degradations].sort((a, b) =>
460
+ codePointCompare(a.capability, b.capability),
461
+ );
462
+
463
+ return {
464
+ machineSchemaVersion: "1.0",
465
+ generatedAt: options.generatedAt,
466
+ registryVersion: options.registryVersion,
467
+ request: options.request,
468
+ ...(sourceFormat !== undefined && { sourceFormat }),
469
+ ...(targetFormat !== undefined && { targetFormat }),
470
+ ...(detection !== undefined && { detection }),
471
+ canonical,
472
+ ...(compatibility !== undefined && { compatibility }),
473
+ ...(plan !== undefined && { plan }),
474
+ defaults,
475
+ normalizations,
476
+ diagnostics,
477
+ degradations,
478
+ } as InspectionReportEnvelope;
479
+ }
480
+
481
+ /**
482
+ * Count canonical fields based on the inspection report's canonical summary.
483
+ */
484
+ function countCanonicalFields(report: InspectionReport): number {
485
+ const c = report.canonical;
486
+ let count = 0;
487
+ if (c.artifactName) count++;
488
+ if (c.type) count++;
489
+ count += c.harnesses.length;
490
+ count += c.hookCount;
491
+ count += c.mcpServerCount;
492
+ count += c.workflowCount;
493
+ count += c.bodyOverrideCount;
494
+ return count;
495
+ }
496
+
497
+ /**
498
+ * Build compatibility counts record from report's compatibility summary.
499
+ * Maps each degradation capability to its support level and affected value count.
500
+ */
501
+ function buildCompatibilityCounts(
502
+ report: InspectionReport,
503
+ ): Record<string, { support: string; affectedValues: number }> {
504
+ const counts: Record<string, { support: string; affectedValues: number }> =
505
+ {};
506
+
507
+ // Initialize all canonical capabilities with "full" support
508
+ const ALL_CAPABILITIES = [
509
+ "frontmatter",
510
+ "body",
511
+ "hooks",
512
+ "mcp-servers",
513
+ "workflows",
514
+ "body-overrides",
515
+ "extra-fields",
516
+ "path-scoping",
517
+ "toggleable-rules",
518
+ "file-match-inclusion",
519
+ "system-prompt-merging",
520
+ "skill",
521
+ "power",
522
+ "rule",
523
+ "workflow",
524
+ "agent",
525
+ "prompt",
526
+ "template",
527
+ "reference-pack",
528
+ ];
529
+
530
+ for (const cap of ALL_CAPABILITIES) {
531
+ counts[cap] = { support: "full", affectedValues: 0 };
532
+ }
533
+
534
+ // Override with degradation entries from the compatibility summary
535
+ for (const [capability, action] of Object.entries(
536
+ report.compatibility.degradations,
537
+ )) {
538
+ // Infer support level from action
539
+ const support = action === "omit" ? "none" : "partial";
540
+ counts[capability] = { support, affectedValues: 0 };
541
+ }
542
+
543
+ return counts;
544
+ }
545
+
546
+ // ═══════════════════════════════════════════════════════════════════════════════
547
+ // ANSI Stripping Utility
548
+ // ═══════════════════════════════════════════════════════════════════════════════
549
+
550
+ /** Regex matching ANSI escape sequences */
551
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: intentionally matching ANSI escape sequences
552
+ const ANSI_REGEX = /\x1b\[[0-9;]*[A-Za-z]/g;
553
+
554
+ /** Strip all ANSI escape sequences from a string */
555
+ export function stripAnsi(text: string): string {
556
+ return text.replace(ANSI_REGEX, "");
557
+ }
558
+
559
+ /**
560
+ * Strip ANSI from all string fields in a diagnostic.
561
+ * Returns a new diagnostic with cleaned text.
562
+ */
563
+ function stripAnsiFromDiagnostic(
564
+ diag: TranslationDiagnostic,
565
+ ): TranslationDiagnostic {
566
+ return {
567
+ ...diag,
568
+ message: stripAnsi(diag.message),
569
+ remediation: stripAnsi(diag.remediation),
570
+ };
571
+ }