@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,106 @@
1
+ import { resolveFormat } from "../format-registry";
2
+ import { renderTemplate } from "../template-engine";
3
+ import type { HarnessCapabilityName } from "./capabilities";
4
+ import { applyDegradation } from "./degradation";
5
+ import type { AdapterWarning, HarnessAdapter, OutputFile } from "./types";
6
+
7
+ export const copilotAdapter: HarnessAdapter = (
8
+ artifact,
9
+ templateEnv,
10
+ context?,
11
+ ) => {
12
+ const files: OutputFile[] = [];
13
+ const warnings: AdapterWarning[] = [];
14
+
15
+ // Capability degradation checks
16
+ if (context) {
17
+ const checks: Array<{
18
+ capability: HarnessCapabilityName;
19
+ hasFeature: boolean;
20
+ }> = [
21
+ { capability: "hooks", hasFeature: artifact.hooks.length > 0 },
22
+ { capability: "mcp", hasFeature: artifact.mcpServers.length > 0 },
23
+ { capability: "workflows", hasFeature: artifact.workflows.length > 0 },
24
+ ];
25
+ for (const { capability, hasFeature } of checks) {
26
+ if (!hasFeature) continue;
27
+ const entry = context.capabilities[capability];
28
+ if (entry.support === "full") continue;
29
+ if (context.strict) {
30
+ warnings.push({
31
+ artifactName: artifact.name,
32
+ harnessName: "copilot",
33
+ message: `Strict mode: capability ${capability} not supported by harness copilot`,
34
+ });
35
+ return { files, warnings };
36
+ }
37
+ const degradation = applyDegradation(
38
+ entry.degradation ?? "inline",
39
+ capability,
40
+ artifact,
41
+ "copilot",
42
+ );
43
+ warnings.push(...degradation.warnings);
44
+ }
45
+ }
46
+
47
+ const harnessConfig = (artifact.frontmatter as Record<string, unknown>)[
48
+ "harness-config"
49
+ ] as Record<string, unknown> | undefined;
50
+ const copilotConfig = (harnessConfig?.copilot ?? {}) as Record<
51
+ string,
52
+ unknown
53
+ >;
54
+ const { format } = resolveFormat("copilot", copilotConfig);
55
+
56
+ // Generate .github/copilot-instructions.md
57
+ const instructionsContent = renderTemplate(
58
+ templateEnv,
59
+ "copilot/instructions.md.njk",
60
+ { artifact },
61
+ );
62
+ files.push({
63
+ relativePath: ".github/copilot-instructions.md",
64
+ content: instructionsContent,
65
+ });
66
+
67
+ // Generate path-scoped instructions if file_patterns or harness-config.copilot.path-scoped
68
+ const filePatterns = artifact.frontmatter.file_patterns;
69
+ const pathScoped = copilotConfig["path-scoped"] as string[] | undefined;
70
+ const applyTo = pathScoped || filePatterns;
71
+ if (applyTo && applyTo.length > 0) {
72
+ const applyToStr = applyTo.join(", ");
73
+ const scopedContent = renderTemplate(templateEnv, "copilot/scoped.md.njk", {
74
+ artifact,
75
+ applyTo: applyToStr,
76
+ });
77
+ files.push({
78
+ relativePath: `.github/instructions/${artifact.name}.instructions.md`,
79
+ content: scopedContent,
80
+ });
81
+ }
82
+
83
+ // Generate AGENTS.md when format is "agent", or when workflows exist / agents-md is true
84
+ if (
85
+ format === "agent" ||
86
+ artifact.workflows.length > 0 ||
87
+ copilotConfig["agents-md"] === true
88
+ ) {
89
+ const agentsContent = renderTemplate(templateEnv, "copilot/agents.md.njk", {
90
+ artifact,
91
+ });
92
+ files.push({ relativePath: "AGENTS.md", content: agentsContent });
93
+ }
94
+
95
+ // Skip hooks with warning
96
+ if (artifact.hooks.length > 0) {
97
+ warnings.push({
98
+ artifactName: artifact.name,
99
+ harnessName: "copilot",
100
+ message:
101
+ "GitHub Copilot does not support hooks; skipping all hook definitions",
102
+ });
103
+ }
104
+
105
+ return { files, warnings };
106
+ };
@@ -0,0 +1,97 @@
1
+ import { resolveFormat } from "../format-registry";
2
+ import type { InclusionMode } from "../schemas";
3
+ import { renderTemplate } from "../template-engine";
4
+ import type { HarnessCapabilityName } from "./capabilities";
5
+ import { applyDegradation } from "./degradation";
6
+ import type { AdapterWarning, HarnessAdapter, OutputFile } from "./types";
7
+ import { buildMcpConfig } from "./types";
8
+
9
+ const CURSOR_INCLUSION_MAP: Record<InclusionMode, string> = {
10
+ always: "always",
11
+ auto: "auto",
12
+ fileMatch: "auto",
13
+ manual: "agent-requested",
14
+ };
15
+
16
+ export const cursorAdapter: HarnessAdapter = (
17
+ artifact,
18
+ templateEnv,
19
+ context?,
20
+ ) => {
21
+ const files: OutputFile[] = [];
22
+ const warnings: AdapterWarning[] = [];
23
+
24
+ // Capability degradation checks
25
+ if (context) {
26
+ const checks: Array<{
27
+ capability: HarnessCapabilityName;
28
+ hasFeature: boolean;
29
+ }> = [
30
+ { capability: "hooks", hasFeature: artifact.hooks.length > 0 },
31
+ { capability: "mcp", hasFeature: artifact.mcpServers.length > 0 },
32
+ { capability: "workflows", hasFeature: artifact.workflows.length > 0 },
33
+ ];
34
+ for (const { capability, hasFeature } of checks) {
35
+ if (!hasFeature) continue;
36
+ const entry = context.capabilities[capability];
37
+ if (entry.support === "full") continue;
38
+ if (context.strict) {
39
+ warnings.push({
40
+ artifactName: artifact.name,
41
+ harnessName: "cursor",
42
+ message: `Strict mode: capability ${capability} not supported by harness cursor`,
43
+ });
44
+ return { files, warnings };
45
+ }
46
+ const degradation = applyDegradation(
47
+ entry.degradation ?? "inline",
48
+ capability,
49
+ artifact,
50
+ "cursor",
51
+ );
52
+ warnings.push(...degradation.warnings);
53
+ }
54
+ }
55
+
56
+ const harnessConfig = (artifact.frontmatter as Record<string, unknown>)[
57
+ "harness-config"
58
+ ] as Record<string, unknown> | undefined;
59
+ const cursorConfig = (harnessConfig?.cursor ?? {}) as Record<string, unknown>;
60
+ resolveFormat("cursor", cursorConfig);
61
+
62
+ // Map inclusion mode
63
+ const inclusionOverride = cursorConfig.inclusion as InclusionMode | undefined;
64
+ const canonicalInclusion =
65
+ inclusionOverride || artifact.frontmatter.inclusion;
66
+ const cursorInclusion = CURSOR_INCLUSION_MAP[canonicalInclusion] || "always";
67
+
68
+ // Generate .cursor/rules/<artifact>.md
69
+ const ruleContent = renderTemplate(templateEnv, "cursor/rule.md.njk", {
70
+ artifact,
71
+ cursorInclusion,
72
+ });
73
+ files.push({
74
+ relativePath: `.cursor/rules/${artifact.name}.md`,
75
+ content: ruleContent,
76
+ });
77
+
78
+ // Generate .cursor/mcp.json
79
+ if (artifact.mcpServers.length > 0) {
80
+ const mcpConfig = buildMcpConfig(artifact.mcpServers);
81
+ const mcpContent = renderTemplate(templateEnv, "cursor/mcp.json.njk", {
82
+ mcpConfig,
83
+ });
84
+ files.push({ relativePath: ".cursor/mcp.json", content: mcpContent });
85
+ }
86
+
87
+ // Skip hooks with warning
88
+ if (artifact.hooks.length > 0) {
89
+ warnings.push({
90
+ artifactName: artifact.name,
91
+ harnessName: "cursor",
92
+ message: "Cursor does not support hooks; skipping all hook definitions",
93
+ });
94
+ }
95
+
96
+ return { files, warnings };
97
+ };
@@ -0,0 +1,95 @@
1
+ import type {
2
+ CanonicalHook,
3
+ DegradationStrategy,
4
+ KnowledgeArtifact,
5
+ } from "../schemas";
6
+ import type { HarnessCapabilityName } from "./capabilities";
7
+ import type { AdapterWarning } from "./types";
8
+
9
+ export interface DegradationResult {
10
+ inlineText: string;
11
+ commentText: string;
12
+ warnings: AdapterWarning[];
13
+ }
14
+
15
+ /** Render hooks as prose for inline degradation */
16
+ export function degradeHooksInline(
17
+ hooks: CanonicalHook[],
18
+ artifactName: string,
19
+ harnessName: string,
20
+ ): DegradationResult {
21
+ const warnings: AdapterWarning[] = [];
22
+ if (hooks.length === 0) return { inlineText: "", commentText: "", warnings };
23
+
24
+ const lines = [
25
+ "",
26
+ "---",
27
+ "<!-- forge:degraded hooks (inline) -->",
28
+ "## Automated Behaviors",
29
+ "",
30
+ "The following behaviors should be applied manually since this harness does not support hooks natively:",
31
+ "",
32
+ ];
33
+
34
+ for (const hook of hooks) {
35
+ const trigger = hook.event.replace(/_/g, " ");
36
+ const action =
37
+ hook.action.type === "ask_agent"
38
+ ? hook.action.prompt
39
+ : `Run: \`${hook.action.command}\``;
40
+ lines.push(`- **When** ${trigger}: ${action}`);
41
+ }
42
+
43
+ warnings.push({
44
+ artifactName,
45
+ harnessName,
46
+ message: `hooks: degraded via inline strategy (${hooks.length} hook(s) rendered as prose)`,
47
+ });
48
+
49
+ return { inlineText: lines.join("\n"), commentText: "", warnings };
50
+ }
51
+
52
+ /** Apply a degradation strategy for a given capability */
53
+ export function applyDegradation(
54
+ strategy: DegradationStrategy,
55
+ capability: HarnessCapabilityName,
56
+ artifact: KnowledgeArtifact,
57
+ harnessName: string,
58
+ ): DegradationResult {
59
+ const warnings: AdapterWarning[] = [];
60
+
61
+ switch (strategy) {
62
+ case "inline":
63
+ if (capability === "hooks") {
64
+ return degradeHooksInline(artifact.hooks, artifact.name, harnessName);
65
+ }
66
+ warnings.push({
67
+ artifactName: artifact.name,
68
+ harnessName,
69
+ message: `${capability}: degraded via inline strategy`,
70
+ });
71
+ return {
72
+ inlineText: `\n<!-- forge:degraded ${capability} (inline) -->\n`,
73
+ commentText: "",
74
+ warnings,
75
+ };
76
+
77
+ case "comment": {
78
+ const comment = `<!-- forge:unsupported ${capability} — this harness does not support ${capability} -->`;
79
+ warnings.push({
80
+ artifactName: artifact.name,
81
+ harnessName,
82
+ message: `${capability}: degraded via comment strategy`,
83
+ });
84
+ return { inlineText: "", commentText: comment, warnings };
85
+ }
86
+
87
+ case "omit":
88
+ warnings.push({
89
+ artifactName: artifact.name,
90
+ harnessName,
91
+ message: `${capability}: omitted (not supported by ${harnessName})`,
92
+ });
93
+ return { inlineText: "", commentText: "", warnings };
94
+ }
95
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Adapter Registry — Routes harness adapters through Rosetta Stone target translators
3
+ *
4
+ * Each adapter entry wraps the corresponding target translator with template
5
+ * bundle loading and result mapping, preserving the existing adapter function
6
+ * signatures: (artifact, templateEnv, context?) => AdapterResult.
7
+ *
8
+ * ADR-RS-002: adapterRegistry remains keyed by harness. Each adapter entry
9
+ * calls the corresponding target translator with an in-memory template bundle
10
+ * and maps plans/diagnostics back to AdapterResult.
11
+ *
12
+ * ADR-RS-004: Templates are preloaded into an immutable bundle before entering
13
+ * the pure translation boundary.
14
+ *
15
+ * Requirements: 1.1, 6.1, 12.2, 14.5, 14.10
16
+ */
17
+
18
+ import { resolve } from "node:path";
19
+ import type nunjucks from "nunjucks";
20
+ import { resolveFormat } from "../format-registry";
21
+ import type {
22
+ TargetTranslator,
23
+ TargetTranslatorContext,
24
+ } from "../rosetta/registry";
25
+ import type { ImmutableTemplateBundle } from "../rosetta/templates";
26
+ import type {
27
+ FormatContract,
28
+ HarnessName,
29
+ KnowledgeArtifact,
30
+ } from "../schemas";
31
+ import { SUPPORTED_HARNESSES } from "../schemas";
32
+ import { loadTemplateBundle } from "../template-bundle-loader";
33
+ import type { AdapterContext, AdapterResult, HarnessAdapter } from "./types";
34
+
35
+ export type { HarnessName };
36
+ export { SUPPORTED_HARNESSES };
37
+
38
+ // ═══════════════════════════════════════════════════════════════════════════════
39
+ // Template Bundle Cache
40
+ // ═══════════════════════════════════════════════════════════════════════════════
41
+
42
+ /**
43
+ * Cached immutable template bundle, loaded once on first use.
44
+ * The templates dir is inferred from the Nunjucks Environment's loader path
45
+ * or falls back to the standard `templates/harness-adapters` location.
46
+ */
47
+ let cachedTemplateBundle: ImmutableTemplateBundle | null = null;
48
+
49
+ /**
50
+ * Get or create the immutable template bundle.
51
+ * Lazily loads templates from the filesystem on first call, then returns the
52
+ * cached frozen bundle for all subsequent calls.
53
+ */
54
+ function getTemplateBundle(
55
+ templateEnv: nunjucks.Environment,
56
+ ): ImmutableTemplateBundle {
57
+ if (cachedTemplateBundle !== null) {
58
+ return cachedTemplateBundle;
59
+ }
60
+
61
+ // Extract the templates directory from the Nunjucks environment's loader.
62
+ // The FileSystemLoader stores its search paths; use the first one.
63
+ let templatesDir: string;
64
+ const loaders = (
65
+ templateEnv as unknown as { loaders: Array<{ searchPaths?: string[] }> }
66
+ ).loaders;
67
+ if (loaders?.[0]?.searchPaths?.[0]) {
68
+ templatesDir = loaders[0].searchPaths[0];
69
+ } else {
70
+ // Fallback: resolve from cwd (standard kanon layout)
71
+ templatesDir = resolve("templates/harness-adapters");
72
+ }
73
+
74
+ cachedTemplateBundle = loadTemplateBundle(templatesDir);
75
+ return cachedTemplateBundle;
76
+ }
77
+
78
+ // ═══════════════════════════════════════════════════════════════════════════════
79
+ // Format Contract Lookup (lazy to avoid circular initialization)
80
+ // ═══════════════════════════════════════════════════════════════════════════════
81
+
82
+ /** Lazily-built map from harness name to primary bidirectional/target format contract. */
83
+ let harnessContractMap: Map<HarnessName, FormatContract> | null = null;
84
+
85
+ /**
86
+ * Get the harness-to-contract map, building it on first call.
87
+ * Uses lazy initialization to avoid circular dependency issues at module load.
88
+ */
89
+ function getHarnessContractMap(): ReadonlyMap<HarnessName, FormatContract> {
90
+ if (harnessContractMap !== null) {
91
+ return harnessContractMap;
92
+ }
93
+ // Import lazily to break the circular init chain:
94
+ // adapters/index → rosetta/builtins/contracts → rosetta/builtins/compatibility-profiles → adapters/capabilities
95
+ const { BUILTIN_FORMAT_CONTRACTS } = require("../rosetta/builtins/contracts");
96
+ const map = new Map<HarnessName, FormatContract>();
97
+ for (const contract of BUILTIN_FORMAT_CONTRACTS) {
98
+ if (contract.harness === null) continue;
99
+ if (contract.direction === "source") continue;
100
+ const harness = contract.harness as HarnessName;
101
+ // First matching bidirectional/target contract wins
102
+ if (!map.has(harness)) {
103
+ map.set(harness, contract);
104
+ }
105
+ }
106
+ harnessContractMap = map;
107
+ return map;
108
+ }
109
+
110
+ /** Lazily-built target translator map from format identifiers. */
111
+ let targetTranslatorMap: ReadonlyMap<string, TargetTranslator> | null = null;
112
+
113
+ /**
114
+ * Get the target translator map, loading it on first call.
115
+ */
116
+ function getTargetTranslatorMap(): ReadonlyMap<string, TargetTranslator> {
117
+ if (targetTranslatorMap !== null) {
118
+ return targetTranslatorMap;
119
+ }
120
+ const { TARGET_TRANSLATORS } = require("../rosetta/builtins/targets");
121
+ targetTranslatorMap = TARGET_TRANSLATORS as ReadonlyMap<
122
+ string,
123
+ TargetTranslator
124
+ >;
125
+ return targetTranslatorMap;
126
+ }
127
+
128
+ // ═══════════════════════════════════════════════════════════════════════════════
129
+ // Result Mapping
130
+ // ═══════════════════════════════════════════════════════════════════════════════
131
+
132
+ /**
133
+ * Map a TargetTranslationOutput (plan + diagnostics + degradations) back to
134
+ * the AdapterResult shape that existing consumers expect.
135
+ *
136
+ * - plan.outputFiles → AdapterResult.files (preserving relativePath, content, executable)
137
+ * - diagnostics with severity "warning" → AdapterResult.warnings
138
+ * - diagnostics with severity "error" → AdapterResult.errors
139
+ * - degradation records generate additional warnings
140
+ */
141
+ function mapToAdapterResult(
142
+ output: {
143
+ plan: Record<string, unknown>;
144
+ diagnostics: readonly Record<string, unknown>[];
145
+ degradations: readonly Record<string, unknown>[];
146
+ },
147
+ artifactName: string,
148
+ harnessName: HarnessName,
149
+ ): AdapterResult {
150
+ const plan = output.plan as {
151
+ outputFiles?: Array<{
152
+ relativePath: string;
153
+ content: string | Uint8Array;
154
+ executable?: boolean;
155
+ }>;
156
+ };
157
+ const files = (plan.outputFiles ?? []).map((f) => ({
158
+ relativePath: f.relativePath,
159
+ content:
160
+ typeof f.content === "string"
161
+ ? f.content
162
+ : new TextDecoder().decode(f.content),
163
+ ...(f.executable ? { executable: true } : {}),
164
+ }));
165
+
166
+ const warnings: AdapterResult["warnings"] = [];
167
+ const errors: AdapterResult["errors"] = [];
168
+
169
+ // Map diagnostics to warnings/errors
170
+ for (const diag of output.diagnostics) {
171
+ const d = diag as { severity?: string; message?: string; code?: string };
172
+ const message = d.message ?? d.code ?? "Unknown diagnostic";
173
+
174
+ if (d.severity === "error") {
175
+ errors.push({
176
+ artifactName,
177
+ harnessName,
178
+ message,
179
+ });
180
+ } else {
181
+ // "warning" and "info" both map to adapter warnings for backward compat
182
+ warnings.push({
183
+ artifactName,
184
+ harnessName,
185
+ message,
186
+ });
187
+ }
188
+ }
189
+
190
+ // Map degradation records to warnings
191
+ for (const deg of output.degradations) {
192
+ const d = deg as {
193
+ capability?: string;
194
+ action?: string;
195
+ affectedValueCount?: number;
196
+ };
197
+ warnings.push({
198
+ artifactName,
199
+ harnessName,
200
+ message: `Degradation: ${d.capability ?? "unknown"} (${d.action ?? "omit"}, ${d.affectedValueCount ?? 0} value(s) affected)`,
201
+ });
202
+ }
203
+
204
+ return {
205
+ files,
206
+ warnings,
207
+ ...(errors.length > 0 ? { errors } : {}),
208
+ };
209
+ }
210
+
211
+ // ═══════════════════════════════════════════════════════════════════════════════
212
+ // Adapter Wrapper
213
+ // ═══════════════════════════════════════════════════════════════════════════════
214
+
215
+ /**
216
+ * Create an adapter that routes through a Rosetta Stone target translator.
217
+ *
218
+ * Preserves the existing adapter signature:
219
+ * (artifact: KnowledgeArtifact, templateEnv: nunjucks.Environment, context?: AdapterContext) => AdapterResult
220
+ *
221
+ * During migration, each adapter entry in the registry wraps the target
222
+ * translator with the necessary template loading and result mapping.
223
+ */
224
+ function createRosettaAdapter(harness: HarnessName): HarnessAdapter {
225
+ return (
226
+ artifact: KnowledgeArtifact,
227
+ templateEnv: nunjucks.Environment,
228
+ _context?: AdapterContext,
229
+ ): AdapterResult => {
230
+ // Get the format contract for this harness (lazy init)
231
+ const contractMap = getHarnessContractMap();
232
+ const contract = contractMap.get(harness);
233
+ if (!contract) {
234
+ return {
235
+ files: [],
236
+ warnings: [
237
+ {
238
+ artifactName: artifact.name,
239
+ harnessName: harness,
240
+ message: `No format contract found for harness "${harness}"`,
241
+ },
242
+ ],
243
+ };
244
+ }
245
+
246
+ // Resolve variant from harness-config using the same logic as before
247
+ const harnessConfig = (artifact.frontmatter as Record<string, unknown>)[
248
+ "harness-config"
249
+ ] as Record<string, unknown> | undefined;
250
+ const perHarnessConfig = (harnessConfig?.[harness] ?? {}) as Record<
251
+ string,
252
+ unknown
253
+ >;
254
+ const { format: variant } = resolveFormat(harness, perHarnessConfig);
255
+
256
+ // Preload immutable template bundle (cached after first load)
257
+ const templates = getTemplateBundle(templateEnv);
258
+
259
+ // Build the target translator context
260
+ const translatorContext: TargetTranslatorContext = {
261
+ format: contract,
262
+ variant,
263
+ canonicalSchemaVersion: "1.0.0",
264
+ options: {},
265
+ callerContext: {},
266
+ templates,
267
+ };
268
+
269
+ // Look up and call the target translator (lazy init)
270
+ const translatorMap = getTargetTranslatorMap();
271
+ const translator = translatorMap.get(contract.id as unknown as string);
272
+ if (!translator) {
273
+ return {
274
+ files: [],
275
+ warnings: [
276
+ {
277
+ artifactName: artifact.name,
278
+ harnessName: harness,
279
+ message: `No target translator found for format "${contract.id}"`,
280
+ },
281
+ ],
282
+ };
283
+ }
284
+
285
+ // Call the target translator with the artifact as a record
286
+ const output = translator(
287
+ artifact as unknown as Record<string, unknown>,
288
+ translatorContext,
289
+ );
290
+
291
+ // Map the output back to AdapterResult shape
292
+ return mapToAdapterResult(output, artifact.name, harness);
293
+ };
294
+ }
295
+
296
+ // ═══════════════════════════════════════════════════════════════════════════════
297
+ // Public Adapter Registry
298
+ // ═══════════════════════════════════════════════════════════════════════════════
299
+
300
+ /**
301
+ * Registry mapping harness names to adapter functions.
302
+ *
303
+ * Each entry routes through the corresponding Rosetta Stone target translator,
304
+ * preloading immutable template bundles and mapping plans/diagnostics back to
305
+ * the AdapterResult shape that existing consumers expect.
306
+ */
307
+ export const adapterRegistry: Record<HarnessName, HarnessAdapter> = {
308
+ kiro: createRosettaAdapter("kiro"),
309
+ "claude-code": createRosettaAdapter("claude-code"),
310
+ codex: createRosettaAdapter("codex"),
311
+ copilot: createRosettaAdapter("copilot"),
312
+ cursor: createRosettaAdapter("cursor"),
313
+ windsurf: createRosettaAdapter("windsurf"),
314
+ cline: createRosettaAdapter("cline"),
315
+ qdeveloper: createRosettaAdapter("qdeveloper"),
316
+ };
317
+
318
+ /**
319
+ * Reset the cached template bundle. Used in tests to ensure fresh state.
320
+ * @internal
321
+ */
322
+ export function _resetTemplateBundleCache(): void {
323
+ cachedTemplateBundle = null;
324
+ }