@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,520 @@
1
+ /**
2
+ * Provenance Backfill (Orchestration Shell)
3
+ *
4
+ * ORCHESTRATION LAYER — this module performs filesystem IO (reading upstream
5
+ * sources and the curated knowledge tree, rewriting `knowledge.md`, seeding the
6
+ * base cache) and therefore lives OUTSIDE `src/rosetta/**` (the
7
+ * Pure_Translation_Boundary). It composes the pure Rosetta Stone helpers rather
8
+ * than reimplementing digest, cache, or serialization logic:
9
+ *
10
+ * - `buildProvenanceRecord` / `writeBaseArtifact` / `AcquisitionContext`
11
+ * (src/base-cache.ts, task 19.4) — provenance assembly and the git-ignored
12
+ * base cache at `upstream/.kanon-base/<upstream>/<name>@<digest>`.
13
+ * - `serializeCanonical` (src/rosetta/canonical.ts) — the deterministic
14
+ * canonical byte plan re-rendered into the existing artifact directory.
15
+ * - the built-in source translators (src/rosetta/builtins/sources/*) — turn the
16
+ * current upstream source into the Theirs_Artifact whose digest becomes the
17
+ * recorded `baseDigest`.
18
+ *
19
+ * This is the ONE-TIME backfill described in ADR-0049: existing distilled
20
+ * artifacts carry no `ProvenanceRecord`, so until backfilled they fall back to
21
+ * skip-or-force on re-sync. Backfill matches each distilled artifact to the
22
+ * current upstream by NAME (the reduced-confidence path — no true base ancestor
23
+ * was recorded originally), records the CURRENT upstream `baseDigest` (accepting
24
+ * that any pre-existing drift is baked into the base), writes the provenance
25
+ * block into `knowledge.md`, and seeds the base cache so the first subsequent
26
+ * re-sync has a common ancestor for three-way reconciliation.
27
+ *
28
+ * It is the mechanical successor to the retired hand-maintained drift scripts
29
+ * (`compare-kiro-powers.sh`, `compare-kiro-powers-full.sh`, `diff-kiro-body.sh`,
30
+ * `diff-kiro-steering.sh`) whose hardcoded artifact maps and absolute paths had
31
+ * drifted from reality.
32
+ *
33
+ * Requirements: 18.1, 18.9
34
+ */
35
+
36
+ import { exists, readdir, readFile, writeFile } from "node:fs/promises";
37
+ import { basename, join } from "node:path";
38
+ import {
39
+ type AcquisitionContext,
40
+ buildProvenanceRecord,
41
+ writeBaseArtifact,
42
+ } from "./base-cache";
43
+ import { translateKiroPower } from "./rosetta/builtins/sources/kiro-power";
44
+ import { translateKiroSkill } from "./rosetta/builtins/sources/kiro-skill";
45
+ import { translateSuperpowers } from "./rosetta/builtins/sources/superpowers";
46
+ import { serializeCanonical } from "./rosetta/canonical";
47
+ import { codePointCompare } from "./rosetta/contracts";
48
+ import type { SourceTranslatorContext } from "./rosetta/registry";
49
+ import type {
50
+ FormatIdentifier,
51
+ KnowledgeArtifact,
52
+ NormalizedRelativePath,
53
+ SourceDocument,
54
+ } from "./schemas";
55
+
56
+ // ═══════════════════════════════════════════════════════════════════════════════
57
+ // Types
58
+ // ═══════════════════════════════════════════════════════════════════════════════
59
+
60
+ /** Source formats a backfill can distill from (path-based upstream layouts). */
61
+ export type BackfillSourceFormat = "kiro-power" | "kiro-skill" | "superpowers";
62
+
63
+ /**
64
+ * The Rosetta Stone Format_Contract identifier and version recorded in
65
+ * provenance for each source format, e.g. `kiro-power@1`. Mirrors the map in
66
+ * `import.ts` so a backfilled record is indistinguishable from an
67
+ * acquisition-time record.
68
+ */
69
+ const SOURCE_CONTRACT_IDENTIFIERS: Record<BackfillSourceFormat, string> = {
70
+ "kiro-power": "kiro-power@1",
71
+ "kiro-skill": "kiro-skill@1",
72
+ superpowers: "superpowers@1",
73
+ };
74
+
75
+ /** Primary marker file per source format, used to locate/validate a source dir. */
76
+ const PRIMARY_MARKER: Record<BackfillSourceFormat, string> = {
77
+ "kiro-power": "POWER.md",
78
+ "kiro-skill": "SKILL.md",
79
+ superpowers: "SKILL.md",
80
+ };
81
+
82
+ /**
83
+ * The per-artifact classification a backfill produces. Exactly one outcome is
84
+ * assigned to each distilled artifact considered.
85
+ */
86
+ export type BackfillOutcome =
87
+ | "backfilled"
88
+ | "skipped-has-provenance"
89
+ | "unmatched"
90
+ | "translation-failed";
91
+
92
+ /**
93
+ * The result of considering a single distilled artifact for backfill.
94
+ */
95
+ export interface BackfillEntry {
96
+ /** The distilled artifact name (its directory name under `knowledgeDir`). */
97
+ readonly name: string;
98
+ /** The classification assigned to this artifact. */
99
+ readonly outcome: BackfillOutcome;
100
+ /** The matched upstream source subdirectory name, when matched. */
101
+ readonly upstreamName?: string;
102
+ /** The `sha256:<hex>` base digest recorded, when backfilled. */
103
+ readonly baseDigest?: string;
104
+ /** The base-cache directory written, when backfilled and not a dry run. */
105
+ readonly baseCachePath?: string;
106
+ /** A human-readable explanation for skipped/unmatched/failed outcomes. */
107
+ readonly detail?: string;
108
+ }
109
+
110
+ /**
111
+ * Options for a single upstream's backfill pass.
112
+ */
113
+ export interface BackfillUpstreamOptions {
114
+ /** The upstream identifier — matches a key in config `upstreams`. */
115
+ readonly upstream: string;
116
+ /** Absolute path to the distilled knowledge directory (e.g. knowledge/kiro-official). */
117
+ readonly knowledgeDir: string;
118
+ /** Absolute path to the upstream source root (the subtree prefix directory). */
119
+ readonly upstreamRoot: string;
120
+ /** The source format the upstream is distilled from. */
121
+ readonly sourceFormat: BackfillSourceFormat;
122
+ /** The upstream revision recorded in provenance (subtree commit or a marker). */
123
+ readonly sourceRevision: string;
124
+ /** When true, classify without rewriting knowledge.md or seeding the cache. */
125
+ readonly dryRun: boolean;
126
+ /** Workspace root anchoring `upstream/.kanon-base/` (default: process.cwd()). */
127
+ readonly workspaceRoot?: string;
128
+ /**
129
+ * The import timestamp as an ISO-8601 string. Supplied by the caller so this
130
+ * orchestration stays reproducible in tests; defaults to now.
131
+ */
132
+ readonly importedAt?: string;
133
+ }
134
+
135
+ /**
136
+ * The full result of a backfill pass across one upstream.
137
+ */
138
+ export interface BackfillResult {
139
+ /** The upstream that was backfilled. */
140
+ readonly upstream: string;
141
+ /** Per-artifact entries, ordered by artifact name (code-point order). */
142
+ readonly entries: readonly BackfillEntry[];
143
+ /** Count of artifacts that received a provenance record. */
144
+ readonly backfilledCount: number;
145
+ /** Count skipped because they already carry provenance. */
146
+ readonly skippedCount: number;
147
+ /** Count with no matching upstream source by name. */
148
+ readonly unmatchedCount: number;
149
+ /** Count whose upstream source failed to translate. */
150
+ readonly failedCount: number;
151
+ }
152
+
153
+ // ═══════════════════════════════════════════════════════════════════════════════
154
+ // Source-document building (impure)
155
+ // ═══════════════════════════════════════════════════════════════════════════════
156
+
157
+ /**
158
+ * Read an upstream source directory into an in-memory SourceDocument[] for the
159
+ * matching source translator. Mirrors the layout each translator expects:
160
+ * `POWER.md` + `steering/`, `SKILL.md` + `references/`, or `SKILL.md` +
161
+ * companion Markdown.
162
+ */
163
+ async function buildSourceDocuments(
164
+ sourceDir: string,
165
+ format: BackfillSourceFormat,
166
+ ): Promise<SourceDocument[]> {
167
+ const documents: SourceDocument[] = [];
168
+ const entries = await readdir(sourceDir);
169
+
170
+ if (format === "kiro-power") {
171
+ if (entries.includes("POWER.md")) {
172
+ const content = await readFile(join(sourceDir, "POWER.md"), "utf-8");
173
+ documents.push({
174
+ path: "POWER.md" as NormalizedRelativePath,
175
+ content,
176
+ executable: false,
177
+ });
178
+ }
179
+ const steeringDir = join(sourceDir, "steering");
180
+ if (await exists(steeringDir)) {
181
+ const steeringFiles = (await readdir(steeringDir))
182
+ .filter((f) => f.endsWith(".md"))
183
+ .sort(codePointCompare);
184
+ for (const file of steeringFiles) {
185
+ const content = await readFile(join(steeringDir, file), "utf-8");
186
+ documents.push({
187
+ path: `steering/${file}` as NormalizedRelativePath,
188
+ content,
189
+ executable: false,
190
+ });
191
+ }
192
+ }
193
+ } else if (format === "kiro-skill") {
194
+ if (entries.includes("SKILL.md")) {
195
+ const content = await readFile(join(sourceDir, "SKILL.md"), "utf-8");
196
+ documents.push({
197
+ path: "SKILL.md" as NormalizedRelativePath,
198
+ content,
199
+ executable: false,
200
+ });
201
+ }
202
+ const refsDir = join(sourceDir, "references");
203
+ if (await exists(refsDir)) {
204
+ const refFiles = (await readdir(refsDir))
205
+ .filter((f) => f.endsWith(".md"))
206
+ .sort(codePointCompare);
207
+ for (const file of refFiles) {
208
+ const content = await readFile(join(refsDir, file), "utf-8");
209
+ documents.push({
210
+ path: `references/${file}` as NormalizedRelativePath,
211
+ content,
212
+ executable: false,
213
+ });
214
+ }
215
+ }
216
+ } else {
217
+ // superpowers
218
+ if (entries.includes("SKILL.md")) {
219
+ const content = await readFile(join(sourceDir, "SKILL.md"), "utf-8");
220
+ documents.push({
221
+ path: "SKILL.md" as NormalizedRelativePath,
222
+ content,
223
+ executable: false,
224
+ });
225
+ }
226
+ const additionalMd = entries
227
+ .filter((f) => f.endsWith(".md") && f !== "SKILL.md")
228
+ .sort(codePointCompare);
229
+ for (const file of additionalMd) {
230
+ const content = await readFile(join(sourceDir, file), "utf-8");
231
+ documents.push({
232
+ path: file as NormalizedRelativePath,
233
+ content,
234
+ executable: false,
235
+ });
236
+ }
237
+ }
238
+
239
+ return documents;
240
+ }
241
+
242
+ // ═══════════════════════════════════════════════════════════════════════════════
243
+ // Translation dispatch (pure over documents)
244
+ // ═══════════════════════════════════════════════════════════════════════════════
245
+
246
+ /**
247
+ * Translate an upstream source document set into a Theirs_Artifact via the
248
+ * appropriate built-in source translator. Returns undefined when translation
249
+ * produced no candidate.
250
+ */
251
+ function translateSource(
252
+ documents: readonly SourceDocument[],
253
+ format: BackfillSourceFormat,
254
+ artifactNameHint: string,
255
+ ): KnowledgeArtifact | undefined {
256
+ const context: SourceTranslatorContext = {
257
+ format: {
258
+ id: format as FormatIdentifier,
259
+ } as SourceTranslatorContext["format"],
260
+ canonicalSchemaVersion: "1.0.0",
261
+ options: {},
262
+ callerContext: { artifactNameHint },
263
+ };
264
+
265
+ let output: ReturnType<typeof translateKiroPower>;
266
+ switch (format) {
267
+ case "kiro-power":
268
+ output = translateKiroPower(documents, context);
269
+ break;
270
+ case "kiro-skill":
271
+ output = translateKiroSkill(documents, context);
272
+ break;
273
+ case "superpowers":
274
+ output = translateSuperpowers(documents, context);
275
+ break;
276
+ }
277
+
278
+ if (!output.candidate) {
279
+ return undefined;
280
+ }
281
+ return output.candidate as unknown as KnowledgeArtifact;
282
+ }
283
+
284
+ // ═══════════════════════════════════════════════════════════════════════════════
285
+ // knowledge.md rewrite (impure)
286
+ // ═══════════════════════════════════════════════════════════════════════════════
287
+
288
+ /**
289
+ * Rewrite an existing distilled artifact's `knowledge.md` with the machine-
290
+ * managed provenance block added to its frontmatter. The full artifact is
291
+ * re-serialized deterministically via the Canonical_Serializer so the written
292
+ * bytes are stable; only `knowledge.md` is rewritten (the curated auxiliary
293
+ * files, workflows, and body overrides are left untouched).
294
+ *
295
+ * @returns true when knowledge.md was written; false when re-serialization
296
+ * failed (in which case the caller treats the artifact as unwritten).
297
+ */
298
+ async function writeProvenanceIntoKnowledgeMd(
299
+ artifactDir: string,
300
+ distilled: KnowledgeArtifact,
301
+ ): Promise<boolean> {
302
+ const { plan } = serializeCanonical(distilled, {
303
+ emitEmptyAuxiliaryFiles: false,
304
+ emitBodyOverrides: false,
305
+ emitWorkflows: false,
306
+ });
307
+
308
+ if (!plan) {
309
+ return false;
310
+ }
311
+
312
+ const knowledgeFile = plan.outputFiles.find(
313
+ (f) => f.relativePath === "knowledge.md",
314
+ );
315
+ if (!knowledgeFile) {
316
+ return false;
317
+ }
318
+
319
+ const content =
320
+ typeof knowledgeFile.content === "string"
321
+ ? knowledgeFile.content
322
+ : new TextDecoder().decode(knowledgeFile.content);
323
+
324
+ await writeFile(join(artifactDir, "knowledge.md"), content, "utf-8");
325
+ return true;
326
+ }
327
+
328
+ // ═══════════════════════════════════════════════════════════════════════════════
329
+ // Single-artifact backfill
330
+ // ═══════════════════════════════════════════════════════════════════════════════
331
+
332
+ /**
333
+ * Loader that parses a distilled artifact directory into a KnowledgeArtifact.
334
+ * Injected so tests can supply artifacts without a real filesystem parse, and so
335
+ * this module does not couple to the parser's exact result shape. Returns
336
+ * undefined when the directory does not parse into a valid artifact.
337
+ */
338
+ export type DistilledArtifactLoader = (
339
+ artifactDir: string,
340
+ ) => Promise<KnowledgeArtifact | undefined>;
341
+
342
+ interface BackfillOneOptions extends BackfillUpstreamOptions {
343
+ readonly loadDistilled: DistilledArtifactLoader;
344
+ }
345
+
346
+ /**
347
+ * Consider a single distilled artifact for backfill and return its classified
348
+ * entry, performing writes only when not a dry run.
349
+ */
350
+ async function backfillOne(
351
+ distilledDir: string,
352
+ options: BackfillOneOptions,
353
+ ): Promise<BackfillEntry> {
354
+ const name = basename(distilledDir);
355
+
356
+ const distilled = await options.loadDistilled(distilledDir);
357
+ if (!distilled) {
358
+ return {
359
+ name,
360
+ outcome: "translation-failed",
361
+ detail: `Could not parse distilled artifact at ${distilledDir}`,
362
+ };
363
+ }
364
+
365
+ // Already provenanced → skip (backfill is one-time and never overwrites an
366
+ // existing machine-managed record).
367
+ if (distilled.frontmatter.provenance) {
368
+ return {
369
+ name,
370
+ outcome: "skipped-has-provenance",
371
+ detail: "Artifact already carries a provenance record",
372
+ };
373
+ }
374
+
375
+ // Match to the current upstream by NAME (reduced-confidence path).
376
+ const upstreamSourceDir = join(options.upstreamRoot, name);
377
+ const marker = PRIMARY_MARKER[options.sourceFormat];
378
+ const matched =
379
+ (await exists(upstreamSourceDir)) &&
380
+ (await exists(join(upstreamSourceDir, marker)));
381
+
382
+ if (!matched) {
383
+ return {
384
+ name,
385
+ outcome: "unmatched",
386
+ detail: `No upstream source directory "${name}" with ${marker} under ${options.upstreamRoot}`,
387
+ };
388
+ }
389
+
390
+ // Translate the CURRENT upstream source into a Theirs_Artifact.
391
+ const documents = await buildSourceDocuments(
392
+ upstreamSourceDir,
393
+ options.sourceFormat,
394
+ );
395
+ const theirs = translateSource(documents, options.sourceFormat, name);
396
+ if (!theirs) {
397
+ return {
398
+ name,
399
+ outcome: "translation-failed",
400
+ upstreamName: name,
401
+ detail: `Upstream source "${name}" failed to translate`,
402
+ };
403
+ }
404
+
405
+ // Compute the baseDigest and build the provenance record from the current
406
+ // upstream. Reuses the pure task 19.4 helper; the digest is over the
407
+ // provenance-free Theirs_Artifact.
408
+ const acquisitionContext: AcquisitionContext = {
409
+ upstream: options.upstream,
410
+ sourcePath: name,
411
+ sourceFormat: options.sourceFormat as FormatIdentifier,
412
+ sourceRevision: options.sourceRevision,
413
+ contract: SOURCE_CONTRACT_IDENTIFIERS[options.sourceFormat],
414
+ importedAt: options.importedAt ?? new Date().toISOString(),
415
+ };
416
+
417
+ const { provenance } = buildProvenanceRecord(theirs, acquisitionContext);
418
+ if (!provenance) {
419
+ return {
420
+ name,
421
+ outcome: "translation-failed",
422
+ upstreamName: name,
423
+ detail: `Could not compute base digest for upstream source "${name}"`,
424
+ };
425
+ }
426
+
427
+ if (options.dryRun) {
428
+ return {
429
+ name,
430
+ outcome: "backfilled",
431
+ upstreamName: name,
432
+ baseDigest: provenance.baseDigest,
433
+ };
434
+ }
435
+
436
+ // Write the provenance block into the distilled artifact's knowledge.md.
437
+ const withProvenance: KnowledgeArtifact = {
438
+ ...distilled,
439
+ frontmatter: { ...distilled.frontmatter, provenance },
440
+ };
441
+ const wrote = await writeProvenanceIntoKnowledgeMd(
442
+ distilledDir,
443
+ withProvenance,
444
+ );
445
+ if (!wrote) {
446
+ return {
447
+ name,
448
+ outcome: "translation-failed",
449
+ upstreamName: name,
450
+ detail: `Could not rewrite knowledge.md for "${name}"`,
451
+ };
452
+ }
453
+
454
+ // Seed the base cache with the normalized Theirs_Artifact (the exact content
455
+ // the recorded baseDigest fingerprints) so the first re-sync has a base.
456
+ const workspaceRoot = options.workspaceRoot ?? process.cwd();
457
+ const cacheResult = await writeBaseArtifact(
458
+ theirs,
459
+ provenance.baseDigest,
460
+ { upstream: options.upstream },
461
+ workspaceRoot,
462
+ );
463
+
464
+ return {
465
+ name,
466
+ outcome: "backfilled",
467
+ upstreamName: name,
468
+ baseDigest: provenance.baseDigest,
469
+ baseCachePath: cacheResult.cachePath,
470
+ };
471
+ }
472
+
473
+ // ═══════════════════════════════════════════════════════════════════════════════
474
+ // Upstream backfill orchestration
475
+ // ═══════════════════════════════════════════════════════════════════════════════
476
+
477
+ /**
478
+ * Backfill provenance for every distilled artifact under one upstream's
479
+ * knowledge directory.
480
+ *
481
+ * Deterministic: distilled artifacts are processed in code-point name order and
482
+ * the resulting entries preserve that order, so a dry run and a subsequent write
483
+ * run report the same sequence.
484
+ *
485
+ * @param options The upstream backfill options.
486
+ * @param loadDistilled Loader that parses a distilled artifact directory.
487
+ * @returns The classified per-artifact entries and aggregate counts.
488
+ */
489
+ export async function backfillUpstream(
490
+ options: BackfillUpstreamOptions,
491
+ loadDistilled: DistilledArtifactLoader,
492
+ ): Promise<BackfillResult> {
493
+ const distilledEntries = (
494
+ await readdir(options.knowledgeDir, { withFileTypes: true })
495
+ )
496
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
497
+ .map((e) => e.name)
498
+ .sort(codePointCompare);
499
+
500
+ const entries: BackfillEntry[] = [];
501
+ for (const distilledName of distilledEntries) {
502
+ const distilledDir = join(options.knowledgeDir, distilledName);
503
+ const entry = await backfillOne(distilledDir, {
504
+ ...options,
505
+ loadDistilled,
506
+ });
507
+ entries.push(entry);
508
+ }
509
+
510
+ return {
511
+ upstream: options.upstream,
512
+ entries,
513
+ backfilledCount: entries.filter((e) => e.outcome === "backfilled").length,
514
+ skippedCount: entries.filter((e) => e.outcome === "skipped-has-provenance")
515
+ .length,
516
+ unmatchedCount: entries.filter((e) => e.outcome === "unmatched").length,
517
+ failedCount: entries.filter((e) => e.outcome === "translation-failed")
518
+ .length,
519
+ };
520
+ }