@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,502 @@
1
+ /**
2
+ * Reconcile Orchestrator — the `reconcile` collision policy for the sync shell.
3
+ *
4
+ * This impure orchestration module sits between the sync shell and the pure
5
+ * reconciliation core (`src/rosetta/reconcile.ts`, task 19.2) plus its digest
6
+ * helpers (`src/rosetta/provenance-digest.ts`, task 19.3). It is the mechanical
7
+ * replacement for the retired hand-maintained drift scripts (ADR-0049 /
8
+ * ADR-RS-007).
9
+ *
10
+ * Responsibilities (Requirements 18.3, 18.9, 18.10, 18.15, 18.17):
11
+ * 1. Derive the reconcile set from `ProvenanceRecord`s — NOT a hardcoded map.
12
+ * Only Ours artifacts that carry provenance participate in three-way
13
+ * reconciliation; provenance-less artifacts are excluded and left to the
14
+ * existing collision behavior (Requirement 18.17).
15
+ * 2. Classify `orphaned` (a provenanced Ours artifact whose upstream source is
16
+ * no longer present, Requirement 18.9) and `new` (an acquired upstream
17
+ * artifact with no Ours counterpart by provenance identity,
18
+ * Requirement 18.10).
19
+ * 3. For each provenanced collision, self-verify the recorded base against the
20
+ * reconstructed base (task 19.3), run the pure `reconcileArtifact`, then
21
+ * serialize the merged artifact through the EXISTING canonical serializer
22
+ * and plan validation path (`serializeCanonical` + `validatePlan`) so the
23
+ * result flows through the same validation, path checks, and application as
24
+ * any other write. This module never bypasses plan validation.
25
+ * 4. Build a deterministic `ReconciliationReport` ordered by outcome, then
26
+ * upstream, then artifact name (Requirement 18.15), renderable as human
27
+ * text and versioned JSON by `reconcile-report-renderer.ts`.
28
+ *
29
+ * BOUNDARY: This module is impure by design — it is the orchestration shell, not
30
+ * the pure core. It reconstructs the Base_Artifact by invoking a caller-supplied
31
+ * loader (the base-cache read at `upstream/.kanon-base/<upstream>/<name>@<digest>`
32
+ * is owned by task 19.4; this module only *reads through* the injected loader and
33
+ * does not reimplement the write). The pure merge, digest, and self-verification
34
+ * remain in `src/rosetta/`.
35
+ *
36
+ * Requirements: 18.3, 18.9, 18.10, 18.15, 18.17
37
+ */
38
+
39
+ import {
40
+ type CanonicalSerializerOptions,
41
+ serializeCanonical,
42
+ } from "./rosetta/canonical";
43
+ import { codePointCompare } from "./rosetta/contracts";
44
+ import { validatePlan } from "./rosetta/plan";
45
+ import {
46
+ readProvenance,
47
+ selfVerifyReconcileInput,
48
+ } from "./rosetta/provenance-digest";
49
+ import { type ReconcileInput, reconcileArtifact } from "./rosetta/reconcile";
50
+ import type {
51
+ FieldOwnershipPolicy,
52
+ KnowledgeArtifact,
53
+ ProvenanceRecord,
54
+ ReconciliationDiagnostic,
55
+ ReconciliationOutcome,
56
+ ReconciliationReport,
57
+ ReconciliationReportEntry,
58
+ ReconciliationResult,
59
+ TranslationDiagnostic,
60
+ TranslationPlan,
61
+ } from "./schemas";
62
+ import { DEFAULT_FIELD_OWNERSHIP_POLICY } from "./schemas";
63
+
64
+ // ═══════════════════════════════════════════════════════════════════════════════
65
+ // Public inputs
66
+ // ═══════════════════════════════════════════════════════════════════════════════
67
+
68
+ /**
69
+ * A loader that reconstructs the recorded Base_Artifact for a given Ours
70
+ * artifact. The concrete cache read (git-ignored
71
+ * `upstream/.kanon-base/<upstream>/<name>@<digest>`) is owned by task 19.4; this
72
+ * module consumes whatever the loader returns. Returning `undefined` signals a
73
+ * cache miss, which routes the artifact to the reduced-confidence two-way path
74
+ * (Requirement 18.11). The loader MAY be async (it usually reads a file).
75
+ */
76
+ export type BaseArtifactLoader = (
77
+ ours: KnowledgeArtifact,
78
+ provenance: ProvenanceRecord,
79
+ ) => Promise<KnowledgeArtifact | undefined> | KnowledgeArtifact | undefined;
80
+
81
+ /**
82
+ * Inputs to a single upstream's reconciliation pass.
83
+ */
84
+ export interface ReconcileUpstreamOptions {
85
+ /** The upstream identifier these artifacts belong to (report ordering key). */
86
+ readonly upstream: string;
87
+ /**
88
+ * The current curated artifacts in the canonical knowledge tree (Ours). Only
89
+ * those carrying a `ProvenanceRecord` for `upstream` participate; the rest are
90
+ * excluded (Requirement 18.17) and surfaced via `excludedArtifactNames`.
91
+ */
92
+ readonly ours: readonly KnowledgeArtifact[];
93
+ /**
94
+ * The freshly translated upstream artifacts (Theirs), keyed for identity by
95
+ * their provenance `sourcePath` when they carry one, otherwise by name.
96
+ */
97
+ readonly theirs: readonly KnowledgeArtifact[];
98
+ /** Per-upstream field-ownership policy; defaults to the documented default. */
99
+ readonly policy?: FieldOwnershipPolicy;
100
+ /** Reconstructs the recorded base for an Ours artifact (cache read, task 19.4). */
101
+ readonly loadBase: BaseArtifactLoader;
102
+ /** Serializer options; must match those used at import for digest stability. */
103
+ readonly serializerOptions?: CanonicalSerializerOptions;
104
+ }
105
+
106
+ /**
107
+ * The reconciliation of a single artifact plus the validated plan produced for
108
+ * its merged result. `plan` is `undefined` for `orphaned` artifacts (nothing to
109
+ * write) and when the merged artifact failed serialization/validation — in the
110
+ * latter case `planDiagnostics` explains why.
111
+ */
112
+ export interface ReconciledArtifact {
113
+ /** Report identity: upstream and artifact name. */
114
+ readonly upstream: string;
115
+ readonly artifactName: string;
116
+ /** The pure reconciliation result (merged artifact, outcome, diagnostics). */
117
+ readonly result: ReconciliationResult;
118
+ /** The validated canonical plan for the merged artifact, or undefined. */
119
+ readonly plan: TranslationPlan | undefined;
120
+ /** Diagnostics from serializing/validating the merged artifact's plan. */
121
+ readonly planDiagnostics: readonly TranslationDiagnostic[];
122
+ }
123
+
124
+ /**
125
+ * The full result of reconciling one or more upstreams: the deterministic
126
+ * report, the per-artifact plans to apply, and the names of provenance-less
127
+ * artifacts that were excluded from reconciliation (Requirement 18.17).
128
+ */
129
+ export interface ReconcileOrchestrationResult {
130
+ /** Deterministic report ordered by outcome, upstream, then artifact name. */
131
+ readonly report: ReconciliationReport;
132
+ /** Per-artifact reconciliations with their validated plans, in report order. */
133
+ readonly reconciled: readonly ReconciledArtifact[];
134
+ /** Names of Ours artifacts excluded because they carry no ProvenanceRecord. */
135
+ readonly excludedArtifactNames: readonly string[];
136
+ }
137
+
138
+ // ═══════════════════════════════════════════════════════════════════════════════
139
+ // Identity helpers
140
+ // ═══════════════════════════════════════════════════════════════════════════════
141
+
142
+ /**
143
+ * The identity of an artifact for reconciliation matching. Two artifacts are the
144
+ * same distilled artifact when their provenance `sourcePath` matches for the
145
+ * same upstream; when Theirs carries no provenance we fall back to matching on
146
+ * artifact name so a freshly translated upstream artifact still pairs with its
147
+ * curated counterpart.
148
+ */
149
+ function oursIdentity(provenance: ProvenanceRecord): string {
150
+ return provenance.sourcePath;
151
+ }
152
+
153
+ /**
154
+ * Derive the identity key for a Theirs artifact. Prefer its provenance
155
+ * `sourcePath` (set when the source translator recorded provenance), else the
156
+ * artifact name — matching the `sourcePath` the Ours provenance recorded at
157
+ * import for the same source subpath.
158
+ */
159
+ function theirsIdentity(theirs: KnowledgeArtifact): string {
160
+ const provenance = readProvenance(theirs);
161
+ return provenance?.sourcePath ?? theirs.name;
162
+ }
163
+
164
+ // ═══════════════════════════════════════════════════════════════════════════════
165
+ // Merged-artifact plan production (through the existing validation path)
166
+ // ═══════════════════════════════════════════════════════════════════════════════
167
+
168
+ /**
169
+ * Serialize a merged artifact through the existing canonical serializer and
170
+ * validate the resulting plan through `validatePlan`. This deliberately reuses
171
+ * the same path every other write uses so a reconciled artifact is subject to
172
+ * identical schema, path, and collision validation (Requirement 18.3, design
173
+ * "Boundary" note). Returns the validated plan (or undefined) plus diagnostics.
174
+ */
175
+ function planForMergedArtifact(
176
+ merged: KnowledgeArtifact,
177
+ serializerOptions: CanonicalSerializerOptions | undefined,
178
+ ): {
179
+ plan: TranslationPlan | undefined;
180
+ diagnostics: readonly TranslationDiagnostic[];
181
+ } {
182
+ const { plan: serializedPlan, diagnostics: serializeDiagnostics } =
183
+ serializeCanonical(merged, serializerOptions);
184
+
185
+ if (!serializedPlan) {
186
+ return { plan: undefined, diagnostics: serializeDiagnostics };
187
+ }
188
+
189
+ const validation = validatePlan(serializedPlan);
190
+ const diagnostics: TranslationDiagnostic[] = [
191
+ ...serializeDiagnostics,
192
+ ...validation.diagnostics,
193
+ ];
194
+
195
+ if (!validation.valid || !validation.plan) {
196
+ return { plan: undefined, diagnostics };
197
+ }
198
+
199
+ return { plan: validation.plan, diagnostics };
200
+ }
201
+
202
+ // ═══════════════════════════════════════════════════════════════════════════════
203
+ // orphaned / new synthesis
204
+ // ═══════════════════════════════════════════════════════════════════════════════
205
+
206
+ const ORPHANED_CODE = "RS_RECONCILE_ORPHANED";
207
+ const NEW_CODE = "RS_RECONCILE_NEW";
208
+
209
+ /**
210
+ * Build an `orphaned` reconciliation result for an Ours artifact whose recorded
211
+ * upstream source is absent from the freshly acquired upstream. The artifact is
212
+ * neither deleted nor overwritten (Requirement 18.9); Ours is preserved verbatim.
213
+ */
214
+ function orphanedResult(ours: KnowledgeArtifact): ReconciliationResult {
215
+ const diagnostic: ReconciliationDiagnostic = {
216
+ code: ORPHANED_CODE,
217
+ severity: "warning",
218
+ phase: "source-translation",
219
+ message: `Artifact "${ours.name}" carries provenance but its recorded upstream source is no longer present upstream; it was left untouched.`,
220
+ remediation:
221
+ "Confirm whether the upstream removed this artifact. If so, decide whether to retire the curated copy; nothing was changed automatically.",
222
+ canonical: {
223
+ artifactName: ours.name,
224
+ fieldPath: "frontmatter.provenance",
225
+ },
226
+ unavailableDetails: [],
227
+ blocking: false,
228
+ field: "provenance",
229
+ fieldClass: "machine-owned",
230
+ outcome: "orphaned",
231
+ baseValuePresent: false,
232
+ confidence: "reduced",
233
+ };
234
+ return {
235
+ artifact: ours,
236
+ outcome: "orphaned",
237
+ diagnostics: [diagnostic],
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Build a `new` reconciliation result for an acquired upstream artifact with no
243
+ * Ours counterpart by provenance identity (Requirement 18.10). Theirs is carried
244
+ * as the artifact so a downstream import decision has the candidate to hand; no
245
+ * curation exists to preserve.
246
+ */
247
+ function newResult(theirs: KnowledgeArtifact): ReconciliationResult {
248
+ const diagnostic: ReconciliationDiagnostic = {
249
+ code: NEW_CODE,
250
+ severity: "info",
251
+ phase: "source-translation",
252
+ message: `Upstream artifact "${theirs.name}" has no curated counterpart; it is new.`,
253
+ remediation:
254
+ "Import this artifact if it should be distilled into the curated tree.",
255
+ canonical: {
256
+ artifactName: theirs.name,
257
+ fieldPath: "frontmatter.provenance",
258
+ },
259
+ unavailableDetails: [],
260
+ blocking: false,
261
+ field: "provenance",
262
+ fieldClass: "machine-owned",
263
+ outcome: "new",
264
+ baseValuePresent: false,
265
+ confidence: "reduced",
266
+ };
267
+ return {
268
+ artifact: theirs,
269
+ outcome: "new",
270
+ diagnostics: [diagnostic],
271
+ };
272
+ }
273
+
274
+ // ═══════════════════════════════════════════════════════════════════════════════
275
+ // Deterministic report ordering
276
+ // ═══════════════════════════════════════════════════════════════════════════════
277
+
278
+ /**
279
+ * Stable ordering of outcomes in the report. This is a fixed, documented order
280
+ * (Requirement 18.15 requires ordering *by* outcome; the concrete precedence is
281
+ * conflict-first so the most actionable rows surface at the top of a human
282
+ * render, followed by the remaining outcomes in a stable sequence).
283
+ */
284
+ const OUTCOME_ORDER: Record<ReconciliationOutcome, number> = {
285
+ conflict: 0,
286
+ merged: 1,
287
+ "fast-forward": 2,
288
+ new: 3,
289
+ orphaned: 4,
290
+ clean: 5,
291
+ };
292
+
293
+ /**
294
+ * Sort report entries deterministically by outcome, then upstream, then artifact
295
+ * name (Requirement 18.15). Independent of input order.
296
+ */
297
+ function sortReportEntries(
298
+ entries: readonly ReconciliationReportEntry[],
299
+ ): ReconciliationReportEntry[] {
300
+ return [...entries].sort((a, b) => {
301
+ const outcomeCmp =
302
+ OUTCOME_ORDER[a.result.outcome] - OUTCOME_ORDER[b.result.outcome];
303
+ if (outcomeCmp !== 0) return outcomeCmp;
304
+ const upstreamCmp = codePointCompare(a.upstream, b.upstream);
305
+ if (upstreamCmp !== 0) return upstreamCmp;
306
+ return codePointCompare(a.artifactName, b.artifactName);
307
+ });
308
+ }
309
+
310
+ // ═══════════════════════════════════════════════════════════════════════════════
311
+ // Single-upstream reconciliation
312
+ // ═══════════════════════════════════════════════════════════════════════════════
313
+
314
+ /**
315
+ * Reconcile every provenanced Ours artifact for a single upstream against the
316
+ * freshly translated Theirs artifacts, classifying orphaned/new and producing a
317
+ * validated plan per merged artifact.
318
+ *
319
+ * @returns The per-artifact reconciliations and the excluded (provenance-less)
320
+ * artifact names for this upstream.
321
+ */
322
+ export async function reconcileUpstream(
323
+ options: ReconcileUpstreamOptions,
324
+ ): Promise<{
325
+ reconciled: ReconciledArtifact[];
326
+ excludedArtifactNames: string[];
327
+ }> {
328
+ const {
329
+ upstream,
330
+ ours,
331
+ theirs,
332
+ loadBase,
333
+ serializerOptions,
334
+ policy = DEFAULT_FIELD_OWNERSHIP_POLICY,
335
+ } = options;
336
+
337
+ const reconciled: ReconciledArtifact[] = [];
338
+ const excludedArtifactNames: string[] = [];
339
+
340
+ // Index Theirs by identity so each Ours can find its counterpart and so we
341
+ // can report leftover Theirs as `new`.
342
+ const theirsByIdentity = new Map<string, KnowledgeArtifact>();
343
+ for (const t of theirs) {
344
+ theirsByIdentity.set(theirsIdentity(t), t);
345
+ }
346
+ const matchedTheirsIdentities = new Set<string>();
347
+
348
+ // Deterministic iteration over Ours by name so async base loads run in a
349
+ // stable order and the result list is stable before the final report sort.
350
+ const sortedOurs = [...ours].sort((a, b) => codePointCompare(a.name, b.name));
351
+
352
+ for (const oursArtifact of sortedOurs) {
353
+ const provenance = readProvenance(oursArtifact);
354
+
355
+ // Provenance-less artifacts are excluded from reconciliation entirely
356
+ // (Requirement 18.17) — the sync shell falls back to existing collision
357
+ // behavior for them.
358
+ if (!provenance || provenance.upstream !== upstream) {
359
+ if (!provenance) {
360
+ excludedArtifactNames.push(oursArtifact.name);
361
+ }
362
+ continue;
363
+ }
364
+
365
+ const identity = oursIdentity(provenance);
366
+ const theirsArtifact = theirsByIdentity.get(identity);
367
+
368
+ if (!theirsArtifact) {
369
+ // Recorded upstream source is gone → orphaned (Requirement 18.9).
370
+ const result = orphanedResult(oursArtifact);
371
+ reconciled.push({
372
+ upstream,
373
+ artifactName: oursArtifact.name,
374
+ result,
375
+ plan: undefined,
376
+ planDiagnostics: [],
377
+ });
378
+ continue;
379
+ }
380
+
381
+ matchedTheirsIdentities.add(identity);
382
+
383
+ // Reconstruct the recorded base (cache read owned by task 19.4). A miss
384
+ // leaves `base` undefined so the pure core degrades to the two-way path.
385
+ const base = await loadBase(oursArtifact, provenance);
386
+
387
+ // Self-verify the recorded base against Ours; a hand-edited provenance
388
+ // forces the reduced-confidence path (Requirement 18.16). When there is no
389
+ // base to check the request is unchanged.
390
+ const baseInput: ReconcileInput = {
391
+ base,
392
+ ours: oursArtifact,
393
+ theirs: theirsArtifact,
394
+ policy,
395
+ };
396
+ const { input } = selfVerifyReconcileInput(baseInput, serializerOptions);
397
+
398
+ const result = reconcileArtifact(input);
399
+
400
+ // Serialize + validate the merged artifact through the EXISTING path.
401
+ // Orphaned/clean artifacts still produce a plan (identical bytes for a
402
+ // clean outcome), which the applier's reconcile policy will no-op or
403
+ // overwrite as appropriate; conflicts still apply non-conflicting fields.
404
+ const { plan, diagnostics: planDiagnostics } = planForMergedArtifact(
405
+ result.artifact,
406
+ serializerOptions,
407
+ );
408
+
409
+ reconciled.push({
410
+ upstream,
411
+ artifactName: oursArtifact.name,
412
+ result,
413
+ plan,
414
+ planDiagnostics,
415
+ });
416
+ }
417
+
418
+ // Any Theirs identity never matched by an Ours provenance is `new`
419
+ // (Requirement 18.10). Sorted for determinism.
420
+ const unmatched = [...theirsByIdentity.entries()]
421
+ .filter(([identity]) => !matchedTheirsIdentities.has(identity))
422
+ .map(([, artifact]) => artifact)
423
+ .sort((a, b) => codePointCompare(a.name, b.name));
424
+
425
+ for (const theirsArtifact of unmatched) {
426
+ reconciled.push({
427
+ upstream,
428
+ artifactName: theirsArtifact.name,
429
+ result: newResult(theirsArtifact),
430
+ plan: undefined,
431
+ planDiagnostics: [],
432
+ });
433
+ }
434
+
435
+ return { reconciled, excludedArtifactNames };
436
+ }
437
+
438
+ // ═══════════════════════════════════════════════════════════════════════════════
439
+ // Multi-upstream orchestration + report assembly
440
+ // ═══════════════════════════════════════════════════════════════════════════════
441
+
442
+ /**
443
+ * Reconcile one or more upstreams and assemble the deterministic
444
+ * `ReconciliationReport`.
445
+ *
446
+ * The reconcile set is derived entirely from `ProvenanceRecord`s (Requirement
447
+ * 18.9–18.10): it can never silently miss a newly distilled artifact and it
448
+ * surfaces orphans and new upstream artifacts. Provenance-less artifacts are
449
+ * excluded (Requirement 18.17).
450
+ *
451
+ * @param upstreams One `ReconcileUpstreamOptions` per upstream to reconcile.
452
+ * @returns The report, per-artifact validated plans, and excluded artifact names.
453
+ */
454
+ export async function reconcileUpstreams(
455
+ upstreams: readonly ReconcileUpstreamOptions[],
456
+ ): Promise<ReconcileOrchestrationResult> {
457
+ const allReconciled: ReconciledArtifact[] = [];
458
+ const allExcluded: string[] = [];
459
+
460
+ // Iterate upstreams deterministically by identifier.
461
+ const sortedUpstreams = [...upstreams].sort((a, b) =>
462
+ codePointCompare(a.upstream, b.upstream),
463
+ );
464
+
465
+ for (const upstreamOptions of sortedUpstreams) {
466
+ const { reconciled, excludedArtifactNames } =
467
+ await reconcileUpstream(upstreamOptions);
468
+ allReconciled.push(...reconciled);
469
+ allExcluded.push(...excludedArtifactNames);
470
+ }
471
+
472
+ const entries: ReconciliationReportEntry[] = allReconciled.map((r) => ({
473
+ upstream: r.upstream,
474
+ artifactName: r.artifactName,
475
+ result: r.result,
476
+ }));
477
+
478
+ const sortedEntries = sortReportEntries(entries);
479
+
480
+ // Reorder the reconciled list to match the report ordering so callers that
481
+ // apply plans in report order see a consistent sequence.
482
+ const orderIndex = new Map<string, number>();
483
+ sortedEntries.forEach((entry, index) => {
484
+ orderIndex.set(`${entry.upstream}\u0000${entry.artifactName}`, index);
485
+ });
486
+ const orderedReconciled = [...allReconciled].sort((a, b) => {
487
+ const keyA = orderIndex.get(`${a.upstream}\u0000${a.artifactName}`) ?? 0;
488
+ const keyB = orderIndex.get(`${b.upstream}\u0000${b.artifactName}`) ?? 0;
489
+ return keyA - keyB;
490
+ });
491
+
492
+ const report: ReconciliationReport = {
493
+ machineSchemaVersion: "1.0",
494
+ entries: sortedEntries,
495
+ };
496
+
497
+ return {
498
+ report,
499
+ reconciled: orderedReconciled,
500
+ excludedArtifactNames: [...allExcluded].sort(codePointCompare),
501
+ };
502
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Reconciliation Report Renderer — human text and versioned JSON.
3
+ *
4
+ * Renders the deterministic `ReconciliationReport` produced by the reconcile
5
+ * orchestrator into two views of the same data (Requirement 18.15):
6
+ * - `renderReconciliationHuman` — terminal-friendly summary, grouped by outcome
7
+ * in the report's stable order, with per-artifact conflict field detail.
8
+ * - `renderReconciliationJson` — versioned, schema-valid, deterministic JSON
9
+ * with no ANSI escapes and stable key ordering, validated through
10
+ * `ReconciliationReportSchema`.
11
+ *
12
+ * Both renderers are pure functions of the report. They add no ordering of their
13
+ * own: the report entries arrive already ordered by outcome, upstream, then
14
+ * artifact name, so rendering preserves that order.
15
+ *
16
+ * Requirements: 18.15
17
+ */
18
+
19
+ import { stableJsonStringify } from "./rosetta/contracts";
20
+ import {
21
+ type ReconciliationOutcome,
22
+ type ReconciliationReport,
23
+ type ReconciliationReportEntry,
24
+ ReconciliationReportSchema,
25
+ } from "./schemas";
26
+
27
+ // ═══════════════════════════════════════════════════════════════════════════════
28
+ // Human renderer
29
+ // ═══════════════════════════════════════════════════════════════════════════════
30
+
31
+ /** Options for the human renderer. */
32
+ export interface ReconciliationRenderOptions {
33
+ /** Emit ANSI color codes (default: false — plain text, CI-safe). */
34
+ readonly color?: boolean;
35
+ }
36
+
37
+ const ANSI = {
38
+ reset: "\x1b[0m",
39
+ bold: "\x1b[1m",
40
+ dim: "\x1b[2m",
41
+ red: "\x1b[31m",
42
+ yellow: "\x1b[33m",
43
+ green: "\x1b[32m",
44
+ blue: "\x1b[34m",
45
+ cyan: "\x1b[36m",
46
+ gray: "\x1b[90m",
47
+ } as const;
48
+
49
+ /** A short, stable one-line label per outcome for the human summary. */
50
+ const OUTCOME_LABEL: Record<ReconciliationOutcome, string> = {
51
+ conflict: "conflict",
52
+ merged: "merged",
53
+ "fast-forward": "fast-forward",
54
+ new: "new",
55
+ orphaned: "orphaned",
56
+ clean: "clean",
57
+ };
58
+
59
+ /** Severity color for an outcome in the human render. */
60
+ const OUTCOME_COLOR: Record<ReconciliationOutcome, string> = {
61
+ conflict: ANSI.red,
62
+ merged: ANSI.cyan,
63
+ "fast-forward": ANSI.green,
64
+ new: ANSI.blue,
65
+ orphaned: ANSI.yellow,
66
+ clean: ANSI.gray,
67
+ };
68
+
69
+ function paint(text: string, code: string, enabled: boolean): string {
70
+ return enabled ? `${code}${text}${ANSI.reset}` : text;
71
+ }
72
+
73
+ /**
74
+ * Render a reconciliation report as human-readable text. Deterministic: the same
75
+ * report always renders identical text (given identical options).
76
+ */
77
+ export function renderReconciliationHuman(
78
+ report: ReconciliationReport,
79
+ options?: ReconciliationRenderOptions,
80
+ ): string {
81
+ const color = options?.color ?? false;
82
+ const lines: string[] = [];
83
+
84
+ lines.push(paint("Reconciliation Report", `${ANSI.bold}${ANSI.cyan}`, color));
85
+
86
+ const counts = countByOutcome(report.entries);
87
+ const summaryParts = (
88
+ Object.keys(OUTCOME_LABEL) as ReconciliationOutcome[]
89
+ ).map((outcome) => {
90
+ const label = OUTCOME_LABEL[outcome];
91
+ const count = counts[outcome] ?? 0;
92
+ return paint(`${label}: ${count}`, OUTCOME_COLOR[outcome], color);
93
+ });
94
+ lines.push(summaryParts.join(" "));
95
+
96
+ if (report.entries.length === 0) {
97
+ lines.push(
98
+ paint("No provenanced artifacts to reconcile.", ANSI.dim, color),
99
+ );
100
+ return `${lines.join("\n")}\n`;
101
+ }
102
+
103
+ lines.push("");
104
+
105
+ for (const entry of report.entries) {
106
+ lines.push(...renderEntry(entry, color));
107
+ }
108
+
109
+ return `${lines.join("\n")}\n`;
110
+ }
111
+
112
+ /** Render a single report entry to lines. */
113
+ function renderEntry(
114
+ entry: ReconciliationReportEntry,
115
+ color: boolean,
116
+ ): string[] {
117
+ const lines: string[] = [];
118
+ const { outcome, diagnostics } = entry.result;
119
+ const badge = paint(
120
+ `[${OUTCOME_LABEL[outcome]}]`,
121
+ OUTCOME_COLOR[outcome],
122
+ color,
123
+ );
124
+ const identity = paint(
125
+ `${entry.upstream}/${entry.artifactName}`,
126
+ ANSI.bold,
127
+ color,
128
+ );
129
+ lines.push(`${badge} ${identity}`);
130
+
131
+ // For conflict outcomes, list the flagged fields so a maintainer can confine
132
+ // resolution to them (Requirement 18.18 surfaced in the human view).
133
+ const conflictFields = diagnostics
134
+ .filter((d) => d.outcome === "conflict")
135
+ .map((d) => d.canonical?.fieldPath ?? d.field);
136
+ if (conflictFields.length > 0) {
137
+ lines.push(
138
+ paint(
139
+ ` conflicting fields: ${conflictFields.join(", ")}`,
140
+ ANSI.dim,
141
+ color,
142
+ ),
143
+ );
144
+ }
145
+
146
+ return lines;
147
+ }
148
+
149
+ /** Count entries by outcome. */
150
+ function countByOutcome(
151
+ entries: readonly ReconciliationReportEntry[],
152
+ ): Partial<Record<ReconciliationOutcome, number>> {
153
+ const counts: Partial<Record<ReconciliationOutcome, number>> = {};
154
+ for (const entry of entries) {
155
+ const outcome = entry.result.outcome;
156
+ counts[outcome] = (counts[outcome] ?? 0) + 1;
157
+ }
158
+ return counts;
159
+ }
160
+
161
+ // ═══════════════════════════════════════════════════════════════════════════════
162
+ // JSON renderer
163
+ // ═══════════════════════════════════════════════════════════════════════════════
164
+
165
+ /**
166
+ * Render a reconciliation report as versioned, deterministic JSON. The report is
167
+ * validated through `ReconciliationReportSchema` first so the JSON view can only
168
+ * ever emit a schema-valid envelope. Output contains no ANSI escapes and uses
169
+ * stable, deep-sorted key ordering.
170
+ *
171
+ * @throws If the report does not validate against ReconciliationReportSchema.
172
+ */
173
+ export function renderReconciliationJson(report: ReconciliationReport): string {
174
+ const validated = ReconciliationReportSchema.parse(report);
175
+ return stableJsonStringify(validated);
176
+ }
@@ -0,0 +1,15 @@
1
+ import type { HarnessName, KnowledgeArtifact } from "./schemas";
2
+
3
+ /**
4
+ * Resolves the effective markdown body for an artifact + harness. Returns the
5
+ * harness-specific override (from a `body.<harness>.md` file) when present,
6
+ * otherwise the artifact's canonical body. Used by both the compile pipeline
7
+ * (build.ts) and the plugin-skills generator so per-harness bodies behave
8
+ * identically across cross-compilation targets.
9
+ */
10
+ export function resolveBody(
11
+ artifact: KnowledgeArtifact,
12
+ harness: HarnessName,
13
+ ): string {
14
+ return artifact.bodyOverrides[harness] ?? artifact.body;
15
+ }