@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,348 @@
1
+ /**
2
+ * Rosetta Stone — Claude Code Harness-Native Source Translator
3
+ *
4
+ * Translates Claude Code's native format (CLAUDE.md, .claude/settings.json)
5
+ * into a canonical KnowledgeArtifact candidate.
6
+ *
7
+ * CONSTRAINTS:
8
+ * - NO filesystem, process, clock, random, Git, or network imports
9
+ * - Pure function only
10
+ *
11
+ * Requirements: 2.9, 4.1, 4.2, 4.3, 4.5, 4.6
12
+ */
13
+
14
+ import matter from "gray-matter";
15
+ import type {
16
+ CanonicalHook,
17
+ McpServerDefinition,
18
+ SourceDocument,
19
+ TranslationDiagnostic,
20
+ } from "../../../schemas";
21
+ import { createDiagnostic } from "../../diagnostics";
22
+ import type {
23
+ SourceTranslationOutput,
24
+ SourceTranslatorContext,
25
+ } from "../../registry";
26
+ import {
27
+ namespacedExtraField,
28
+ normalizeDocumentOrder,
29
+ SourceAccountant,
30
+ } from "../../source-accounting";
31
+
32
+ // ═══════════════════════════════════════════════════════════════════════════════
33
+ // Internal helpers
34
+ // ═══════════════════════════════════════════════════════════════════════════════
35
+
36
+ function isClaudeMd(path: string): boolean {
37
+ return path === "CLAUDE.md" || path.endsWith("/CLAUDE.md");
38
+ }
39
+
40
+ function isSettingsJson(path: string): boolean {
41
+ return path.endsWith("settings.json");
42
+ }
43
+
44
+ function isMcpJson(path: string): boolean {
45
+ return path.endsWith("mcp.json");
46
+ }
47
+
48
+ /**
49
+ * Parse CLAUDE.md — markdown with optional frontmatter.
50
+ */
51
+ function parseClaudeMd(
52
+ doc: SourceDocument,
53
+ diagnostics: TranslationDiagnostic[],
54
+ ): { body: string; frontmatter: Record<string, unknown> } {
55
+ const content =
56
+ typeof doc.content === "string"
57
+ ? doc.content
58
+ : new TextDecoder().decode(doc.content);
59
+
60
+ try {
61
+ const parsed = matter(content);
62
+ return { body: parsed.content.trim(), frontmatter: { ...parsed.data } };
63
+ } catch {
64
+ diagnostics.push(
65
+ createDiagnostic("RS_CANONICAL_INVALID_FRONTMATTER", {
66
+ message: `Failed to parse frontmatter in "${doc.path}".`,
67
+ source: { path: doc.path },
68
+ }),
69
+ );
70
+ return { body: content.trim(), frontmatter: {} };
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Parse .claude/settings.json — extract command entries as agent_stop hooks
76
+ * and preserve unmapped settings as extra fields.
77
+ */
78
+ function parseSettingsDocument(
79
+ doc: SourceDocument,
80
+ formatId: string,
81
+ diagnostics: TranslationDiagnostic[],
82
+ ): { hooks: CanonicalHook[]; extraFields: Record<string, unknown> } {
83
+ const content =
84
+ typeof doc.content === "string"
85
+ ? doc.content
86
+ : new TextDecoder().decode(doc.content);
87
+
88
+ let data: Record<string, unknown>;
89
+ try {
90
+ data = JSON.parse(content);
91
+ } catch {
92
+ diagnostics.push(
93
+ createDiagnostic("RS_CANONICAL_INVALID_YAML", {
94
+ message: `Settings file "${doc.path}" contains invalid JSON.`,
95
+ source: { path: doc.path },
96
+ }),
97
+ );
98
+ return { hooks: [], extraFields: {} };
99
+ }
100
+
101
+ const hooks: CanonicalHook[] = [];
102
+ const extraFields: Record<string, unknown> = {};
103
+ const knownFields = new Set(["commands", "permissions"]);
104
+
105
+ // Extract command entries → CanonicalHook with event: "agent_stop"
106
+ if (data.commands && Array.isArray(data.commands)) {
107
+ for (const cmd of data.commands) {
108
+ if (typeof cmd === "string") {
109
+ hooks.push({
110
+ name: `agent-stop-${cmd.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`,
111
+ event: "agent_stop",
112
+ action: { type: "run_command", command: cmd },
113
+ });
114
+ } else if (
115
+ cmd &&
116
+ typeof cmd === "object" &&
117
+ (cmd as Record<string, unknown>).command
118
+ ) {
119
+ const cmdObj = cmd as Record<string, unknown>;
120
+ hooks.push({
121
+ name:
122
+ (cmdObj.name as string) ??
123
+ `agent-stop-${(cmdObj.command as string).replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`,
124
+ description: cmdObj.description as string | undefined,
125
+ event: "agent_stop",
126
+ action: { type: "run_command", command: cmdObj.command as string },
127
+ });
128
+ }
129
+ }
130
+ }
131
+
132
+ // Preserve unmapped fields as namespaced extra data
133
+ for (const [key, value] of Object.entries(data)) {
134
+ if (!knownFields.has(key)) {
135
+ extraFields[namespacedExtraField(formatId, doc.path, key)] = value;
136
+ }
137
+ }
138
+
139
+ // Preserve permissions as extra data too
140
+ if (data.permissions !== undefined) {
141
+ extraFields[namespacedExtraField(formatId, doc.path, "permissions")] =
142
+ data.permissions;
143
+ }
144
+
145
+ return { hooks, extraFields };
146
+ }
147
+
148
+ /**
149
+ * Parse .claude/mcp.json — extract MCP server definitions.
150
+ */
151
+ function parseMcpJsonDocument(
152
+ doc: SourceDocument,
153
+ diagnostics: TranslationDiagnostic[],
154
+ ): McpServerDefinition[] {
155
+ const content =
156
+ typeof doc.content === "string"
157
+ ? doc.content
158
+ : new TextDecoder().decode(doc.content);
159
+
160
+ let data: unknown;
161
+ try {
162
+ data = JSON.parse(content);
163
+ } catch {
164
+ diagnostics.push(
165
+ createDiagnostic("RS_CANONICAL_INVALID_YAML", {
166
+ message: `MCP file "${doc.path}" contains invalid JSON.`,
167
+ source: { path: doc.path },
168
+ }),
169
+ );
170
+ return [];
171
+ }
172
+
173
+ const servers: McpServerDefinition[] = [];
174
+
175
+ if (data && typeof data === "object" && !Array.isArray(data)) {
176
+ const obj = data as Record<string, unknown>;
177
+ const mcpServers = (obj.mcpServers ?? obj) as Record<string, unknown>;
178
+ for (const [name, config] of Object.entries(mcpServers)) {
179
+ if (!config || typeof config !== "object") continue;
180
+ const cfg = config as Record<string, unknown>;
181
+ if (cfg.url) {
182
+ const transport =
183
+ cfg.type === "http" ? ("http" as const) : ("sse" as const);
184
+ servers.push({
185
+ name,
186
+ transport,
187
+ url: cfg.url as string,
188
+ env: (cfg.env as Record<string, string>) ?? {},
189
+ ...(cfg.timeout ? { timeout: cfg.timeout as number } : {}),
190
+ ...(cfg.autoApprove
191
+ ? { autoApprove: cfg.autoApprove as string[] }
192
+ : {}),
193
+ });
194
+ } else if (cfg.command) {
195
+ servers.push({
196
+ name,
197
+ transport: "stdio" as const,
198
+ command: cfg.command as string,
199
+ args: (cfg.args as string[]) ?? [],
200
+ env: (cfg.env as Record<string, string>) ?? {},
201
+ ...(cfg.timeout ? { timeout: cfg.timeout as number } : {}),
202
+ ...(cfg.autoApprove
203
+ ? { autoApprove: cfg.autoApprove as string[] }
204
+ : {}),
205
+ });
206
+ }
207
+ }
208
+ } else if (Array.isArray(data)) {
209
+ for (const entry of data) {
210
+ if (!entry || typeof entry !== "object") continue;
211
+ const cfg = entry as Record<string, unknown>;
212
+ if (!cfg.name) continue;
213
+ if (cfg.url) {
214
+ servers.push({
215
+ name: cfg.name as string,
216
+ transport:
217
+ cfg.transport === "http" ? ("http" as const) : ("sse" as const),
218
+ url: cfg.url as string,
219
+ env: (cfg.env as Record<string, string>) ?? {},
220
+ });
221
+ } else if (cfg.command) {
222
+ servers.push({
223
+ name: cfg.name as string,
224
+ transport: "stdio" as const,
225
+ command: cfg.command as string,
226
+ args: (cfg.args as string[]) ?? [],
227
+ env: (cfg.env as Record<string, string>) ?? {},
228
+ });
229
+ }
230
+ }
231
+ }
232
+
233
+ return servers;
234
+ }
235
+
236
+ // ═══════════════════════════════════════════════════════════════════════════════
237
+ // Exported Translator
238
+ // ═══════════════════════════════════════════════════════════════════════════════
239
+
240
+ /**
241
+ * Claude Code harness-native source translator.
242
+ *
243
+ * Consumes:
244
+ * - CLAUDE.md → body content becomes the artifact body
245
+ * - .claude/settings.json → extracts hooks and relevant settings as extra fields
246
+ * - .claude/mcp.json → extracts MCP server definitions
247
+ *
248
+ * Sets type: "rule" and harnesses: ["claude-code"]
249
+ */
250
+ export function translateClaudeCodeNative(
251
+ documents: readonly SourceDocument[],
252
+ context: SourceTranslatorContext,
253
+ ): SourceTranslationOutput {
254
+ const accountant = new SourceAccountant();
255
+ const diagnostics: TranslationDiagnostic[] = [];
256
+ const sorted = normalizeDocumentOrder(documents);
257
+
258
+ // Determine artifact name from caller context
259
+ const artifactNameHint = context.callerContext.artifactNameHint as
260
+ | string
261
+ | undefined;
262
+
263
+ // Classify documents
264
+ const claudeMdDocs = sorted.filter((d) => isClaudeMd(d.path));
265
+ const settingsDocs = sorted.filter((d) => isSettingsJson(d.path));
266
+ const mcpDocs = sorted.filter((d) => isMcpJson(d.path));
267
+
268
+ // Handle missing primary file
269
+ if (claudeMdDocs.length === 0) {
270
+ diagnostics.push(
271
+ createDiagnostic("RS_CANONICAL_MISSING_KNOWLEDGE_MD", {
272
+ formatId: context.format.id,
273
+ message: "No CLAUDE.md file found in the document set.",
274
+ }),
275
+ );
276
+ return {
277
+ diagnostics,
278
+ consumedPaths: accountant.getConsumedPaths(),
279
+ preservedPaths: accountant.getPreservedPaths(),
280
+ };
281
+ }
282
+
283
+ // Parse primary CLAUDE.md
284
+ const primaryDoc = claudeMdDocs[0];
285
+ const { body, frontmatter } = parseClaudeMd(primaryDoc, diagnostics);
286
+
287
+ accountant.consume(primaryDoc.path);
288
+ accountant.mapField(primaryDoc.path, "content", "body");
289
+
290
+ // Mark additional CLAUDE.md files as preserved
291
+ for (let i = 1; i < claudeMdDocs.length; i++) {
292
+ accountant.preserve(claudeMdDocs[i].path);
293
+ }
294
+
295
+ // Parse settings files
296
+ const allHooks: CanonicalHook[] = [];
297
+ const allExtraFields: Record<string, unknown> = {};
298
+
299
+ for (const settingsDoc of settingsDocs) {
300
+ const { hooks, extraFields } = parseSettingsDocument(
301
+ settingsDoc,
302
+ context.format.id,
303
+ diagnostics,
304
+ );
305
+ allHooks.push(...hooks);
306
+ Object.assign(allExtraFields, extraFields);
307
+ accountant.consume(settingsDoc.path);
308
+ accountant.mapField(settingsDoc.path, "commands", "hooks");
309
+ accountant.mapField(settingsDoc.path, "settings", "extraFields");
310
+ }
311
+
312
+ // Parse MCP server files
313
+ const allMcpServers: McpServerDefinition[] = [];
314
+ for (const mcpDoc of mcpDocs) {
315
+ const servers = parseMcpJsonDocument(mcpDoc, diagnostics);
316
+ allMcpServers.push(...servers);
317
+ accountant.consume(mcpDoc.path);
318
+ accountant.mapField(mcpDoc.path, "mcpServers", "mcpServers");
319
+ }
320
+
321
+ // Derive artifact name
322
+ const name = artifactNameHint ?? "claude";
323
+
324
+ // Build the canonical candidate
325
+ const candidate: Record<string, unknown> = {
326
+ name,
327
+ frontmatter: {
328
+ name,
329
+ ...frontmatter,
330
+ type: "rule",
331
+ harnesses: ["claude-code"],
332
+ },
333
+ body,
334
+ hooks: allHooks,
335
+ mcpServers: allMcpServers,
336
+ workflows: [],
337
+ sourcePath: primaryDoc.path,
338
+ extraFields: allExtraFields,
339
+ bodyOverrides: {},
340
+ };
341
+
342
+ return {
343
+ candidate,
344
+ diagnostics,
345
+ consumedPaths: accountant.getConsumedPaths(),
346
+ preservedPaths: accountant.getPreservedPaths(),
347
+ };
348
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Rosetta Stone — Cline Harness-Native Source Translator
3
+ *
4
+ * Translates Cline's native format (.clinerules or .cline/rules/*.md)
5
+ * into a canonical KnowledgeArtifact candidate.
6
+ *
7
+ * CONSTRAINTS:
8
+ * - NO filesystem, process, clock, random, Git, or network imports
9
+ * - Pure function only
10
+ *
11
+ * Requirements: 2.9, 4.1, 4.2, 4.3, 4.5, 4.6
12
+ */
13
+
14
+ import matter from "gray-matter";
15
+ import type { SourceDocument, TranslationDiagnostic } from "../../../schemas";
16
+ import { createDiagnostic } from "../../diagnostics";
17
+ import type {
18
+ SourceTranslationOutput,
19
+ SourceTranslatorContext,
20
+ } from "../../registry";
21
+ import {
22
+ normalizeDocumentOrder,
23
+ SourceAccountant,
24
+ } from "../../source-accounting";
25
+
26
+ // ═══════════════════════════════════════════════════════════════════════════════
27
+ // Internal helpers
28
+ // ═══════════════════════════════════════════════════════════════════════════════
29
+
30
+ /**
31
+ * Checks if a document is the root .clinerules file.
32
+ */
33
+ function isClineRulesRoot(path: string): boolean {
34
+ return path === ".clinerules" || path.endsWith("/.clinerules");
35
+ }
36
+
37
+ /**
38
+ * Checks if a document is a Cline rule file under .cline/rules/.
39
+ */
40
+ function isClineRuleFile(path: string): boolean {
41
+ return (
42
+ (path.includes(".cline/rules/") || path.includes(".cline/rules\\")) &&
43
+ path.endsWith(".md")
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Checks if a document is any recognized Cline file.
49
+ */
50
+ function isClineFile(path: string): boolean {
51
+ return isClineRulesRoot(path) || isClineRuleFile(path);
52
+ }
53
+
54
+ /**
55
+ * Derives a kebab-case artifact name from a document path.
56
+ */
57
+ function deriveArtifactName(path: string): string {
58
+ const segments = path.split("/");
59
+ const base = segments[segments.length - 1] ?? "";
60
+ const name = base.replace(/\.[^.]+$/, "");
61
+ return name
62
+ .toLowerCase()
63
+ .replace(/[^a-z0-9]+/g, "-")
64
+ .replace(/^-|-$/g, "");
65
+ }
66
+
67
+ // ═══════════════════════════════════════════════════════════════════════════════
68
+ // Exported Translator
69
+ // ═══════════════════════════════════════════════════════════════════════════════
70
+
71
+ /**
72
+ * Cline harness-native source translator.
73
+ *
74
+ * Consumes:
75
+ * - .clinerules → body content becomes the artifact body
76
+ * - .cline/rules/*.md → rule file body content
77
+ *
78
+ * Sets type: "rule" and harnesses: ["cline"]
79
+ */
80
+ export function translateClineNative(
81
+ documents: readonly SourceDocument[],
82
+ context: SourceTranslatorContext,
83
+ ): SourceTranslationOutput {
84
+ const accountant = new SourceAccountant();
85
+ const diagnostics: TranslationDiagnostic[] = [];
86
+ const sorted = normalizeDocumentOrder(documents);
87
+
88
+ // Determine artifact name from caller context
89
+ const artifactNameHint = context.callerContext.artifactNameHint as
90
+ | string
91
+ | undefined;
92
+
93
+ // Classify documents — prefer .clinerules, fall back to rule dir files
94
+ const rootRuleDocs = sorted.filter((d) => isClineRulesRoot(d.path));
95
+ const ruleDirDocs = sorted.filter((d) => isClineRuleFile(d.path));
96
+
97
+ const primaryDoc = rootRuleDocs[0] ?? ruleDirDocs[0];
98
+
99
+ // Handle missing primary file
100
+ if (!primaryDoc) {
101
+ diagnostics.push(
102
+ createDiagnostic("RS_CANONICAL_MISSING_KNOWLEDGE_MD", {
103
+ formatId: context.format.id,
104
+ message:
105
+ "No .clinerules or .cline/rules/ file found in the document set.",
106
+ }),
107
+ );
108
+ return {
109
+ diagnostics,
110
+ consumedPaths: accountant.getConsumedPaths(),
111
+ preservedPaths: accountant.getPreservedPaths(),
112
+ };
113
+ }
114
+
115
+ // Parse primary file
116
+ const content =
117
+ typeof primaryDoc.content === "string"
118
+ ? primaryDoc.content
119
+ : new TextDecoder().decode(primaryDoc.content);
120
+
121
+ let frontmatterData: Record<string, unknown> = {};
122
+ let body = "";
123
+
124
+ try {
125
+ const parsed = matter(content);
126
+ frontmatterData = { ...parsed.data };
127
+ body = parsed.content.trim();
128
+ } catch {
129
+ diagnostics.push(
130
+ createDiagnostic("RS_CANONICAL_INVALID_FRONTMATTER", {
131
+ formatId: context.format.id,
132
+ message: `Failed to parse frontmatter in "${primaryDoc.path}".`,
133
+ source: { path: primaryDoc.path },
134
+ }),
135
+ );
136
+ body = content.trim();
137
+ }
138
+
139
+ accountant.consume(primaryDoc.path);
140
+ accountant.mapField(primaryDoc.path, "content", "body");
141
+
142
+ // Mark remaining files as preserved
143
+ for (const doc of sorted) {
144
+ if (doc.path !== primaryDoc.path && isClineFile(doc.path)) {
145
+ accountant.preserve(doc.path);
146
+ }
147
+ }
148
+
149
+ // Derive artifact name
150
+ const name = artifactNameHint ?? deriveArtifactName(primaryDoc.path);
151
+
152
+ // Build the canonical candidate
153
+ const candidate: Record<string, unknown> = {
154
+ name,
155
+ frontmatter: {
156
+ name,
157
+ ...frontmatterData,
158
+ type: "rule",
159
+ harnesses: ["cline"],
160
+ },
161
+ body,
162
+ hooks: [],
163
+ mcpServers: [],
164
+ workflows: [],
165
+ sourcePath: primaryDoc.path,
166
+ extraFields: {},
167
+ bodyOverrides: {},
168
+ };
169
+
170
+ return {
171
+ candidate,
172
+ diagnostics,
173
+ consumedPaths: accountant.getConsumedPaths(),
174
+ preservedPaths: accountant.getPreservedPaths(),
175
+ };
176
+ }