@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,369 @@
1
+ /**
2
+ * Rosetta Stone — Pure Base_Digest Computation and Provenance Self-Verification
3
+ *
4
+ * Layers digest computation and provenance self-verification ON TOP of the pure
5
+ * reconciliation core (`reconcile.ts`, task 19.2). The reconciliation core
6
+ * deliberately computes no digests; this module supplies the two machine-owned
7
+ * operations that recorded-base three-way reconciliation depends on (ADR-0049 /
8
+ * ADR-RS-007):
9
+ *
10
+ * 1. `computeBaseDigest` — the deterministic `sha256` fingerprint of the
11
+ * normalized Theirs_Artifact, reusing the Canonical_Serializer's ordering
12
+ * from `canonical.ts` so the digest is stable across machines
13
+ * (Requirement 18.2). It hashes the exact canonical byte plan the serializer
14
+ * produces; it does NOT invent a new serialization.
15
+ *
16
+ * 2. `verifyProvenanceBase` / `selfVerifyReconcileInput` — given an
17
+ * Ours_Artifact carrying a `ProvenanceRecord` (with its recorded
18
+ * `baseDigest`) and the reconstructed candidate Base_Artifact, recompute the
19
+ * candidate's digest and compare. A mismatch means the artifact was
20
+ * hand-edited such that its provenance no longer matches the recorded base,
21
+ * so reconciliation is routed to the reduced-confidence two-way path
22
+ * (`baseUnverified: true`) with a warning diagnostic (Requirement 18.16).
23
+ *
24
+ * BOUNDARY (Pure_Translation_Boundary): This module is a pure function of its
25
+ * inputs. It performs NO filesystem, subprocess, network, `process`, clock,
26
+ * random, or Git access. `sha256` is computed with Node's `node:crypto`
27
+ * `createHash`, which is a synchronous, pure computation over in-memory bytes —
28
+ * it is explicitly permitted by the architecture-boundary test's allowlist
29
+ * (`rosetta-architecture-boundary.test.ts`), which forbids fs/subprocess/
30
+ * network/process/prompt/FileSystemLoader access but not `node:crypto`. The
31
+ * base-artifact cache, its IO, and Git access remain in the orchestration shell
32
+ * (ADR-0049): this module only turns an in-memory artifact into a digest and
33
+ * compares digests.
34
+ *
35
+ * Requirements: 18.2, 18.16
36
+ */
37
+
38
+ import { createHash } from "node:crypto";
39
+ import type {
40
+ KnowledgeArtifact,
41
+ ProvenanceRecord,
42
+ ReconciliationDiagnostic,
43
+ TranslationDiagnostic,
44
+ } from "../schemas";
45
+ import {
46
+ type CanonicalSerializerOptions,
47
+ serializeCanonical,
48
+ } from "./canonical";
49
+ import type { ReconcileInput } from "./reconcile";
50
+
51
+ // ═══════════════════════════════════════════════════════════════════════════════
52
+ // Base_Digest computation
53
+ // ═══════════════════════════════════════════════════════════════════════════════
54
+
55
+ /**
56
+ * The digest algorithm prefix. Recorded `baseDigest` values are namespaced with
57
+ * the algorithm so a future migration to a different digest can be detected
58
+ * rather than silently compared against the wrong scheme (matches the
59
+ * `sha256:…` shape shown in ADR-0049).
60
+ */
61
+ const DIGEST_ALGORITHM = "sha256" as const;
62
+
63
+ /** NUL separator between the path and content of each serialized output file. */
64
+ const FIELD_SEPARATOR = "\u0000";
65
+ /** NUL separator between successive serialized output files. */
66
+ const RECORD_SEPARATOR = "\u0000\u0000";
67
+
68
+ /**
69
+ * The outcome of a Base_Digest computation. When serialization of the artifact
70
+ * fails (an invalid artifact), no digest can be produced and the serializer's
71
+ * diagnostics are surfaced so the caller can decide how to degrade.
72
+ */
73
+ export interface BaseDigestResult {
74
+ /** The `sha256:<hex>` digest, or undefined when serialization failed. */
75
+ readonly digest: string | undefined;
76
+ /** Serializer diagnostics (empty on success). */
77
+ readonly diagnostics: readonly TranslationDiagnostic[];
78
+ }
79
+
80
+ /**
81
+ * Fold the deterministic canonical byte plan of an artifact into a stable byte
82
+ * sequence for hashing. The Canonical_Serializer already sorts its output files
83
+ * deterministically (by normalized path, code-point order) and renders each
84
+ * file's bytes deterministically, so hashing `path \0 content \0\0` per file in
85
+ * plan order is itself deterministic and reuses the serializer's ordering
86
+ * rather than introducing a new one.
87
+ */
88
+ function foldPlanToBytes(
89
+ outputFiles: readonly {
90
+ readonly relativePath: string;
91
+ readonly content: string | Uint8Array;
92
+ }[],
93
+ ): Uint8Array {
94
+ const encoder = new TextEncoder();
95
+ const chunks: Uint8Array[] = [];
96
+ for (const file of outputFiles) {
97
+ chunks.push(encoder.encode(file.relativePath));
98
+ chunks.push(encoder.encode(FIELD_SEPARATOR));
99
+ chunks.push(
100
+ typeof file.content === "string"
101
+ ? encoder.encode(file.content)
102
+ : file.content,
103
+ );
104
+ chunks.push(encoder.encode(RECORD_SEPARATOR));
105
+ }
106
+
107
+ let total = 0;
108
+ for (const chunk of chunks) {
109
+ total += chunk.length;
110
+ }
111
+ const combined = new Uint8Array(total);
112
+ let offset = 0;
113
+ for (const chunk of chunks) {
114
+ combined.set(chunk, offset);
115
+ offset += chunk.length;
116
+ }
117
+ return combined;
118
+ }
119
+
120
+ /**
121
+ * Compute the Base_Digest of an already-translated Theirs_Artifact.
122
+ *
123
+ * The digest is `sha256` over the deterministically serialized artifact,
124
+ * reusing the Canonical_Serializer ordering (Requirement 18.2). Identical
125
+ * artifacts (Canonically_Equivalent) yield an identical digest regardless of
126
+ * frontmatter key insertion order, because the serializer canonicalizes key
127
+ * order before rendering.
128
+ *
129
+ * Pure: depends only on `artifact` (and optional serializer options) and
130
+ * performs no IO. Returns `undefined` with diagnostics when the artifact cannot
131
+ * be serialized.
132
+ *
133
+ * @param artifact The normalized Theirs_Artifact to fingerprint.
134
+ * @param options Optional serializer options; defaults match the serializer.
135
+ * @returns The `sha256:<hex>` digest plus any serializer diagnostics.
136
+ */
137
+ export function computeBaseDigest(
138
+ artifact: KnowledgeArtifact,
139
+ options?: CanonicalSerializerOptions,
140
+ ): BaseDigestResult {
141
+ const { plan, diagnostics } = serializeCanonical(artifact, options);
142
+ if (!plan) {
143
+ return {
144
+ digest: undefined,
145
+ diagnostics,
146
+ };
147
+ }
148
+
149
+ const bytes = foldPlanToBytes(plan.outputFiles);
150
+ const hex = createHash(DIGEST_ALGORITHM).update(bytes).digest("hex");
151
+ return { digest: `${DIGEST_ALGORITHM}:${hex}`, diagnostics: [] };
152
+ }
153
+
154
+ // ═══════════════════════════════════════════════════════════════════════════════
155
+ // Provenance self-verification
156
+ // ═══════════════════════════════════════════════════════════════════════════════
157
+
158
+ const SELF_VERIFY_MISMATCH_CODE = "RS_RECONCILE_PROVENANCE_UNVERIFIED";
159
+ const SELF_VERIFY_MISMATCH_REMEDIATION =
160
+ "The recorded provenance base digest does not match the reconstructed base. " +
161
+ "Re-import the artifact to refresh its provenance, or resolve fields manually; " +
162
+ "reconciliation used the reduced-confidence two-way path.";
163
+
164
+ /**
165
+ * The outcome of a provenance self-verification check.
166
+ */
167
+ export interface ProvenanceVerification {
168
+ /**
169
+ * True when the recorded provenance base could be verified against the
170
+ * candidate Base_Artifact (the recorded digest equals the recomputed digest).
171
+ * When false, callers MUST treat any candidate base as untrustworthy and run
172
+ * the reduced-confidence two-way path (Requirement 18.16).
173
+ */
174
+ readonly verified: boolean;
175
+ /** The recorded digest read from the ProvenanceRecord, if present. */
176
+ readonly recordedDigest: string | undefined;
177
+ /** The digest recomputed from the candidate Base_Artifact, if computable. */
178
+ readonly recomputedDigest: string | undefined;
179
+ /** A warning diagnostic when verification failed; undefined when verified. */
180
+ readonly diagnostic: ReconciliationDiagnostic | undefined;
181
+ }
182
+
183
+ /**
184
+ * Build the warning diagnostic emitted when self-verification fails
185
+ * (Requirement 18.16).
186
+ */
187
+ function buildUnverifiedDiagnostic(
188
+ artifactName: string,
189
+ ): ReconciliationDiagnostic {
190
+ return {
191
+ code: SELF_VERIFY_MISMATCH_CODE,
192
+ severity: "warning",
193
+ phase: "source-translation",
194
+ message:
195
+ "The recorded provenance base failed self-verification (its Base_Digest " +
196
+ "does not match the reconstructed base); the artifact appears to have been " +
197
+ "hand-edited. Reconciliation used the reduced-confidence two-way path.",
198
+ remediation: SELF_VERIFY_MISMATCH_REMEDIATION,
199
+ canonical: {
200
+ artifactName,
201
+ fieldPath: "frontmatter.provenance",
202
+ },
203
+ unavailableDetails: [],
204
+ blocking: false,
205
+ field: "provenance",
206
+ fieldClass: "machine-owned",
207
+ outcome: "conflict",
208
+ baseValuePresent: false,
209
+ confidence: "reduced",
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Read the ProvenanceRecord from an artifact's canonical frontmatter, if any.
215
+ * Artifacts authored from scratch carry no provenance (Requirement 18.17) and
216
+ * yield `undefined`.
217
+ */
218
+ export function readProvenance(
219
+ artifact: KnowledgeArtifact,
220
+ ): ProvenanceRecord | undefined {
221
+ return artifact.frontmatter.provenance;
222
+ }
223
+
224
+ /**
225
+ * Self-verify an Ours_Artifact's recorded provenance base against a candidate
226
+ * Base_Artifact.
227
+ *
228
+ * Recomputes the Base_Digest of `candidateBase` (Requirement 18.2) and compares
229
+ * it to the digest recorded in `ours.frontmatter.provenance.baseDigest`. The
230
+ * check is:
231
+ *
232
+ * - `verified: true` only when a ProvenanceRecord is present, a candidate base
233
+ * is supplied, the candidate serializes cleanly, and the recomputed digest
234
+ * equals the recorded digest.
235
+ * - `verified: false` (with a warning diagnostic) when the digests differ — the
236
+ * hand-edited-provenance case (Requirement 18.16).
237
+ * - `verified: false` (no diagnostic) when there is nothing to verify against
238
+ * (no provenance record, or no candidate base to reconstruct from). This is
239
+ * not a hand-edit; it is the plain missing-base case the reconciliation core
240
+ * already handles by treating `base` as absent, so no extra warning is added
241
+ * here.
242
+ *
243
+ * Pure: no IO. The candidate base reconstruction (cache read, Git) is the
244
+ * orchestrator's responsibility; this function only compares in-memory digests.
245
+ *
246
+ * @param ours The current curated artifact carrying the recorded provenance.
247
+ * @param candidateBase The reconstructed Base_Artifact, or undefined on cache miss.
248
+ * @param options Optional serializer options (must match those used at import).
249
+ * @returns A ProvenanceVerification describing whether the base can be trusted.
250
+ */
251
+ export function verifyProvenanceBase(
252
+ ours: KnowledgeArtifact,
253
+ candidateBase: KnowledgeArtifact | undefined,
254
+ options?: CanonicalSerializerOptions,
255
+ ): ProvenanceVerification {
256
+ const provenance = readProvenance(ours);
257
+ const recordedDigest = provenance?.baseDigest;
258
+
259
+ // Nothing to verify: no recorded provenance or no candidate base to check
260
+ // against. The reconciliation core treats this as an absent/untrusted base
261
+ // on its own; do not manufacture a hand-edit warning.
262
+ if (recordedDigest === undefined || candidateBase === undefined) {
263
+ return {
264
+ verified: false,
265
+ recordedDigest,
266
+ recomputedDigest: undefined,
267
+ diagnostic: undefined,
268
+ };
269
+ }
270
+
271
+ const { digest: recomputedDigest } = computeBaseDigest(
272
+ candidateBase,
273
+ options,
274
+ );
275
+
276
+ // The candidate base could not be serialized — treat as unverifiable rather
277
+ // than as a hand-edit; the core degrades via the untrusted-base path.
278
+ if (recomputedDigest === undefined) {
279
+ return {
280
+ verified: false,
281
+ recordedDigest,
282
+ recomputedDigest: undefined,
283
+ diagnostic: undefined,
284
+ };
285
+ }
286
+
287
+ if (recomputedDigest === recordedDigest) {
288
+ return {
289
+ verified: true,
290
+ recordedDigest,
291
+ recomputedDigest,
292
+ diagnostic: undefined,
293
+ };
294
+ }
295
+
296
+ // Digests diverge: the artifact was hand-edited such that its provenance no
297
+ // longer matches the reconstructed base (Requirement 18.16).
298
+ return {
299
+ verified: false,
300
+ recordedDigest,
301
+ recomputedDigest,
302
+ diagnostic: buildUnverifiedDiagnostic(ours.name),
303
+ };
304
+ }
305
+
306
+ /**
307
+ * The result of preparing a reconciliation input with provenance
308
+ * self-verification applied.
309
+ */
310
+ export interface SelfVerifiedReconcileInput {
311
+ /**
312
+ * A ReconcileInput ready to pass to `reconcileArtifact`. When
313
+ * self-verification failed for a supplied candidate base, `baseUnverified`
314
+ * is forced to `true` so the core runs the reduced-confidence two-way path.
315
+ */
316
+ readonly input: ReconcileInput;
317
+ /** The verification outcome that produced this input. */
318
+ readonly verification: ProvenanceVerification;
319
+ /**
320
+ * A warning diagnostic to surface alongside reconciliation when
321
+ * self-verification failed; undefined otherwise. The reconciliation core
322
+ * itself also emits its own reduced-confidence artifact-level diagnostic when
323
+ * `baseUnverified` is set, so callers that pass this input straight through
324
+ * do not need to merge this diagnostic; it is exposed for callers that verify
325
+ * without immediately reconciling.
326
+ */
327
+ readonly diagnostic: ReconciliationDiagnostic | undefined;
328
+ }
329
+
330
+ /**
331
+ * Layer provenance self-verification on top of a would-be reconciliation input.
332
+ *
333
+ * Given Base (a reconstructed candidate), Ours, Theirs, and a policy, this
334
+ * recomputes and verifies Ours's recorded Base_Digest against the candidate
335
+ * base. If verification fails (hand-edited provenance), it returns a
336
+ * ReconcileInput with `baseUnverified: true` so `reconcileArtifact` degrades to
337
+ * the reduced-confidence two-way path (Requirement 18.16). When verification
338
+ * succeeds — or when there is simply no provenance/base to check — the input is
339
+ * returned with `baseUnverified` reflecting the original request.
340
+ *
341
+ * Pure: no IO. Composes `verifyProvenanceBase` with the caller's request shape.
342
+ *
343
+ * @param input The three-way reconciliation request (Base optional).
344
+ * @param options Optional serializer options for digest recomputation.
345
+ * @returns The (possibly downgraded) ReconcileInput plus the verification.
346
+ */
347
+ export function selfVerifyReconcileInput(
348
+ input: ReconcileInput,
349
+ options?: CanonicalSerializerOptions,
350
+ ): SelfVerifiedReconcileInput {
351
+ const verification = verifyProvenanceBase(input.ours, input.base, options);
352
+
353
+ // Only downgrade when there is a genuine, verifiable mismatch (a diagnostic
354
+ // was produced). The missing-provenance / missing-base cases leave the
355
+ // caller's original signal untouched.
356
+ if (verification.diagnostic !== undefined) {
357
+ return {
358
+ input: { ...input, baseUnverified: true },
359
+ verification,
360
+ diagnostic: verification.diagnostic,
361
+ };
362
+ }
363
+
364
+ return {
365
+ input,
366
+ verification,
367
+ diagnostic: undefined,
368
+ };
369
+ }