@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,812 @@
1
+ /**
2
+ * Rosetta Stone — Pure Three-Way Reconciliation Core
3
+ *
4
+ * Implements curation-preserving reconciliation of a distilled artifact against
5
+ * a freshly translated upstream. Given three already-translated
6
+ * `KnowledgeArtifact` values — Base (the common ancestor), Ours (the current
7
+ * curated artifact) and Theirs (the new upstream) — plus a
8
+ * `FieldOwnershipPolicy`, it produces a merged `KnowledgeArtifact`, an ordered
9
+ * list of `ReconciliationDiagnostic`s, and a per-artifact `ReconciliationOutcome`.
10
+ *
11
+ * Field-class dispatch (per ADR-0049 / ADR-RS-007):
12
+ * - `curation-owned` → always keep Ours; never a conflict, never Curation_Loss.
13
+ * - `upstream-owned` → fast-forward to Theirs when Base == Ours; otherwise a
14
+ * field-addressed conflict that keeps Ours while every
15
+ * other non-conflicting field is still applied.
16
+ * - `merge-by-union` → deterministic union of Ours and Theirs additions minus
17
+ * members removed between Base and Theirs.
18
+ * - `machine-owned` → recomputed from the merged result (never merged here;
19
+ * this core preserves Ours and leaves digest/provenance
20
+ * recomputation to the orchestration layer, task 19.3).
21
+ *
22
+ * When Base is absent (cache miss) or the caller has determined provenance is
23
+ * unverified, reconciliation degrades to a reduced-confidence two-way merge of
24
+ * Ours vs Theirs, marking affected fields with a distinct diagnostic rather
25
+ * than silently overwriting (Requirements 18.11, 18.16).
26
+ *
27
+ * BOUNDARY (Pure_Translation_Boundary): This module is a pure function of its
28
+ * inputs. NO filesystem, subprocess, network, `process`, clock, random, or Git
29
+ * access. It does NOT compute `baseDigest` (sha256) or perform provenance
30
+ * self-verification — those are layered on by task 19.3. Whether Base is
31
+ * present/verified is an input signal the caller resolves.
32
+ *
33
+ * Requirements: 18.3, 18.4, 18.5, 18.6, 18.7, 18.11, 18.12, 18.13
34
+ */
35
+
36
+ import type {
37
+ FieldOwnershipClass,
38
+ FieldOwnershipPolicy,
39
+ KnowledgeArtifact,
40
+ ReconcilableField,
41
+ ReconciliationDiagnostic,
42
+ ReconciliationOutcome,
43
+ ReconciliationResult,
44
+ RosettaSeverity,
45
+ } from "../schemas";
46
+ import { DEFAULT_FIELD_OWNERSHIP_POLICY } from "../schemas";
47
+ import { codePointCompare } from "./contracts";
48
+
49
+ // ═══════════════════════════════════════════════════════════════════════════════
50
+ // Public request/options types
51
+ // ═══════════════════════════════════════════════════════════════════════════════
52
+
53
+ /**
54
+ * The confidence level of a single reconciliation run. `full` when a verified
55
+ * Base is available for a true three-way merge; `reduced` for the two-way path.
56
+ */
57
+ export type ReconciliationConfidence = "full" | "reduced";
58
+
59
+ /**
60
+ * Inputs to a single-artifact reconciliation.
61
+ *
62
+ * `base` is optional: when absent, the merge uses the reduced-confidence
63
+ * two-way path (Requirement 18.11). `baseUnverified` lets the caller force the
64
+ * reduced-confidence path even when a `base` value is supplied — for example
65
+ * when provenance self-verification (task 19.3) has failed and the supplied
66
+ * base cannot be trusted as the true common ancestor (Requirement 18.16).
67
+ */
68
+ export interface ReconcileInput {
69
+ readonly base?: KnowledgeArtifact;
70
+ readonly ours: KnowledgeArtifact;
71
+ readonly theirs: KnowledgeArtifact;
72
+ readonly policy: FieldOwnershipPolicy;
73
+ /**
74
+ * When true, treat any supplied `base` as untrustworthy and run the
75
+ * reduced-confidence two-way path with a warning diagnostic. Defaults to
76
+ * false. This is the signal task 19.3 raises after a failed provenance
77
+ * self-verification, so this core never computes digests itself.
78
+ */
79
+ readonly baseUnverified?: boolean;
80
+ /**
81
+ * Explicit caller override permitting Curation_Loss on curation-owned
82
+ * fields. Defaults to false. Without it, curation-owned fields always keep
83
+ * Ours (Requirement 18.6).
84
+ */
85
+ readonly allowCurationOverride?: boolean;
86
+ }
87
+
88
+ // ═══════════════════════════════════════════════════════════════════════════════
89
+ // Field accessors — read/write a reconcilable field on a KnowledgeArtifact
90
+ // ═══════════════════════════════════════════════════════════════════════════════
91
+
92
+ /**
93
+ * A reconcilable field's location on a KnowledgeArtifact and its canonical
94
+ * field path (used in diagnostics). Frontmatter fields live under
95
+ * `frontmatter`; the capability fields (`body`, `workflows`, `mcpServers`,
96
+ * `hooks`) live at the artifact top level.
97
+ */
98
+ interface FieldAccessor {
99
+ /** Dotted canonical path for diagnostics, e.g. `frontmatter.categories`. */
100
+ readonly fieldPath: string;
101
+ /** Read the field's value from an artifact (undefined when absent). */
102
+ readonly read: (artifact: KnowledgeArtifact) => unknown;
103
+ /** Whether this field's value is an array (drives union merge semantics). */
104
+ readonly isArray: boolean;
105
+ }
106
+
107
+ /**
108
+ * Canonical accessor table for every ReconcilableField. Frontmatter is accessed
109
+ * with an index signature because several reconcilable keys (`trust`,
110
+ * `audience`, `priority`, `visibility`) are optional on FrontmatterSchema.
111
+ */
112
+ const FIELD_ACCESSORS: Readonly<Record<ReconcilableField, FieldAccessor>> = {
113
+ // Curation-owned frontmatter fields
114
+ categories: {
115
+ fieldPath: "frontmatter.categories",
116
+ read: (a) => a.frontmatter.categories,
117
+ isArray: true,
118
+ },
119
+ trust: {
120
+ fieldPath: "frontmatter.trust",
121
+ read: (a) => a.frontmatter.trust,
122
+ isArray: false,
123
+ },
124
+ collections: {
125
+ fieldPath: "frontmatter.collections",
126
+ read: (a) => a.frontmatter.collections,
127
+ isArray: true,
128
+ },
129
+ audience: {
130
+ fieldPath: "frontmatter.audience",
131
+ read: (a) => a.frontmatter.audience,
132
+ isArray: false,
133
+ },
134
+ priority: {
135
+ fieldPath: "frontmatter.priority",
136
+ read: (a) => a.frontmatter.priority,
137
+ isArray: false,
138
+ },
139
+ visibility: {
140
+ fieldPath: "frontmatter.visibility",
141
+ read: (a) => a.frontmatter.visibility,
142
+ isArray: false,
143
+ },
144
+ hooks: {
145
+ fieldPath: "hooks",
146
+ read: (a) => a.hooks,
147
+ isArray: true,
148
+ },
149
+ attribution: {
150
+ fieldPath: "frontmatter.attribution",
151
+ read: (a) => a.frontmatter.attribution,
152
+ isArray: false,
153
+ },
154
+ // Upstream-owned capabilities
155
+ body: {
156
+ fieldPath: "body",
157
+ read: (a) => a.body,
158
+ isArray: false,
159
+ },
160
+ workflows: {
161
+ fieldPath: "workflows",
162
+ read: (a) => a.workflows,
163
+ isArray: true,
164
+ },
165
+ mcpServers: {
166
+ fieldPath: "mcpServers",
167
+ read: (a) => a.mcpServers,
168
+ isArray: true,
169
+ },
170
+ // Merge-by-union frontmatter fields
171
+ keywords: {
172
+ fieldPath: "frontmatter.keywords",
173
+ read: (a) => a.frontmatter.keywords,
174
+ isArray: true,
175
+ },
176
+ enhances: {
177
+ fieldPath: "frontmatter.enhances",
178
+ read: (a) => a.frontmatter.enhances,
179
+ isArray: true,
180
+ },
181
+ depends: {
182
+ fieldPath: "frontmatter.depends",
183
+ read: (a) => a.frontmatter.depends,
184
+ isArray: true,
185
+ },
186
+ // Machine-owned fields
187
+ provenance: {
188
+ fieldPath: "frontmatter.provenance",
189
+ read: (a) => a.frontmatter.provenance,
190
+ isArray: false,
191
+ },
192
+ version: {
193
+ fieldPath: "frontmatter.version",
194
+ read: (a) => a.frontmatter.version,
195
+ isArray: false,
196
+ },
197
+ };
198
+
199
+ /**
200
+ * The complete, code-point-sorted list of reconcilable field keys. Iterating in
201
+ * this order makes diagnostics deterministic regardless of policy key order
202
+ * (Requirement 18.13).
203
+ */
204
+ const RECONCILABLE_FIELDS: readonly ReconcilableField[] = (
205
+ Object.keys(FIELD_ACCESSORS) as ReconcilableField[]
206
+ )
207
+ .slice()
208
+ .sort(codePointCompare);
209
+
210
+ // Frontmatter keys that live under `artifact.frontmatter`.
211
+ const FRONTMATTER_FIELDS: ReadonlySet<ReconcilableField> =
212
+ new Set<ReconcilableField>([
213
+ "categories",
214
+ "trust",
215
+ "collections",
216
+ "audience",
217
+ "priority",
218
+ "visibility",
219
+ "attribution",
220
+ "keywords",
221
+ "enhances",
222
+ "depends",
223
+ "provenance",
224
+ "version",
225
+ ]);
226
+
227
+ // ═══════════════════════════════════════════════════════════════════════════════
228
+ // Deterministic value equality and cloning
229
+ // ═══════════════════════════════════════════════════════════════════════════════
230
+
231
+ /**
232
+ * Recursively sort object keys by code-point order so semantically equivalent
233
+ * values with different key insertion orders compare equal. Arrays preserve
234
+ * their order (order is semantic for canonical fields).
235
+ */
236
+ function canonicalize(value: unknown): unknown {
237
+ if (value === null || value === undefined) {
238
+ return value;
239
+ }
240
+ if (Array.isArray(value)) {
241
+ return value.map(canonicalize);
242
+ }
243
+ if (typeof value === "object") {
244
+ const source = value as Record<string, unknown>;
245
+ const sorted: Record<string, unknown> = {};
246
+ for (const key of Object.keys(source).sort(codePointCompare)) {
247
+ sorted[key] = canonicalize(source[key]);
248
+ }
249
+ return sorted;
250
+ }
251
+ return value;
252
+ }
253
+
254
+ /**
255
+ * Deterministic structural equality: two values are equal when their
256
+ * canonicalized JSON serializations match. `undefined` values are normalized so
257
+ * an absent field and an explicitly-undefined field compare equal.
258
+ */
259
+ function valuesEqual(a: unknown, b: unknown): boolean {
260
+ if (a === undefined && b === undefined) return true;
261
+ if (a === undefined || b === undefined) return false;
262
+ return JSON.stringify(canonicalize(a)) === JSON.stringify(canonicalize(b));
263
+ }
264
+
265
+ /** Deep clone via structuredClone; returns undefined unchanged. */
266
+ function cloneValue<T>(value: T): T {
267
+ if (value === undefined) return value;
268
+ return structuredClone(value);
269
+ }
270
+
271
+ // ═══════════════════════════════════════════════════════════════════════════════
272
+ // Merge-by-union
273
+ // ═══════════════════════════════════════════════════════════════════════════════
274
+
275
+ /** Stable identity key for a union member, by code-point-sorted JSON. */
276
+ function memberKey(member: unknown): string {
277
+ return JSON.stringify(canonicalize(member));
278
+ }
279
+
280
+ /** Coerce an unknown field value to an array (missing → empty). */
281
+ function asArray(value: unknown): unknown[] {
282
+ return Array.isArray(value) ? value : [];
283
+ }
284
+
285
+ /**
286
+ * Deterministic merge-by-union: union of Ours and Theirs members, minus members
287
+ * removed between Base and Theirs (present in Base, absent in Theirs). The
288
+ * result preserves Ours order first, then appends new Theirs members in Theirs
289
+ * order — a stable, insertion-order-derived total order (Requirement 18.7).
290
+ *
291
+ * When Base is absent (two-way path), no removal set is computed: the union is
292
+ * simply Ours ∪ Theirs, so nothing curated is dropped.
293
+ */
294
+ function mergeByUnion(
295
+ base: unknown,
296
+ ours: unknown,
297
+ theirs: unknown,
298
+ hasBase: boolean,
299
+ ): { value: unknown[]; changed: boolean } {
300
+ const oursArr = asArray(ours);
301
+ const theirsArr = asArray(theirs);
302
+
303
+ // Members removed upstream: present in Base, absent in Theirs.
304
+ const removed = new Set<string>();
305
+ if (hasBase) {
306
+ const theirsKeys = new Set(theirsArr.map(memberKey));
307
+ for (const member of asArray(base)) {
308
+ const key = memberKey(member);
309
+ if (!theirsKeys.has(key)) {
310
+ removed.add(key);
311
+ }
312
+ }
313
+ }
314
+
315
+ const result: unknown[] = [];
316
+ const seen = new Set<string>();
317
+
318
+ // Ours first (preserve curation order), skipping upstream removals.
319
+ for (const member of oursArr) {
320
+ const key = memberKey(member);
321
+ if (removed.has(key)) continue;
322
+ if (seen.has(key)) continue;
323
+ seen.add(key);
324
+ result.push(cloneValue(member));
325
+ }
326
+ // Then new Theirs members in Theirs order.
327
+ for (const member of theirsArr) {
328
+ const key = memberKey(member);
329
+ if (removed.has(key)) continue;
330
+ if (seen.has(key)) continue;
331
+ seen.add(key);
332
+ result.push(cloneValue(member));
333
+ }
334
+
335
+ const changed = !valuesEqual(result, oursArr);
336
+ return { value: result, changed };
337
+ }
338
+
339
+ // ═══════════════════════════════════════════════════════════════════════════════
340
+ // Diagnostic construction
341
+ // ═══════════════════════════════════════════════════════════════════════════════
342
+
343
+ const RECONCILE_MESSAGES: Readonly<
344
+ Record<
345
+ ReconciliationOutcome,
346
+ { code: string; severity: RosettaSeverity; blocking: boolean }
347
+ >
348
+ > = {
349
+ clean: {
350
+ code: "RS_RECONCILE_CLEAN",
351
+ severity: "info",
352
+ blocking: false,
353
+ },
354
+ "fast-forward": {
355
+ code: "RS_RECONCILE_FAST_FORWARD",
356
+ severity: "info",
357
+ blocking: false,
358
+ },
359
+ merged: {
360
+ code: "RS_RECONCILE_MERGED",
361
+ severity: "info",
362
+ blocking: false,
363
+ },
364
+ conflict: {
365
+ code: "RS_RECONCILE_CONFLICT",
366
+ severity: "warning",
367
+ blocking: false,
368
+ },
369
+ orphaned: {
370
+ code: "RS_RECONCILE_ORPHANED",
371
+ severity: "warning",
372
+ blocking: false,
373
+ },
374
+ new: {
375
+ code: "RS_RECONCILE_NEW",
376
+ severity: "info",
377
+ blocking: false,
378
+ },
379
+ };
380
+
381
+ const REDUCED_CONFIDENCE_CODE = "RS_RECONCILE_REDUCED_CONFIDENCE";
382
+ const REDUCED_CONFIDENCE_REMEDIATION =
383
+ "The common-ancestor Base_Artifact was unavailable or unverified; this field was merged two-way (Ours vs Theirs). Review the merged value.";
384
+
385
+ interface DiagnosticParams {
386
+ readonly field: ReconcilableField;
387
+ readonly fieldClass: FieldOwnershipClass;
388
+ readonly outcome: ReconciliationOutcome;
389
+ readonly artifactName: string;
390
+ readonly fieldPath: string;
391
+ readonly message: string;
392
+ readonly remediation: string;
393
+ readonly baseValuePresent: boolean;
394
+ readonly confidence: ReconciliationConfidence;
395
+ readonly severity: RosettaSeverity;
396
+ readonly blocking: boolean;
397
+ readonly code: string;
398
+ }
399
+
400
+ function buildDiagnostic(params: DiagnosticParams): ReconciliationDiagnostic {
401
+ return {
402
+ code: params.code,
403
+ severity: params.severity,
404
+ phase: "source-translation",
405
+ message: params.message,
406
+ remediation: params.remediation,
407
+ canonical: {
408
+ artifactName: params.artifactName,
409
+ fieldPath: params.fieldPath,
410
+ },
411
+ unavailableDetails: [],
412
+ blocking: params.blocking,
413
+ field: params.field,
414
+ fieldClass: params.fieldClass,
415
+ outcome: params.outcome,
416
+ baseValuePresent: params.baseValuePresent,
417
+ confidence: params.confidence,
418
+ };
419
+ }
420
+
421
+ // ═══════════════════════════════════════════════════════════════════════════════
422
+ // Per-field reconciliation
423
+ // ═══════════════════════════════════════════════════════════════════════════════
424
+
425
+ interface FieldResolution {
426
+ /** The resolved value for the field (already cloned; undefined = leave/omit). */
427
+ readonly value: unknown;
428
+ /** Whether to write the resolved value (false = keep Ours untouched). */
429
+ readonly write: boolean;
430
+ /** Per-field outcome, used to derive the artifact outcome. */
431
+ readonly outcome: ReconciliationOutcome;
432
+ /** Diagnostics emitted for this field (may be empty). */
433
+ readonly diagnostics: readonly ReconciliationDiagnostic[];
434
+ }
435
+
436
+ /**
437
+ * Resolve one field under its ownership class. `hasBase` distinguishes the
438
+ * full three-way path from the reduced-confidence two-way path.
439
+ */
440
+ function reconcileField(
441
+ field: ReconcilableField,
442
+ fieldClass: FieldOwnershipClass,
443
+ base: KnowledgeArtifact | undefined,
444
+ ours: KnowledgeArtifact,
445
+ theirs: KnowledgeArtifact,
446
+ hasBase: boolean,
447
+ confidence: ReconciliationConfidence,
448
+ allowCurationOverride: boolean,
449
+ ): FieldResolution {
450
+ const accessor = FIELD_ACCESSORS[field];
451
+ const oursValue = accessor.read(ours);
452
+ const theirsValue = accessor.read(theirs);
453
+ const baseValue = hasBase && base ? accessor.read(base) : undefined;
454
+ const artifactName = ours.name;
455
+ const fieldPath = accessor.fieldPath;
456
+
457
+ switch (fieldClass) {
458
+ case "machine-owned": {
459
+ // Never merged here. Preserve Ours; the orchestration layer recomputes
460
+ // provenance/version/baseDigest from the merged result (task 19.3).
461
+ return {
462
+ value: cloneValue(oursValue),
463
+ write: false,
464
+ outcome: "clean",
465
+ diagnostics: [],
466
+ };
467
+ }
468
+
469
+ case "curation-owned": {
470
+ // Always keep Ours (Requirement 18.6). Only an explicit caller override
471
+ // permits taking Theirs — otherwise never a conflict, never Curation_Loss.
472
+ if (allowCurationOverride && !valuesEqual(oursValue, theirsValue)) {
473
+ return {
474
+ value: cloneValue(theirsValue),
475
+ write: true,
476
+ outcome: "fast-forward",
477
+ diagnostics: [],
478
+ };
479
+ }
480
+ return {
481
+ value: cloneValue(oursValue),
482
+ write: false,
483
+ outcome: "clean",
484
+ diagnostics: [],
485
+ };
486
+ }
487
+
488
+ case "merge-by-union": {
489
+ const { value, changed } = mergeByUnion(
490
+ baseValue,
491
+ oursValue,
492
+ theirsValue,
493
+ hasBase,
494
+ );
495
+ const diagnostics: ReconciliationDiagnostic[] = [];
496
+ const outcome: ReconciliationOutcome = changed ? "merged" : "clean";
497
+ if (changed && !hasBase) {
498
+ diagnostics.push(
499
+ buildDiagnostic({
500
+ field,
501
+ fieldClass,
502
+ outcome,
503
+ artifactName,
504
+ fieldPath,
505
+ code: REDUCED_CONFIDENCE_CODE,
506
+ severity: "warning",
507
+ blocking: false,
508
+ message: `Field "${fieldPath}" was merged by union without a Base_Artifact; upstream member removals could not be detected.`,
509
+ remediation: REDUCED_CONFIDENCE_REMEDIATION,
510
+ baseValuePresent: false,
511
+ confidence: "reduced",
512
+ }),
513
+ );
514
+ }
515
+ return { value, write: changed, outcome, diagnostics };
516
+ }
517
+
518
+ case "upstream-owned": {
519
+ const oursChanged = hasBase
520
+ ? !valuesEqual(baseValue, oursValue)
521
+ : !valuesEqual(oursValue, theirsValue);
522
+ const theirsChanged = hasBase
523
+ ? !valuesEqual(baseValue, theirsValue)
524
+ : !valuesEqual(oursValue, theirsValue);
525
+
526
+ if (!hasBase) {
527
+ // Reduced-confidence two-way path: without a common ancestor we cannot
528
+ // tell who changed. If the values differ, keep Ours and flag it rather
529
+ // than silently overwriting (Requirements 18.11, 18.16).
530
+ if (!valuesEqual(oursValue, theirsValue)) {
531
+ return {
532
+ value: cloneValue(oursValue),
533
+ write: false,
534
+ outcome: "conflict",
535
+ diagnostics: [
536
+ buildDiagnostic({
537
+ field,
538
+ fieldClass,
539
+ outcome: "conflict",
540
+ artifactName,
541
+ fieldPath,
542
+ code: REDUCED_CONFIDENCE_CODE,
543
+ severity: "warning",
544
+ blocking: false,
545
+ message: `Field "${fieldPath}" differs between Ours and Theirs but no Base_Artifact was available to determine which side changed; Ours was preserved.`,
546
+ remediation: REDUCED_CONFIDENCE_REMEDIATION,
547
+ baseValuePresent: false,
548
+ confidence: "reduced",
549
+ }),
550
+ ],
551
+ };
552
+ }
553
+ return {
554
+ value: cloneValue(oursValue),
555
+ write: false,
556
+ outcome: "clean",
557
+ diagnostics: [],
558
+ };
559
+ }
560
+
561
+ // Full three-way path.
562
+ if (!theirsChanged) {
563
+ // Upstream unchanged: nothing to apply.
564
+ return {
565
+ value: cloneValue(oursValue),
566
+ write: false,
567
+ outcome: "clean",
568
+ diagnostics: [],
569
+ };
570
+ }
571
+ if (!oursChanged) {
572
+ // Maintainer never touched it — fast-forward to Theirs (Req 18.4).
573
+ const message = RECONCILE_MESSAGES["fast-forward"];
574
+ return {
575
+ value: cloneValue(theirsValue),
576
+ write: true,
577
+ outcome: "fast-forward",
578
+ diagnostics: [
579
+ buildDiagnostic({
580
+ field,
581
+ fieldClass,
582
+ outcome: "fast-forward",
583
+ artifactName,
584
+ fieldPath,
585
+ code: message.code,
586
+ severity: message.severity,
587
+ blocking: message.blocking,
588
+ message: `Field "${fieldPath}" was unchanged by curation and fast-forwarded to the upstream value.`,
589
+ remediation:
590
+ "No action required; the upstream change was applied cleanly.",
591
+ baseValuePresent: true,
592
+ confidence,
593
+ }),
594
+ ],
595
+ };
596
+ }
597
+ // Both sides changed the same upstream-owned field — conflict. Keep Ours,
598
+ // emit a field-addressed diagnostic (Requirements 18.5, 18.18).
599
+ const message = RECONCILE_MESSAGES.conflict;
600
+ return {
601
+ value: cloneValue(oursValue),
602
+ write: false,
603
+ outcome: "conflict",
604
+ diagnostics: [
605
+ buildDiagnostic({
606
+ field,
607
+ fieldClass,
608
+ outcome: "conflict",
609
+ artifactName,
610
+ fieldPath,
611
+ code: message.code,
612
+ severity: message.severity,
613
+ blocking: message.blocking,
614
+ message: `Field "${fieldPath}" diverged from the Base_Artifact in both the curated and upstream copies. Ours was preserved; resolve this field manually.`,
615
+ remediation:
616
+ "Compare the Base, Ours, and Theirs values for this field and edit knowledge.md to the intended result.",
617
+ baseValuePresent: true,
618
+ confidence,
619
+ }),
620
+ ],
621
+ };
622
+ }
623
+
624
+ default: {
625
+ // Exhaustiveness guard.
626
+ const _exhaustive: never = fieldClass;
627
+ return {
628
+ value: cloneValue(oursValue),
629
+ write: false,
630
+ outcome: "clean",
631
+ diagnostics: [],
632
+ };
633
+ }
634
+ }
635
+ }
636
+
637
+ // ═══════════════════════════════════════════════════════════════════════════════
638
+ // Artifact-level outcome classification
639
+ // ═══════════════════════════════════════════════════════════════════════════════
640
+
641
+ /**
642
+ * Derive the single per-artifact outcome from per-field outcomes. Precedence
643
+ * (most severe first): conflict > merged > fast-forward > clean. `orphaned` and
644
+ * `new` are set by the orchestration layer from provenance identity, not here.
645
+ */
646
+ function classifyArtifactOutcome(
647
+ fieldOutcomes: readonly ReconciliationOutcome[],
648
+ ): ReconciliationOutcome {
649
+ if (fieldOutcomes.includes("conflict")) return "conflict";
650
+ if (fieldOutcomes.includes("merged")) return "merged";
651
+ if (fieldOutcomes.includes("fast-forward")) return "fast-forward";
652
+ return "clean";
653
+ }
654
+
655
+ // ═══════════════════════════════════════════════════════════════════════════════
656
+ // Merged-artifact assembly
657
+ // ═══════════════════════════════════════════════════════════════════════════════
658
+
659
+ /**
660
+ * Build the merged artifact by cloning Ours and overwriting only the fields
661
+ * whose resolution requested a write. Machine-owned and untouched fields keep
662
+ * their Ours value.
663
+ */
664
+ function assembleMergedArtifact(
665
+ ours: KnowledgeArtifact,
666
+ resolutions: ReadonlyMap<ReconcilableField, FieldResolution>,
667
+ ): KnowledgeArtifact {
668
+ const merged = structuredClone(ours);
669
+ for (const field of RECONCILABLE_FIELDS) {
670
+ const resolution = resolutions.get(field);
671
+ if (!resolution?.write) continue;
672
+ if (FRONTMATTER_FIELDS.has(field)) {
673
+ (merged.frontmatter as Record<string, unknown>)[field] = resolution.value;
674
+ } else {
675
+ (merged as unknown as Record<string, unknown>)[field] = resolution.value;
676
+ }
677
+ }
678
+ return merged;
679
+ }
680
+
681
+ // ═══════════════════════════════════════════════════════════════════════════════
682
+ // Diagnostic ordering
683
+ // ═══════════════════════════════════════════════════════════════════════════════
684
+
685
+ /**
686
+ * Deterministic ordering for reconciliation diagnostics: by severity
687
+ * (error < warning < info), then canonical field path (code-point), then code.
688
+ * This is stable across repeated runs and independent of policy key order
689
+ * (Requirement 18.13).
690
+ */
691
+ const RECONCILE_SEVERITY_ORDER: Record<RosettaSeverity, number> = {
692
+ error: 0,
693
+ warning: 1,
694
+ info: 2,
695
+ };
696
+
697
+ function sortReconciliationDiagnostics(
698
+ diagnostics: readonly ReconciliationDiagnostic[],
699
+ ): ReconciliationDiagnostic[] {
700
+ return [...diagnostics].sort((a, b) => {
701
+ const sev =
702
+ RECONCILE_SEVERITY_ORDER[a.severity] -
703
+ RECONCILE_SEVERITY_ORDER[b.severity];
704
+ if (sev !== 0) return sev;
705
+ const pathA = a.canonical?.fieldPath ?? "";
706
+ const pathB = b.canonical?.fieldPath ?? "";
707
+ const pathCmp = codePointCompare(pathA, pathB);
708
+ if (pathCmp !== 0) return pathCmp;
709
+ return codePointCompare(a.code, b.code);
710
+ });
711
+ }
712
+
713
+ // ═══════════════════════════════════════════════════════════════════════════════
714
+ // Public entry point
715
+ // ═══════════════════════════════════════════════════════════════════════════════
716
+
717
+ /**
718
+ * Reconcile a single artifact.
719
+ *
720
+ * Pure: depends only on its arguments and returns a merged `KnowledgeArtifact`,
721
+ * an ordered set of diagnostics, and the per-artifact outcome. Deterministic:
722
+ * identical inputs yield a canonically-equivalent artifact and identical ordered
723
+ * diagnostics (Requirement 18.13).
724
+ *
725
+ * @param input Base (optional), Ours, Theirs, and a FieldOwnershipPolicy.
726
+ * @returns A ReconciliationResult for the single artifact.
727
+ */
728
+ export function reconcileArtifact(input: ReconcileInput): ReconciliationResult {
729
+ const {
730
+ base,
731
+ ours,
732
+ theirs,
733
+ policy,
734
+ baseUnverified = false,
735
+ allowCurationOverride = false,
736
+ } = input;
737
+
738
+ // Base is usable for a true three-way merge only when it is present AND the
739
+ // caller has not flagged it as unverified (task 19.3's self-verification).
740
+ const hasBase = base !== undefined && !baseUnverified;
741
+ const confidence: ReconciliationConfidence = hasBase ? "full" : "reduced";
742
+
743
+ const resolutions = new Map<ReconcilableField, FieldResolution>();
744
+ const diagnostics: ReconciliationDiagnostic[] = [];
745
+ const fieldOutcomes: ReconciliationOutcome[] = [];
746
+
747
+ for (const field of RECONCILABLE_FIELDS) {
748
+ const fieldClass = resolveFieldClass(field, policy);
749
+ const resolution = reconcileField(
750
+ field,
751
+ fieldClass,
752
+ base,
753
+ ours,
754
+ theirs,
755
+ hasBase,
756
+ confidence,
757
+ allowCurationOverride,
758
+ );
759
+ resolutions.set(field, resolution);
760
+ diagnostics.push(...resolution.diagnostics);
761
+ fieldOutcomes.push(resolution.outcome);
762
+ }
763
+
764
+ // When a base was supplied but flagged unverified, surface a single
765
+ // artifact-level warning so the reduced-confidence path is explicit
766
+ // (Requirement 18.16). The reduced two-way path with no base at all is
767
+ // already implied by per-field reduced-confidence diagnostics.
768
+ if (base !== undefined && baseUnverified) {
769
+ diagnostics.push({
770
+ code: REDUCED_CONFIDENCE_CODE,
771
+ severity: "warning",
772
+ phase: "source-translation",
773
+ message:
774
+ "The recorded provenance base failed self-verification; reconciliation used the reduced-confidence two-way path.",
775
+ remediation:
776
+ "Re-import the artifact to refresh its provenance base, or resolve fields manually.",
777
+ canonical: {
778
+ artifactName: ours.name,
779
+ fieldPath: "frontmatter.provenance",
780
+ },
781
+ unavailableDetails: [],
782
+ blocking: false,
783
+ field: "provenance",
784
+ fieldClass: "machine-owned",
785
+ outcome: "conflict",
786
+ baseValuePresent: false,
787
+ confidence: "reduced",
788
+ });
789
+ }
790
+
791
+ const mergedArtifact = assembleMergedArtifact(ours, resolutions);
792
+ const outcome = classifyArtifactOutcome(fieldOutcomes);
793
+
794
+ return {
795
+ artifact: mergedArtifact,
796
+ outcome,
797
+ diagnostics: sortReconciliationDiagnostics(diagnostics),
798
+ };
799
+ }
800
+
801
+ /**
802
+ * Resolve a field's ownership class from the policy, falling back to the
803
+ * documented default when a per-upstream override omits the field. The
804
+ * Configuration_Validator (task 19.9) guarantees a complete effective policy
805
+ * before this core is called, so the default fallback is a defensive belt.
806
+ */
807
+ function resolveFieldClass(
808
+ field: ReconcilableField,
809
+ policy: FieldOwnershipPolicy,
810
+ ): FieldOwnershipClass {
811
+ return policy[field] ?? DEFAULT_FIELD_OWNERSHIP_POLICY[field];
812
+ }