@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,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
|
+
}
|