@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
package/src/parser.ts ADDED
@@ -0,0 +1,446 @@
1
+ import { exists, readdir, readFile } from "node:fs/promises";
2
+ import { basename, join, resolve } from "node:path";
3
+ import matter from "gray-matter";
4
+ import * as yaml from "js-yaml";
5
+ import { parseCanonical } from "./rosetta/canonical";
6
+ import {
7
+ type CanonicalHook,
8
+ type Frontmatter,
9
+ FrontmatterSchema,
10
+ HarnessNameSchema,
11
+ HooksFileSchema,
12
+ type KnowledgeArtifact,
13
+ type McpServerDefinition,
14
+ McpServersFileSchema,
15
+ type SourceDocument,
16
+ type ValidationError,
17
+ type WorkflowFile,
18
+ } from "./schemas";
19
+
20
+ /**
21
+ * @deprecated Use getKnownFrontmatterKeys() from rosetta/canonical.ts which
22
+ * derives keys from the FrontmatterSchema shape automatically.
23
+ */
24
+ const KNOWN_FRONTMATTER_FIELDS = new Set([
25
+ "name",
26
+ "displayName",
27
+ "description",
28
+ "keywords",
29
+ "author",
30
+ "version",
31
+ "harnesses",
32
+ "type",
33
+ "inclusion",
34
+ "file_patterns",
35
+ "harness-config",
36
+ "categories",
37
+ "ecosystem",
38
+ "depends",
39
+ "enhances",
40
+ // Bazaar manifest fields
41
+ "id",
42
+ "license",
43
+ "maturity",
44
+ "trust",
45
+ "risk-level",
46
+ "audience",
47
+ "model-assumptions",
48
+ "successor",
49
+ "replaces",
50
+ "changelog",
51
+ "collections",
52
+ "inherit-hooks",
53
+ "visibility",
54
+ "priority",
55
+ // Machine-managed distillation provenance (see ProvenanceRecordSchema).
56
+ "provenance",
57
+ // Curation-owned human/legal attribution (see AttributionRecordSchema).
58
+ "attribution",
59
+ ]);
60
+
61
+ export interface ParseResult<T> {
62
+ data: T;
63
+ warnings: string[];
64
+ }
65
+
66
+ export interface ParseError {
67
+ errors: ValidationError[];
68
+ }
69
+
70
+ function isParseError(
71
+ result: ParseResult<unknown> | ParseError,
72
+ ): result is ParseError {
73
+ return "errors" in result;
74
+ }
75
+
76
+ export { isParseError };
77
+
78
+ export async function parseKnowledgeMd(filePath: string): Promise<
79
+ | ParseResult<{
80
+ frontmatter: Frontmatter;
81
+ body: string;
82
+ extraFields: Record<string, unknown>;
83
+ harnessConfig: Record<string, unknown>;
84
+ }>
85
+ | ParseError
86
+ > {
87
+ const warnings: string[] = [];
88
+ let raw: string;
89
+ try {
90
+ raw = await readFile(filePath, "utf-8");
91
+ } catch {
92
+ return {
93
+ errors: [{ field: "knowledge.md", message: "File not found", filePath }],
94
+ };
95
+ }
96
+
97
+ let parsed: matter.GrayMatterFile<string>;
98
+ try {
99
+ parsed = matter(raw);
100
+ } catch (e: unknown) {
101
+ const msg = e instanceof Error ? e.message : String(e);
102
+ return {
103
+ errors: [
104
+ {
105
+ field: "frontmatter",
106
+ message: `Invalid YAML frontmatter: ${msg}`,
107
+ filePath,
108
+ },
109
+ ],
110
+ };
111
+ }
112
+
113
+ const rawData = parsed.data ?? {};
114
+ // Infer name from directory if not in frontmatter
115
+ if (!rawData.name) {
116
+ const dirName = basename(resolve(filePath, ".."));
117
+ rawData.name = dirName;
118
+ }
119
+
120
+ // Extract harness-config before validation
121
+ const harnessConfig: Record<string, unknown> =
122
+ rawData["harness-config"] ?? {};
123
+
124
+ // Separate extra fields from known fields
125
+ const extraFields: Record<string, unknown> = {};
126
+ for (const [key, value] of Object.entries(rawData)) {
127
+ if (!KNOWN_FRONTMATTER_FIELDS.has(key)) {
128
+ extraFields[key] = value;
129
+ }
130
+ }
131
+
132
+ const result = FrontmatterSchema.safeParse(rawData);
133
+ if (!result.success) {
134
+ const errors: ValidationError[] = result.error.issues.map((issue) => ({
135
+ field: issue.path.join(".") || "frontmatter",
136
+ message: issue.message,
137
+ filePath,
138
+ }));
139
+ return { errors };
140
+ }
141
+
142
+ return {
143
+ data: {
144
+ frontmatter: result.data,
145
+ body: parsed.content.trim(),
146
+ extraFields,
147
+ harnessConfig,
148
+ },
149
+ warnings,
150
+ };
151
+ }
152
+
153
+ export async function parseHooksYaml(
154
+ filePath: string,
155
+ ): Promise<ParseResult<CanonicalHook[]> | ParseError> {
156
+ const warnings: string[] = [];
157
+ let raw: string;
158
+ try {
159
+ raw = await readFile(filePath, "utf-8");
160
+ } catch {
161
+ // Missing hooks.yaml is fine — return empty
162
+ return { data: [], warnings };
163
+ }
164
+
165
+ let parsed: unknown;
166
+ try {
167
+ parsed = yaml.load(raw);
168
+ } catch (e: unknown) {
169
+ const msg = e instanceof Error ? e.message : String(e);
170
+ return {
171
+ errors: [
172
+ { field: "hooks.yaml", message: `Invalid YAML: ${msg}`, filePath },
173
+ ],
174
+ };
175
+ }
176
+
177
+ // Empty file or empty array
178
+ if (
179
+ parsed === null ||
180
+ parsed === undefined ||
181
+ (Array.isArray(parsed) && parsed.length === 0)
182
+ ) {
183
+ return { data: [], warnings };
184
+ }
185
+
186
+ const result = HooksFileSchema.safeParse(parsed);
187
+ if (!result.success) {
188
+ const errors: ValidationError[] = result.error.issues.map((issue) => ({
189
+ field: issue.path.join(".") || "hooks",
190
+ message: issue.message,
191
+ filePath,
192
+ }));
193
+ return { errors };
194
+ }
195
+
196
+ return { data: result.data, warnings };
197
+ }
198
+
199
+ export async function parseMcpServersYaml(
200
+ filePath: string,
201
+ ): Promise<ParseResult<McpServerDefinition[]> | ParseError> {
202
+ const warnings: string[] = [];
203
+ let raw: string;
204
+ try {
205
+ raw = await readFile(filePath, "utf-8");
206
+ } catch {
207
+ return { data: [], warnings };
208
+ }
209
+
210
+ let parsed: unknown;
211
+ try {
212
+ parsed = yaml.load(raw);
213
+ } catch (e: unknown) {
214
+ const msg = e instanceof Error ? e.message : String(e);
215
+ return {
216
+ errors: [
217
+ {
218
+ field: "mcp-servers.yaml",
219
+ message: `Invalid YAML: ${msg}`,
220
+ filePath,
221
+ },
222
+ ],
223
+ };
224
+ }
225
+
226
+ if (
227
+ parsed === null ||
228
+ parsed === undefined ||
229
+ (Array.isArray(parsed) && parsed.length === 0)
230
+ ) {
231
+ return { data: [], warnings };
232
+ }
233
+
234
+ const result = McpServersFileSchema.safeParse(parsed);
235
+ if (!result.success) {
236
+ const errors: ValidationError[] = result.error.issues.map((issue) => ({
237
+ field: issue.path.join(".") || "mcp-servers",
238
+ message: issue.message,
239
+ filePath,
240
+ }));
241
+ return { errors };
242
+ }
243
+
244
+ return { data: result.data, warnings };
245
+ }
246
+
247
+ async function collectWorkflowFiles(
248
+ workflowsDir: string,
249
+ prefix = "",
250
+ ): Promise<string[]> {
251
+ const entries = await readdir(join(workflowsDir, prefix), {
252
+ withFileTypes: true,
253
+ });
254
+ const files: string[] = [];
255
+
256
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
257
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
258
+ if (entry.isDirectory()) {
259
+ files.push(...(await collectWorkflowFiles(workflowsDir, relativePath)));
260
+ } else {
261
+ files.push(relativePath);
262
+ }
263
+ }
264
+
265
+ return files;
266
+ }
267
+
268
+ export async function parseWorkflows(
269
+ workflowsDir: string,
270
+ ): Promise<ParseResult<WorkflowFile[]>> {
271
+ const warnings: string[] = [];
272
+ const dirExists = await exists(workflowsDir);
273
+ if (!dirExists) {
274
+ return { data: [], warnings };
275
+ }
276
+
277
+ // Preserve nested reference trees and non-Markdown fixtures (for example
278
+ // CSV/TXT practice data) so adapters can reproduce an upstream skill's
279
+ // progressive-disclosure layout.
280
+ const filenames = await collectWorkflowFiles(workflowsDir);
281
+ const workflows: WorkflowFile[] = [];
282
+
283
+ for (const filename of filenames) {
284
+ const content = await readFile(join(workflowsDir, filename), "utf-8");
285
+ const name = filename
286
+ .replace(/\.[^./]+$/, "")
287
+ .replace(/[/-]/g, " ")
288
+ .replace(/\b\w/g, (c) => c.toUpperCase());
289
+ workflows.push({ name, filename, content: content.trim() });
290
+ }
291
+
292
+ return { data: workflows, warnings };
293
+ }
294
+
295
+ const BODY_OVERRIDE_RE = /^body\.(.+)\.md$/;
296
+
297
+ /**
298
+ * Scans an artifact directory for optional `body.<harness>.md` sibling files.
299
+ * Returns a map keyed by harness name → markdown body (frontmatter, if any, is
300
+ * discarded — the artifact's canonical frontmatter always wins). Files whose
301
+ * `<harness>` token is not a supported harness are ignored with a warning.
302
+ */
303
+ async function _parseBodyOverrides(
304
+ artifactDir: string,
305
+ ): Promise<ParseResult<Record<string, string>>> {
306
+ const warnings: string[] = [];
307
+ const overrides: Record<string, string> = {};
308
+
309
+ let entries: string[];
310
+ try {
311
+ const dirents = await readdir(artifactDir, { withFileTypes: true });
312
+ entries = dirents.filter((d) => d.isFile()).map((d) => d.name);
313
+ } catch {
314
+ return { data: overrides, warnings };
315
+ }
316
+
317
+ for (const filename of entries) {
318
+ const match = filename.match(BODY_OVERRIDE_RE);
319
+ if (!match) continue;
320
+ const harness = match[1];
321
+ if (!HarnessNameSchema.safeParse(harness).success) {
322
+ warnings.push(
323
+ `Ignoring "${filename}": "${harness}" is not a supported harness`,
324
+ );
325
+ continue;
326
+ }
327
+ const raw = await readFile(join(artifactDir, filename), "utf-8");
328
+ overrides[harness] = matter(raw).content.trim();
329
+ }
330
+
331
+ return { data: overrides, warnings };
332
+ }
333
+
334
+ export async function loadKnowledgeArtifact(
335
+ artifactDir: string,
336
+ ): Promise<ParseResult<KnowledgeArtifact> | ParseError> {
337
+ // Build in-memory SourceDocument[] from the filesystem
338
+ const documents = await readArtifactDocuments(artifactDir);
339
+
340
+ const artifactName = basename(artifactDir);
341
+
342
+ // Delegate to the pure canonical parser
343
+ const { artifact, diagnostics } = parseCanonical(documents, {
344
+ artifactNameHint: artifactName,
345
+ });
346
+
347
+ // Map pure parser diagnostics to legacy ParseError/ParseResult format
348
+ if (!artifact) {
349
+ const errors: ValidationError[] = diagnostics.map((d) => ({
350
+ field: d.source?.path ?? "artifact",
351
+ message: d.message,
352
+ filePath: d.source?.path ? join(artifactDir, d.source.path) : artifactDir,
353
+ }));
354
+ return { errors };
355
+ }
356
+
357
+ // The pure parser uses a logical sourcePath; the filesystem adapter
358
+ // overwrites it with the actual directory path for backward compatibility.
359
+ const result: KnowledgeArtifact = {
360
+ ...artifact,
361
+ sourcePath: artifactDir,
362
+ };
363
+
364
+ // Map non-blocking diagnostics to warnings
365
+ const warnings: string[] = diagnostics
366
+ .filter((d) => !d.blocking)
367
+ .map((d) => d.message);
368
+
369
+ return { data: result, warnings };
370
+ }
371
+
372
+ // ═══════════════════════════════════════════════════════════════════════════════
373
+ // Filesystem → SourceDocument[] adapter
374
+ // ═══════════════════════════════════════════════════════════════════════════════
375
+
376
+ /**
377
+ * Reads an artifact directory into an array of SourceDocuments suitable
378
+ * for the pure CanonicalParser. This is the impure filesystem boundary.
379
+ */
380
+ async function readArtifactDocuments(
381
+ artifactDir: string,
382
+ ): Promise<SourceDocument[]> {
383
+ const documents: SourceDocument[] = [];
384
+
385
+ // Read knowledge.md (required)
386
+ const knowledgeMdPath = join(artifactDir, "knowledge.md");
387
+ try {
388
+ const content = await readFile(knowledgeMdPath, "utf-8");
389
+ documents.push({ path: "knowledge.md", content, executable: false });
390
+ } catch {
391
+ // Missing knowledge.md — the pure parser will emit a diagnostic
392
+ }
393
+
394
+ // Read hooks.yaml (optional)
395
+ const hooksYamlPath = join(artifactDir, "hooks.yaml");
396
+ try {
397
+ const content = await readFile(hooksYamlPath, "utf-8");
398
+ documents.push({ path: "hooks.yaml", content, executable: false });
399
+ } catch {
400
+ // Missing is fine — optional
401
+ }
402
+
403
+ // Read mcp-servers.yaml (optional)
404
+ const mcpServersYamlPath = join(artifactDir, "mcp-servers.yaml");
405
+ try {
406
+ const content = await readFile(mcpServersYamlPath, "utf-8");
407
+ documents.push({ path: "mcp-servers.yaml", content, executable: false });
408
+ } catch {
409
+ // Missing is fine — optional
410
+ }
411
+
412
+ // Read workflows directory (optional)
413
+ const workflowsDir = join(artifactDir, "workflows");
414
+ const workflowsExist = await exists(workflowsDir);
415
+ if (workflowsExist) {
416
+ const workflowFiles = await collectWorkflowFiles(workflowsDir);
417
+ for (const filename of workflowFiles) {
418
+ const content = await readFile(join(workflowsDir, filename), "utf-8");
419
+ documents.push({
420
+ path: `workflows/${filename}`,
421
+ content,
422
+ executable: false,
423
+ });
424
+ }
425
+ }
426
+
427
+ // Read body override files (optional)
428
+ try {
429
+ const dirents = await readdir(artifactDir, { withFileTypes: true });
430
+ const bodyOverrideRe = /^body\..+\.md$/;
431
+ for (const dirent of dirents) {
432
+ if (dirent.isFile() && bodyOverrideRe.test(dirent.name)) {
433
+ const content = await readFile(join(artifactDir, dirent.name), "utf-8");
434
+ documents.push({
435
+ path: dirent.name,
436
+ content,
437
+ executable: false,
438
+ });
439
+ }
440
+ }
441
+ } catch {
442
+ // Directory read failure — body overrides are optional
443
+ }
444
+
445
+ return documents;
446
+ }