@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.
- package/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bridge/mcp-server.cjs +14171 -0
- package/package.json +98 -0
- package/src/adapters/capabilities.ts +178 -0
- package/src/adapters/claude-code.ts +110 -0
- package/src/adapters/cline.ts +98 -0
- package/src/adapters/codex.ts +173 -0
- package/src/adapters/copilot.ts +106 -0
- package/src/adapters/cursor.ts +97 -0
- package/src/adapters/degradation.ts +95 -0
- package/src/adapters/index.ts +324 -0
- package/src/adapters/kiro-frontmatter.ts +139 -0
- package/src/adapters/kiro-inclusion.ts +86 -0
- package/src/adapters/kiro.ts +412 -0
- package/src/adapters/qdeveloper.ts +115 -0
- package/src/adapters/types.ts +81 -0
- package/src/adapters/windsurf.ts +96 -0
- package/src/admin.ts +283 -0
- package/src/asset-conventions.ts +118 -0
- package/src/attribution-backfill.ts +319 -0
- package/src/attribution-report.ts +95 -0
- package/src/attribution.ts +239 -0
- package/src/backends/github.ts +194 -0
- package/src/backends/http.ts +122 -0
- package/src/backends/index.ts +39 -0
- package/src/backends/local.ts +47 -0
- package/src/backends/s3.ts +157 -0
- package/src/backends/types.ts +59 -0
- package/src/base-cache.ts +270 -0
- package/src/browse-ui.ts +3754 -0
- package/src/browse.ts +1038 -0
- package/src/build.ts +1108 -0
- package/src/catalog.ts +204 -0
- package/src/cli-deprecated.ts +29 -0
- package/src/cli.ts +773 -0
- package/src/collection-admin.ts +287 -0
- package/src/collection-builder.ts +464 -0
- package/src/collections.ts +116 -0
- package/src/compatibility.ts +105 -0
- package/src/config.ts +743 -0
- package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
- package/src/eval.ts +1169 -0
- package/src/file-writer.ts +61 -0
- package/src/format-registry.ts +141 -0
- package/src/guild/auto-updater.ts +163 -0
- package/src/guild/backend-resolver.ts +49 -0
- package/src/guild/cli.ts +592 -0
- package/src/guild/collection-expander.ts +47 -0
- package/src/guild/global-cache.ts +247 -0
- package/src/guild/hook-generator.ts +100 -0
- package/src/guild/manifest.ts +154 -0
- package/src/guild/path-utils.ts +12 -0
- package/src/guild/sync.ts +622 -0
- package/src/guild/version-resolver.ts +42 -0
- package/src/help/metadata.ts +445 -0
- package/src/help/renderer.ts +265 -0
- package/src/help/typo-suggester.ts +25 -0
- package/src/hooks/expression.ts +493 -0
- package/src/hooks/pipeline.ts +141 -0
- package/src/import.ts +773 -0
- package/src/importers/claude-code.ts +134 -0
- package/src/importers/cline.ts +103 -0
- package/src/importers/codex.ts +140 -0
- package/src/importers/copilot.ts +103 -0
- package/src/importers/cursor.ts +105 -0
- package/src/importers/index.ts +390 -0
- package/src/importers/kiro.ts +110 -0
- package/src/importers/qdeveloper.ts +103 -0
- package/src/importers/types.ts +54 -0
- package/src/importers/windsurf.ts +104 -0
- package/src/install.ts +1005 -0
- package/src/manifest-admin.ts +306 -0
- package/src/mcp-bridge.ts +240 -0
- package/src/mutation/delta.ts +50 -0
- package/src/mutation/history.ts +66 -0
- package/src/mutation/operators.ts +524 -0
- package/src/mutation/runner.ts +332 -0
- package/src/new.ts +106 -0
- package/src/outcomes/collision.ts +127 -0
- package/src/outcomes/normalize.ts +208 -0
- package/src/outcomes/registry.ts +173 -0
- package/src/parser.ts +446 -0
- package/src/provenance-backfill-cli.ts +319 -0
- package/src/provenance-backfill.ts +520 -0
- package/src/publish.ts +354 -0
- package/src/reconcile-orchestrator.ts +502 -0
- package/src/reconcile-report-renderer.ts +176 -0
- package/src/resolve-body.ts +15 -0
- package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
- package/src/rosetta/builtins/contracts.ts +1033 -0
- package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
- package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
- package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
- package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
- package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
- package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
- package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
- package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
- package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
- package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
- package/src/rosetta/builtins/sources/cline-native.ts +176 -0
- package/src/rosetta/builtins/sources/codex-native.ts +343 -0
- package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
- package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
- package/src/rosetta/builtins/sources/index.ts +95 -0
- package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
- package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
- package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
- package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
- package/src/rosetta/builtins/sources/superpowers.ts +240 -0
- package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
- package/src/rosetta/builtins/targets/claude-code.ts +181 -0
- package/src/rosetta/builtins/targets/cline.ts +87 -0
- package/src/rosetta/builtins/targets/codex.ts +226 -0
- package/src/rosetta/builtins/targets/copilot.ts +103 -0
- package/src/rosetta/builtins/targets/cursor.ts +87 -0
- package/src/rosetta/builtins/targets/index.ts +60 -0
- package/src/rosetta/builtins/targets/kiro.ts +278 -0
- package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
- package/src/rosetta/builtins/targets/windsurf.ts +87 -0
- package/src/rosetta/canonical.ts +729 -0
- package/src/rosetta/compatibility.ts +432 -0
- package/src/rosetta/contracts.ts +329 -0
- package/src/rosetta/detector.ts +724 -0
- package/src/rosetta/diagnostics.ts +630 -0
- package/src/rosetta/engine-bootstrap.ts +103 -0
- package/src/rosetta/engine.ts +744 -0
- package/src/rosetta/index.ts +381 -0
- package/src/rosetta/inspection.ts +530 -0
- package/src/rosetta/plan.ts +448 -0
- package/src/rosetta/provenance-digest.ts +369 -0
- package/src/rosetta/reconcile.ts +812 -0
- package/src/rosetta/redaction.ts +467 -0
- package/src/rosetta/registry.ts +712 -0
- package/src/rosetta/renderers.ts +571 -0
- package/src/rosetta/request-guard.ts +335 -0
- package/src/rosetta/resolution.ts +419 -0
- package/src/rosetta/source-accounting.ts +233 -0
- package/src/rosetta/templates.ts +129 -0
- package/src/rosetta-cli.ts +717 -0
- package/src/rosetta-docs-generator.ts +793 -0
- package/src/rosetta-profiles-cli.ts +367 -0
- package/src/schemas.ts +1712 -0
- package/src/spec-coordination.ts +1141 -0
- package/src/temper.ts +747 -0
- package/src/template-bundle-loader.ts +312 -0
- package/src/template-engine.ts +53 -0
- package/src/translation-application-policy.ts +496 -0
- package/src/translation-orchestrator.ts +1013 -0
- package/src/translation-plan-applier.ts +473 -0
- package/src/tutorial.ts +305 -0
- package/src/validate.ts +1093 -0
- package/src/versioning.ts +553 -0
- package/src/wizard.ts +660 -0
- package/src/workspace.ts +237 -0
- package/templates/eval-contexts/claude-code.md.njk +6 -0
- package/templates/eval-contexts/cline.md.njk +6 -0
- package/templates/eval-contexts/copilot.md.njk +6 -0
- package/templates/eval-contexts/cursor.md.njk +6 -0
- package/templates/eval-contexts/kiro.md.njk +10 -0
- package/templates/eval-contexts/qdeveloper.md.njk +6 -0
- package/templates/eval-contexts/windsurf.md.njk +6 -0
- package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
- package/templates/harness-adapters/_base/base.md.njk +16 -0
- package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
- package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
- package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
- package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
- package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
- package/templates/harness-adapters/cline/hook.sh.njk +4 -0
- package/templates/harness-adapters/cline/mcp.json.njk +1 -0
- package/templates/harness-adapters/cline/rule.md.njk +1 -0
- package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
- package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
- package/templates/harness-adapters/codex/skill.md.njk +27 -0
- package/templates/harness-adapters/copilot/agents.md.njk +1 -0
- package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
- package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
- package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
- package/templates/harness-adapters/cursor/rule.md.njk +6 -0
- package/templates/harness-adapters/kiro/hook.json.njk +1 -0
- package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
- package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
- package/templates/harness-adapters/kiro/power.md.njk +12 -0
- package/templates/harness-adapters/kiro/steering.md.njk +16 -0
- package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
- package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
- package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
- package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
- package/templates/knowledge/hooks.yaml.njk +4 -0
- package/templates/knowledge/knowledge.md.njk +53 -0
- 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
|
+
}
|