@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,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance Backfill (Orchestration Shell)
|
|
3
|
+
*
|
|
4
|
+
* ORCHESTRATION LAYER — this module performs filesystem IO (reading upstream
|
|
5
|
+
* sources and the curated knowledge tree, rewriting `knowledge.md`, seeding the
|
|
6
|
+
* base cache) and therefore lives OUTSIDE `src/rosetta/**` (the
|
|
7
|
+
* Pure_Translation_Boundary). It composes the pure Rosetta Stone helpers rather
|
|
8
|
+
* than reimplementing digest, cache, or serialization logic:
|
|
9
|
+
*
|
|
10
|
+
* - `buildProvenanceRecord` / `writeBaseArtifact` / `AcquisitionContext`
|
|
11
|
+
* (src/base-cache.ts, task 19.4) — provenance assembly and the git-ignored
|
|
12
|
+
* base cache at `upstream/.kanon-base/<upstream>/<name>@<digest>`.
|
|
13
|
+
* - `serializeCanonical` (src/rosetta/canonical.ts) — the deterministic
|
|
14
|
+
* canonical byte plan re-rendered into the existing artifact directory.
|
|
15
|
+
* - the built-in source translators (src/rosetta/builtins/sources/*) — turn the
|
|
16
|
+
* current upstream source into the Theirs_Artifact whose digest becomes the
|
|
17
|
+
* recorded `baseDigest`.
|
|
18
|
+
*
|
|
19
|
+
* This is the ONE-TIME backfill described in ADR-0049: existing distilled
|
|
20
|
+
* artifacts carry no `ProvenanceRecord`, so until backfilled they fall back to
|
|
21
|
+
* skip-or-force on re-sync. Backfill matches each distilled artifact to the
|
|
22
|
+
* current upstream by NAME (the reduced-confidence path — no true base ancestor
|
|
23
|
+
* was recorded originally), records the CURRENT upstream `baseDigest` (accepting
|
|
24
|
+
* that any pre-existing drift is baked into the base), writes the provenance
|
|
25
|
+
* block into `knowledge.md`, and seeds the base cache so the first subsequent
|
|
26
|
+
* re-sync has a common ancestor for three-way reconciliation.
|
|
27
|
+
*
|
|
28
|
+
* It is the mechanical successor to the retired hand-maintained drift scripts
|
|
29
|
+
* (`compare-kiro-powers.sh`, `compare-kiro-powers-full.sh`, `diff-kiro-body.sh`,
|
|
30
|
+
* `diff-kiro-steering.sh`) whose hardcoded artifact maps and absolute paths had
|
|
31
|
+
* drifted from reality.
|
|
32
|
+
*
|
|
33
|
+
* Requirements: 18.1, 18.9
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { exists, readdir, readFile, writeFile } from "node:fs/promises";
|
|
37
|
+
import { basename, join } from "node:path";
|
|
38
|
+
import {
|
|
39
|
+
type AcquisitionContext,
|
|
40
|
+
buildProvenanceRecord,
|
|
41
|
+
writeBaseArtifact,
|
|
42
|
+
} from "./base-cache";
|
|
43
|
+
import { translateKiroPower } from "./rosetta/builtins/sources/kiro-power";
|
|
44
|
+
import { translateKiroSkill } from "./rosetta/builtins/sources/kiro-skill";
|
|
45
|
+
import { translateSuperpowers } from "./rosetta/builtins/sources/superpowers";
|
|
46
|
+
import { serializeCanonical } from "./rosetta/canonical";
|
|
47
|
+
import { codePointCompare } from "./rosetta/contracts";
|
|
48
|
+
import type { SourceTranslatorContext } from "./rosetta/registry";
|
|
49
|
+
import type {
|
|
50
|
+
FormatIdentifier,
|
|
51
|
+
KnowledgeArtifact,
|
|
52
|
+
NormalizedRelativePath,
|
|
53
|
+
SourceDocument,
|
|
54
|
+
} from "./schemas";
|
|
55
|
+
|
|
56
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
57
|
+
// Types
|
|
58
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
59
|
+
|
|
60
|
+
/** Source formats a backfill can distill from (path-based upstream layouts). */
|
|
61
|
+
export type BackfillSourceFormat = "kiro-power" | "kiro-skill" | "superpowers";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The Rosetta Stone Format_Contract identifier and version recorded in
|
|
65
|
+
* provenance for each source format, e.g. `kiro-power@1`. Mirrors the map in
|
|
66
|
+
* `import.ts` so a backfilled record is indistinguishable from an
|
|
67
|
+
* acquisition-time record.
|
|
68
|
+
*/
|
|
69
|
+
const SOURCE_CONTRACT_IDENTIFIERS: Record<BackfillSourceFormat, string> = {
|
|
70
|
+
"kiro-power": "kiro-power@1",
|
|
71
|
+
"kiro-skill": "kiro-skill@1",
|
|
72
|
+
superpowers: "superpowers@1",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Primary marker file per source format, used to locate/validate a source dir. */
|
|
76
|
+
const PRIMARY_MARKER: Record<BackfillSourceFormat, string> = {
|
|
77
|
+
"kiro-power": "POWER.md",
|
|
78
|
+
"kiro-skill": "SKILL.md",
|
|
79
|
+
superpowers: "SKILL.md",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The per-artifact classification a backfill produces. Exactly one outcome is
|
|
84
|
+
* assigned to each distilled artifact considered.
|
|
85
|
+
*/
|
|
86
|
+
export type BackfillOutcome =
|
|
87
|
+
| "backfilled"
|
|
88
|
+
| "skipped-has-provenance"
|
|
89
|
+
| "unmatched"
|
|
90
|
+
| "translation-failed";
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The result of considering a single distilled artifact for backfill.
|
|
94
|
+
*/
|
|
95
|
+
export interface BackfillEntry {
|
|
96
|
+
/** The distilled artifact name (its directory name under `knowledgeDir`). */
|
|
97
|
+
readonly name: string;
|
|
98
|
+
/** The classification assigned to this artifact. */
|
|
99
|
+
readonly outcome: BackfillOutcome;
|
|
100
|
+
/** The matched upstream source subdirectory name, when matched. */
|
|
101
|
+
readonly upstreamName?: string;
|
|
102
|
+
/** The `sha256:<hex>` base digest recorded, when backfilled. */
|
|
103
|
+
readonly baseDigest?: string;
|
|
104
|
+
/** The base-cache directory written, when backfilled and not a dry run. */
|
|
105
|
+
readonly baseCachePath?: string;
|
|
106
|
+
/** A human-readable explanation for skipped/unmatched/failed outcomes. */
|
|
107
|
+
readonly detail?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Options for a single upstream's backfill pass.
|
|
112
|
+
*/
|
|
113
|
+
export interface BackfillUpstreamOptions {
|
|
114
|
+
/** The upstream identifier — matches a key in config `upstreams`. */
|
|
115
|
+
readonly upstream: string;
|
|
116
|
+
/** Absolute path to the distilled knowledge directory (e.g. knowledge/kiro-official). */
|
|
117
|
+
readonly knowledgeDir: string;
|
|
118
|
+
/** Absolute path to the upstream source root (the subtree prefix directory). */
|
|
119
|
+
readonly upstreamRoot: string;
|
|
120
|
+
/** The source format the upstream is distilled from. */
|
|
121
|
+
readonly sourceFormat: BackfillSourceFormat;
|
|
122
|
+
/** The upstream revision recorded in provenance (subtree commit or a marker). */
|
|
123
|
+
readonly sourceRevision: string;
|
|
124
|
+
/** When true, classify without rewriting knowledge.md or seeding the cache. */
|
|
125
|
+
readonly dryRun: boolean;
|
|
126
|
+
/** Workspace root anchoring `upstream/.kanon-base/` (default: process.cwd()). */
|
|
127
|
+
readonly workspaceRoot?: string;
|
|
128
|
+
/**
|
|
129
|
+
* The import timestamp as an ISO-8601 string. Supplied by the caller so this
|
|
130
|
+
* orchestration stays reproducible in tests; defaults to now.
|
|
131
|
+
*/
|
|
132
|
+
readonly importedAt?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The full result of a backfill pass across one upstream.
|
|
137
|
+
*/
|
|
138
|
+
export interface BackfillResult {
|
|
139
|
+
/** The upstream that was backfilled. */
|
|
140
|
+
readonly upstream: string;
|
|
141
|
+
/** Per-artifact entries, ordered by artifact name (code-point order). */
|
|
142
|
+
readonly entries: readonly BackfillEntry[];
|
|
143
|
+
/** Count of artifacts that received a provenance record. */
|
|
144
|
+
readonly backfilledCount: number;
|
|
145
|
+
/** Count skipped because they already carry provenance. */
|
|
146
|
+
readonly skippedCount: number;
|
|
147
|
+
/** Count with no matching upstream source by name. */
|
|
148
|
+
readonly unmatchedCount: number;
|
|
149
|
+
/** Count whose upstream source failed to translate. */
|
|
150
|
+
readonly failedCount: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
154
|
+
// Source-document building (impure)
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Read an upstream source directory into an in-memory SourceDocument[] for the
|
|
159
|
+
* matching source translator. Mirrors the layout each translator expects:
|
|
160
|
+
* `POWER.md` + `steering/`, `SKILL.md` + `references/`, or `SKILL.md` +
|
|
161
|
+
* companion Markdown.
|
|
162
|
+
*/
|
|
163
|
+
async function buildSourceDocuments(
|
|
164
|
+
sourceDir: string,
|
|
165
|
+
format: BackfillSourceFormat,
|
|
166
|
+
): Promise<SourceDocument[]> {
|
|
167
|
+
const documents: SourceDocument[] = [];
|
|
168
|
+
const entries = await readdir(sourceDir);
|
|
169
|
+
|
|
170
|
+
if (format === "kiro-power") {
|
|
171
|
+
if (entries.includes("POWER.md")) {
|
|
172
|
+
const content = await readFile(join(sourceDir, "POWER.md"), "utf-8");
|
|
173
|
+
documents.push({
|
|
174
|
+
path: "POWER.md" as NormalizedRelativePath,
|
|
175
|
+
content,
|
|
176
|
+
executable: false,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const steeringDir = join(sourceDir, "steering");
|
|
180
|
+
if (await exists(steeringDir)) {
|
|
181
|
+
const steeringFiles = (await readdir(steeringDir))
|
|
182
|
+
.filter((f) => f.endsWith(".md"))
|
|
183
|
+
.sort(codePointCompare);
|
|
184
|
+
for (const file of steeringFiles) {
|
|
185
|
+
const content = await readFile(join(steeringDir, file), "utf-8");
|
|
186
|
+
documents.push({
|
|
187
|
+
path: `steering/${file}` as NormalizedRelativePath,
|
|
188
|
+
content,
|
|
189
|
+
executable: false,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} else if (format === "kiro-skill") {
|
|
194
|
+
if (entries.includes("SKILL.md")) {
|
|
195
|
+
const content = await readFile(join(sourceDir, "SKILL.md"), "utf-8");
|
|
196
|
+
documents.push({
|
|
197
|
+
path: "SKILL.md" as NormalizedRelativePath,
|
|
198
|
+
content,
|
|
199
|
+
executable: false,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const refsDir = join(sourceDir, "references");
|
|
203
|
+
if (await exists(refsDir)) {
|
|
204
|
+
const refFiles = (await readdir(refsDir))
|
|
205
|
+
.filter((f) => f.endsWith(".md"))
|
|
206
|
+
.sort(codePointCompare);
|
|
207
|
+
for (const file of refFiles) {
|
|
208
|
+
const content = await readFile(join(refsDir, file), "utf-8");
|
|
209
|
+
documents.push({
|
|
210
|
+
path: `references/${file}` as NormalizedRelativePath,
|
|
211
|
+
content,
|
|
212
|
+
executable: false,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
// superpowers
|
|
218
|
+
if (entries.includes("SKILL.md")) {
|
|
219
|
+
const content = await readFile(join(sourceDir, "SKILL.md"), "utf-8");
|
|
220
|
+
documents.push({
|
|
221
|
+
path: "SKILL.md" as NormalizedRelativePath,
|
|
222
|
+
content,
|
|
223
|
+
executable: false,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const additionalMd = entries
|
|
227
|
+
.filter((f) => f.endsWith(".md") && f !== "SKILL.md")
|
|
228
|
+
.sort(codePointCompare);
|
|
229
|
+
for (const file of additionalMd) {
|
|
230
|
+
const content = await readFile(join(sourceDir, file), "utf-8");
|
|
231
|
+
documents.push({
|
|
232
|
+
path: file as NormalizedRelativePath,
|
|
233
|
+
content,
|
|
234
|
+
executable: false,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return documents;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
243
|
+
// Translation dispatch (pure over documents)
|
|
244
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Translate an upstream source document set into a Theirs_Artifact via the
|
|
248
|
+
* appropriate built-in source translator. Returns undefined when translation
|
|
249
|
+
* produced no candidate.
|
|
250
|
+
*/
|
|
251
|
+
function translateSource(
|
|
252
|
+
documents: readonly SourceDocument[],
|
|
253
|
+
format: BackfillSourceFormat,
|
|
254
|
+
artifactNameHint: string,
|
|
255
|
+
): KnowledgeArtifact | undefined {
|
|
256
|
+
const context: SourceTranslatorContext = {
|
|
257
|
+
format: {
|
|
258
|
+
id: format as FormatIdentifier,
|
|
259
|
+
} as SourceTranslatorContext["format"],
|
|
260
|
+
canonicalSchemaVersion: "1.0.0",
|
|
261
|
+
options: {},
|
|
262
|
+
callerContext: { artifactNameHint },
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
let output: ReturnType<typeof translateKiroPower>;
|
|
266
|
+
switch (format) {
|
|
267
|
+
case "kiro-power":
|
|
268
|
+
output = translateKiroPower(documents, context);
|
|
269
|
+
break;
|
|
270
|
+
case "kiro-skill":
|
|
271
|
+
output = translateKiroSkill(documents, context);
|
|
272
|
+
break;
|
|
273
|
+
case "superpowers":
|
|
274
|
+
output = translateSuperpowers(documents, context);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!output.candidate) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
return output.candidate as unknown as KnowledgeArtifact;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
285
|
+
// knowledge.md rewrite (impure)
|
|
286
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Rewrite an existing distilled artifact's `knowledge.md` with the machine-
|
|
290
|
+
* managed provenance block added to its frontmatter. The full artifact is
|
|
291
|
+
* re-serialized deterministically via the Canonical_Serializer so the written
|
|
292
|
+
* bytes are stable; only `knowledge.md` is rewritten (the curated auxiliary
|
|
293
|
+
* files, workflows, and body overrides are left untouched).
|
|
294
|
+
*
|
|
295
|
+
* @returns true when knowledge.md was written; false when re-serialization
|
|
296
|
+
* failed (in which case the caller treats the artifact as unwritten).
|
|
297
|
+
*/
|
|
298
|
+
async function writeProvenanceIntoKnowledgeMd(
|
|
299
|
+
artifactDir: string,
|
|
300
|
+
distilled: KnowledgeArtifact,
|
|
301
|
+
): Promise<boolean> {
|
|
302
|
+
const { plan } = serializeCanonical(distilled, {
|
|
303
|
+
emitEmptyAuxiliaryFiles: false,
|
|
304
|
+
emitBodyOverrides: false,
|
|
305
|
+
emitWorkflows: false,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!plan) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const knowledgeFile = plan.outputFiles.find(
|
|
313
|
+
(f) => f.relativePath === "knowledge.md",
|
|
314
|
+
);
|
|
315
|
+
if (!knowledgeFile) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const content =
|
|
320
|
+
typeof knowledgeFile.content === "string"
|
|
321
|
+
? knowledgeFile.content
|
|
322
|
+
: new TextDecoder().decode(knowledgeFile.content);
|
|
323
|
+
|
|
324
|
+
await writeFile(join(artifactDir, "knowledge.md"), content, "utf-8");
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
329
|
+
// Single-artifact backfill
|
|
330
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Loader that parses a distilled artifact directory into a KnowledgeArtifact.
|
|
334
|
+
* Injected so tests can supply artifacts without a real filesystem parse, and so
|
|
335
|
+
* this module does not couple to the parser's exact result shape. Returns
|
|
336
|
+
* undefined when the directory does not parse into a valid artifact.
|
|
337
|
+
*/
|
|
338
|
+
export type DistilledArtifactLoader = (
|
|
339
|
+
artifactDir: string,
|
|
340
|
+
) => Promise<KnowledgeArtifact | undefined>;
|
|
341
|
+
|
|
342
|
+
interface BackfillOneOptions extends BackfillUpstreamOptions {
|
|
343
|
+
readonly loadDistilled: DistilledArtifactLoader;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Consider a single distilled artifact for backfill and return its classified
|
|
348
|
+
* entry, performing writes only when not a dry run.
|
|
349
|
+
*/
|
|
350
|
+
async function backfillOne(
|
|
351
|
+
distilledDir: string,
|
|
352
|
+
options: BackfillOneOptions,
|
|
353
|
+
): Promise<BackfillEntry> {
|
|
354
|
+
const name = basename(distilledDir);
|
|
355
|
+
|
|
356
|
+
const distilled = await options.loadDistilled(distilledDir);
|
|
357
|
+
if (!distilled) {
|
|
358
|
+
return {
|
|
359
|
+
name,
|
|
360
|
+
outcome: "translation-failed",
|
|
361
|
+
detail: `Could not parse distilled artifact at ${distilledDir}`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Already provenanced → skip (backfill is one-time and never overwrites an
|
|
366
|
+
// existing machine-managed record).
|
|
367
|
+
if (distilled.frontmatter.provenance) {
|
|
368
|
+
return {
|
|
369
|
+
name,
|
|
370
|
+
outcome: "skipped-has-provenance",
|
|
371
|
+
detail: "Artifact already carries a provenance record",
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Match to the current upstream by NAME (reduced-confidence path).
|
|
376
|
+
const upstreamSourceDir = join(options.upstreamRoot, name);
|
|
377
|
+
const marker = PRIMARY_MARKER[options.sourceFormat];
|
|
378
|
+
const matched =
|
|
379
|
+
(await exists(upstreamSourceDir)) &&
|
|
380
|
+
(await exists(join(upstreamSourceDir, marker)));
|
|
381
|
+
|
|
382
|
+
if (!matched) {
|
|
383
|
+
return {
|
|
384
|
+
name,
|
|
385
|
+
outcome: "unmatched",
|
|
386
|
+
detail: `No upstream source directory "${name}" with ${marker} under ${options.upstreamRoot}`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Translate the CURRENT upstream source into a Theirs_Artifact.
|
|
391
|
+
const documents = await buildSourceDocuments(
|
|
392
|
+
upstreamSourceDir,
|
|
393
|
+
options.sourceFormat,
|
|
394
|
+
);
|
|
395
|
+
const theirs = translateSource(documents, options.sourceFormat, name);
|
|
396
|
+
if (!theirs) {
|
|
397
|
+
return {
|
|
398
|
+
name,
|
|
399
|
+
outcome: "translation-failed",
|
|
400
|
+
upstreamName: name,
|
|
401
|
+
detail: `Upstream source "${name}" failed to translate`,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Compute the baseDigest and build the provenance record from the current
|
|
406
|
+
// upstream. Reuses the pure task 19.4 helper; the digest is over the
|
|
407
|
+
// provenance-free Theirs_Artifact.
|
|
408
|
+
const acquisitionContext: AcquisitionContext = {
|
|
409
|
+
upstream: options.upstream,
|
|
410
|
+
sourcePath: name,
|
|
411
|
+
sourceFormat: options.sourceFormat as FormatIdentifier,
|
|
412
|
+
sourceRevision: options.sourceRevision,
|
|
413
|
+
contract: SOURCE_CONTRACT_IDENTIFIERS[options.sourceFormat],
|
|
414
|
+
importedAt: options.importedAt ?? new Date().toISOString(),
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const { provenance } = buildProvenanceRecord(theirs, acquisitionContext);
|
|
418
|
+
if (!provenance) {
|
|
419
|
+
return {
|
|
420
|
+
name,
|
|
421
|
+
outcome: "translation-failed",
|
|
422
|
+
upstreamName: name,
|
|
423
|
+
detail: `Could not compute base digest for upstream source "${name}"`,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (options.dryRun) {
|
|
428
|
+
return {
|
|
429
|
+
name,
|
|
430
|
+
outcome: "backfilled",
|
|
431
|
+
upstreamName: name,
|
|
432
|
+
baseDigest: provenance.baseDigest,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Write the provenance block into the distilled artifact's knowledge.md.
|
|
437
|
+
const withProvenance: KnowledgeArtifact = {
|
|
438
|
+
...distilled,
|
|
439
|
+
frontmatter: { ...distilled.frontmatter, provenance },
|
|
440
|
+
};
|
|
441
|
+
const wrote = await writeProvenanceIntoKnowledgeMd(
|
|
442
|
+
distilledDir,
|
|
443
|
+
withProvenance,
|
|
444
|
+
);
|
|
445
|
+
if (!wrote) {
|
|
446
|
+
return {
|
|
447
|
+
name,
|
|
448
|
+
outcome: "translation-failed",
|
|
449
|
+
upstreamName: name,
|
|
450
|
+
detail: `Could not rewrite knowledge.md for "${name}"`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Seed the base cache with the normalized Theirs_Artifact (the exact content
|
|
455
|
+
// the recorded baseDigest fingerprints) so the first re-sync has a base.
|
|
456
|
+
const workspaceRoot = options.workspaceRoot ?? process.cwd();
|
|
457
|
+
const cacheResult = await writeBaseArtifact(
|
|
458
|
+
theirs,
|
|
459
|
+
provenance.baseDigest,
|
|
460
|
+
{ upstream: options.upstream },
|
|
461
|
+
workspaceRoot,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
name,
|
|
466
|
+
outcome: "backfilled",
|
|
467
|
+
upstreamName: name,
|
|
468
|
+
baseDigest: provenance.baseDigest,
|
|
469
|
+
baseCachePath: cacheResult.cachePath,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
474
|
+
// Upstream backfill orchestration
|
|
475
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Backfill provenance for every distilled artifact under one upstream's
|
|
479
|
+
* knowledge directory.
|
|
480
|
+
*
|
|
481
|
+
* Deterministic: distilled artifacts are processed in code-point name order and
|
|
482
|
+
* the resulting entries preserve that order, so a dry run and a subsequent write
|
|
483
|
+
* run report the same sequence.
|
|
484
|
+
*
|
|
485
|
+
* @param options The upstream backfill options.
|
|
486
|
+
* @param loadDistilled Loader that parses a distilled artifact directory.
|
|
487
|
+
* @returns The classified per-artifact entries and aggregate counts.
|
|
488
|
+
*/
|
|
489
|
+
export async function backfillUpstream(
|
|
490
|
+
options: BackfillUpstreamOptions,
|
|
491
|
+
loadDistilled: DistilledArtifactLoader,
|
|
492
|
+
): Promise<BackfillResult> {
|
|
493
|
+
const distilledEntries = (
|
|
494
|
+
await readdir(options.knowledgeDir, { withFileTypes: true })
|
|
495
|
+
)
|
|
496
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
497
|
+
.map((e) => e.name)
|
|
498
|
+
.sort(codePointCompare);
|
|
499
|
+
|
|
500
|
+
const entries: BackfillEntry[] = [];
|
|
501
|
+
for (const distilledName of distilledEntries) {
|
|
502
|
+
const distilledDir = join(options.knowledgeDir, distilledName);
|
|
503
|
+
const entry = await backfillOne(distilledDir, {
|
|
504
|
+
...options,
|
|
505
|
+
loadDistilled,
|
|
506
|
+
});
|
|
507
|
+
entries.push(entry);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
upstream: options.upstream,
|
|
512
|
+
entries,
|
|
513
|
+
backfilledCount: entries.filter((e) => e.outcome === "backfilled").length,
|
|
514
|
+
skippedCount: entries.filter((e) => e.outcome === "skipped-has-provenance")
|
|
515
|
+
.length,
|
|
516
|
+
unmatchedCount: entries.filter((e) => e.outcome === "unmatched").length,
|
|
517
|
+
failedCount: entries.filter((e) => e.outcome === "translation-failed")
|
|
518
|
+
.length,
|
|
519
|
+
};
|
|
520
|
+
}
|