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