@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,729 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rosetta Stone — Pure Canonical Parser and Serializer
|
|
3
|
+
*
|
|
4
|
+
* Parses in-memory SourceDocuments into a validated KnowledgeArtifact candidate.
|
|
5
|
+
* Serializes a validated KnowledgeArtifact into a deterministic TranslationPlan.
|
|
6
|
+
* Returns structured TranslationDiagnostics for grammar/schema errors instead of
|
|
7
|
+
* throwing exceptions.
|
|
8
|
+
*
|
|
9
|
+
* CONSTRAINTS:
|
|
10
|
+
* - NO filesystem, process, clock, random, Git, or network imports
|
|
11
|
+
* - Accepts SourceDocument[] (in-memory documents with normalized relative paths)
|
|
12
|
+
* - Returns diagnostics, never throws for parse/validation failures
|
|
13
|
+
* - Serialization is deterministic: same input always produces same byte output
|
|
14
|
+
*
|
|
15
|
+
* Requirements: 4.5, 4.6, 5.1, 5.2, 5.3, 5.5, 5.6, 6.6, 12.1, 13.7
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import matter from "gray-matter";
|
|
19
|
+
import * as yaml from "js-yaml";
|
|
20
|
+
import {
|
|
21
|
+
type CanonicalHook,
|
|
22
|
+
type Frontmatter,
|
|
23
|
+
FrontmatterSchema,
|
|
24
|
+
HarnessNameSchema,
|
|
25
|
+
HooksFileSchema,
|
|
26
|
+
type KnowledgeArtifact,
|
|
27
|
+
KnowledgeArtifactSchema,
|
|
28
|
+
type McpServerDefinition,
|
|
29
|
+
McpServersFileSchema,
|
|
30
|
+
type OutputFile,
|
|
31
|
+
type SourceDocumentInput,
|
|
32
|
+
type TranslationDiagnostic,
|
|
33
|
+
type TranslationPlan,
|
|
34
|
+
type WorkflowFile,
|
|
35
|
+
} from "../schemas";
|
|
36
|
+
import { codePointCompare } from "./contracts";
|
|
37
|
+
import { createDiagnostic } from "./diagnostics";
|
|
38
|
+
import { createPlan } from "./plan";
|
|
39
|
+
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
41
|
+
// Types
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Context provided to the canonical parser alongside source documents.
|
|
46
|
+
*/
|
|
47
|
+
export interface CanonicalParserContext {
|
|
48
|
+
/** Hint for the artifact name when not derivable from frontmatter. */
|
|
49
|
+
readonly artifactNameHint?: string;
|
|
50
|
+
/** Canonical schema version for validation context. */
|
|
51
|
+
readonly canonicalSchemaVersion?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Output of the pure canonical parser.
|
|
56
|
+
*/
|
|
57
|
+
export interface CanonicalParserOutput {
|
|
58
|
+
/** The parsed and validated KnowledgeArtifact, or undefined on blocking failure. */
|
|
59
|
+
readonly artifact: KnowledgeArtifact | undefined;
|
|
60
|
+
/** Structured diagnostics for any grammar/schema/validation issues. */
|
|
61
|
+
readonly diagnostics: TranslationDiagnostic[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
65
|
+
// Known Frontmatter Keys — Derived from Schema
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Derive the set of known frontmatter keys from the Zod schema shape.
|
|
70
|
+
*
|
|
71
|
+
* FrontmatterSchema uses `.passthrough().superRefine()` which makes internal
|
|
72
|
+
* navigation unreliable across Zod versions. We use Zod's `keyof()` when
|
|
73
|
+
* available, and fall back to parsing a probe object to discover declared keys.
|
|
74
|
+
*
|
|
75
|
+
* We also include 'harness-config' which is validated via .passthrough() +
|
|
76
|
+
* superRefine but is a recognized canonical key.
|
|
77
|
+
*/
|
|
78
|
+
function deriveKnownFrontmatterKeys(): ReadonlySet<string> {
|
|
79
|
+
// Strategy: parse a minimal valid object to extract the full key set.
|
|
80
|
+
// FrontmatterSchema.passthrough() means parsed output retains all keys,
|
|
81
|
+
// but the declared shape keys receive defaults/transforms. We can discover
|
|
82
|
+
// them by inspecting what `safeParse({name:"x"}).data` produces (defaults
|
|
83
|
+
// cause declared keys to appear even when not supplied).
|
|
84
|
+
const probe = FrontmatterSchema.safeParse({ name: "probe" });
|
|
85
|
+
const keys = new Set<string>();
|
|
86
|
+
|
|
87
|
+
// Add keys discovered from parsing (these are fields with defaults)
|
|
88
|
+
if (probe.success && probe.data && typeof probe.data === "object") {
|
|
89
|
+
for (const k of Object.keys(probe.data)) {
|
|
90
|
+
keys.add(k);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Add all keys from CANONICAL_KEY_ORDER (covers optional fields without defaults)
|
|
95
|
+
for (const k of CANONICAL_KEY_ORDER) {
|
|
96
|
+
keys.add(k);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// harness-config is a recognized passthrough key validated in superRefine
|
|
100
|
+
keys.add("harness-config");
|
|
101
|
+
// Additional schema fields not in CANONICAL_KEY_ORDER
|
|
102
|
+
keys.add("migrations");
|
|
103
|
+
keys.add("outcomes");
|
|
104
|
+
keys.add("file_patterns");
|
|
105
|
+
return keys;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Cached set of known canonical frontmatter keys derived from FrontmatterSchema.
|
|
110
|
+
*/
|
|
111
|
+
let _knownKeysCache: ReadonlySet<string> | undefined;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns the schema-derived set of known frontmatter keys.
|
|
115
|
+
* Cached after first derivation.
|
|
116
|
+
*/
|
|
117
|
+
export function getKnownFrontmatterKeys(): ReadonlySet<string> {
|
|
118
|
+
if (!_knownKeysCache) {
|
|
119
|
+
_knownKeysCache = deriveKnownFrontmatterKeys();
|
|
120
|
+
}
|
|
121
|
+
return _knownKeysCache;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
125
|
+
// Pure Canonical Parser
|
|
126
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse in-memory source documents into a validated KnowledgeArtifact.
|
|
130
|
+
*
|
|
131
|
+
* This function is pure: no filesystem, process, clock, random, Git, or
|
|
132
|
+
* network access. All input is provided via `documents` and `context`.
|
|
133
|
+
*
|
|
134
|
+
* @param documents - In-memory documents with normalized relative paths
|
|
135
|
+
* @param context - Optional context (artifact name hint, schema version)
|
|
136
|
+
* @returns Parsed artifact (or undefined) plus structured diagnostics
|
|
137
|
+
*/
|
|
138
|
+
export function parseCanonical(
|
|
139
|
+
documents: readonly SourceDocumentInput[],
|
|
140
|
+
context: CanonicalParserContext = {},
|
|
141
|
+
): CanonicalParserOutput {
|
|
142
|
+
const diagnostics: TranslationDiagnostic[] = [];
|
|
143
|
+
|
|
144
|
+
// --- Step 1: Find knowledge.md ---
|
|
145
|
+
const knowledgeDoc = documents.find((doc) => doc.path === "knowledge.md");
|
|
146
|
+
|
|
147
|
+
if (!knowledgeDoc) {
|
|
148
|
+
diagnostics.push(
|
|
149
|
+
createDiagnostic("RS_CANONICAL_MISSING_KNOWLEDGE_MD", {
|
|
150
|
+
message: "knowledge.md not found in the provided document set.",
|
|
151
|
+
source: { path: "knowledge.md" },
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
return { artifact: undefined, diagnostics };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Step 2: Parse frontmatter from knowledge.md ---
|
|
158
|
+
const content =
|
|
159
|
+
typeof knowledgeDoc.content === "string"
|
|
160
|
+
? knowledgeDoc.content
|
|
161
|
+
: new TextDecoder().decode(knowledgeDoc.content);
|
|
162
|
+
|
|
163
|
+
let parsed: matter.GrayMatterFile<string>;
|
|
164
|
+
try {
|
|
165
|
+
parsed = matter(content);
|
|
166
|
+
} catch (e: unknown) {
|
|
167
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
168
|
+
diagnostics.push(
|
|
169
|
+
createDiagnostic("RS_CANONICAL_INVALID_FRONTMATTER", {
|
|
170
|
+
message: `Invalid YAML frontmatter: ${msg}`,
|
|
171
|
+
source: { path: "knowledge.md" },
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
return { artifact: undefined, diagnostics };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const rawData = parsed.data ?? {};
|
|
178
|
+
|
|
179
|
+
// Infer name from context hint if not in frontmatter
|
|
180
|
+
if (!rawData.name && context.artifactNameHint) {
|
|
181
|
+
rawData.name = context.artifactNameHint;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- Step 3: Split known fields from extra fields ---
|
|
185
|
+
const knownKeys = getKnownFrontmatterKeys();
|
|
186
|
+
const extraFields: Record<string, unknown> = {};
|
|
187
|
+
for (const [key, value] of Object.entries(rawData)) {
|
|
188
|
+
if (!knownKeys.has(key)) {
|
|
189
|
+
extraFields[key] = value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Extract harness-config before validation
|
|
194
|
+
const _harnessConfig: Record<string, unknown> =
|
|
195
|
+
rawData["harness-config"] ?? {};
|
|
196
|
+
|
|
197
|
+
// Validate frontmatter against schema
|
|
198
|
+
const fmResult = FrontmatterSchema.safeParse(rawData);
|
|
199
|
+
if (!fmResult.success) {
|
|
200
|
+
for (const issue of fmResult.error.issues) {
|
|
201
|
+
diagnostics.push(
|
|
202
|
+
createDiagnostic("RS_CANONICAL_INVALID", {
|
|
203
|
+
message: `Frontmatter validation error: ${issue.path.join(".") || "root"}: ${issue.message}`,
|
|
204
|
+
source: { path: "knowledge.md" },
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return { artifact: undefined, diagnostics };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const frontmatter: Frontmatter = fmResult.data;
|
|
212
|
+
const body = parsed.content.trim();
|
|
213
|
+
|
|
214
|
+
// --- Step 4: Parse hooks.yaml if present ---
|
|
215
|
+
const hooksDoc = documents.find((doc) => doc.path === "hooks.yaml");
|
|
216
|
+
let hooks: CanonicalHook[] = [];
|
|
217
|
+
|
|
218
|
+
if (hooksDoc) {
|
|
219
|
+
const hooksContent =
|
|
220
|
+
typeof hooksDoc.content === "string"
|
|
221
|
+
? hooksDoc.content
|
|
222
|
+
: new TextDecoder().decode(hooksDoc.content);
|
|
223
|
+
|
|
224
|
+
let hooksParsed: unknown;
|
|
225
|
+
try {
|
|
226
|
+
// js-yaml 5.x throws "expected a document, but the input is empty" on
|
|
227
|
+
// empty/whitespace-only input, where 4.x returned undefined. Treat
|
|
228
|
+
// blank content as an empty document so the empty-array contract below
|
|
229
|
+
// holds across both versions.
|
|
230
|
+
hooksParsed =
|
|
231
|
+
hooksContent.trim() === "" ? undefined : yaml.load(hooksContent);
|
|
232
|
+
} catch (e: unknown) {
|
|
233
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
234
|
+
diagnostics.push(
|
|
235
|
+
createDiagnostic("RS_CANONICAL_INVALID_YAML", {
|
|
236
|
+
message: `Invalid YAML in hooks.yaml: ${msg}`,
|
|
237
|
+
source: { path: "hooks.yaml" },
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
return { artifact: undefined, diagnostics };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Empty YAML parses as null/undefined — treat as empty array
|
|
244
|
+
if (
|
|
245
|
+
hooksParsed === null ||
|
|
246
|
+
hooksParsed === undefined ||
|
|
247
|
+
(Array.isArray(hooksParsed) && hooksParsed.length === 0)
|
|
248
|
+
) {
|
|
249
|
+
hooks = [];
|
|
250
|
+
} else {
|
|
251
|
+
const hooksValidation = HooksFileSchema.safeParse(hooksParsed);
|
|
252
|
+
if (!hooksValidation.success) {
|
|
253
|
+
for (const issue of hooksValidation.error.issues) {
|
|
254
|
+
diagnostics.push(
|
|
255
|
+
createDiagnostic("RS_CANONICAL_INVALID_YAML", {
|
|
256
|
+
message: `hooks.yaml validation error: ${issue.path.join(".") || "root"}: ${issue.message}`,
|
|
257
|
+
source: { path: "hooks.yaml" },
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return { artifact: undefined, diagnostics };
|
|
262
|
+
}
|
|
263
|
+
hooks = hooksValidation.data;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// --- Step 5: Parse mcp-servers.yaml if present ---
|
|
268
|
+
const mcpDoc = documents.find((doc) => doc.path === "mcp-servers.yaml");
|
|
269
|
+
let mcpServers: McpServerDefinition[] = [];
|
|
270
|
+
|
|
271
|
+
if (mcpDoc) {
|
|
272
|
+
const mcpContent =
|
|
273
|
+
typeof mcpDoc.content === "string"
|
|
274
|
+
? mcpDoc.content
|
|
275
|
+
: new TextDecoder().decode(mcpDoc.content);
|
|
276
|
+
|
|
277
|
+
let mcpParsed: unknown;
|
|
278
|
+
try {
|
|
279
|
+
// See the hooks.yaml note above: js-yaml 5.x throws on empty input.
|
|
280
|
+
mcpParsed = mcpContent.trim() === "" ? undefined : yaml.load(mcpContent);
|
|
281
|
+
} catch (e: unknown) {
|
|
282
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
283
|
+
diagnostics.push(
|
|
284
|
+
createDiagnostic("RS_CANONICAL_INVALID_YAML", {
|
|
285
|
+
message: `Invalid YAML in mcp-servers.yaml: ${msg}`,
|
|
286
|
+
source: { path: "mcp-servers.yaml" },
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
return { artifact: undefined, diagnostics };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (
|
|
293
|
+
mcpParsed === null ||
|
|
294
|
+
mcpParsed === undefined ||
|
|
295
|
+
(Array.isArray(mcpParsed) && mcpParsed.length === 0)
|
|
296
|
+
) {
|
|
297
|
+
mcpServers = [];
|
|
298
|
+
} else {
|
|
299
|
+
const mcpValidation = McpServersFileSchema.safeParse(mcpParsed);
|
|
300
|
+
if (!mcpValidation.success) {
|
|
301
|
+
for (const issue of mcpValidation.error.issues) {
|
|
302
|
+
diagnostics.push(
|
|
303
|
+
createDiagnostic("RS_CANONICAL_INVALID_YAML", {
|
|
304
|
+
message: `mcp-servers.yaml validation error: ${issue.path.join(".") || "root"}: ${issue.message}`,
|
|
305
|
+
source: { path: "mcp-servers.yaml" },
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return { artifact: undefined, diagnostics };
|
|
310
|
+
}
|
|
311
|
+
mcpServers = mcpValidation.data;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// --- Step 6: Parse workflows/** documents ---
|
|
316
|
+
const WORKFLOWS_PREFIX = "workflows/";
|
|
317
|
+
const workflowDocs = documents.filter((doc) =>
|
|
318
|
+
doc.path.startsWith(WORKFLOWS_PREFIX),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const workflows: WorkflowFile[] = [];
|
|
322
|
+
const seenNormalizedPaths = new Set<string>();
|
|
323
|
+
|
|
324
|
+
// Sort workflow documents by normalized filename using code-point comparison
|
|
325
|
+
const sortedWorkflowDocs = [...workflowDocs].sort((a, b) =>
|
|
326
|
+
codePointCompare(a.path, b.path),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
for (const wfDoc of sortedWorkflowDocs) {
|
|
330
|
+
const relativePath = wfDoc.path.slice(WORKFLOWS_PREFIX.length);
|
|
331
|
+
|
|
332
|
+
// Check for directory traversal
|
|
333
|
+
if (relativePath.includes("..")) {
|
|
334
|
+
diagnostics.push(
|
|
335
|
+
createDiagnostic("RS_CANONICAL_WORKFLOW_TRAVERSAL", {
|
|
336
|
+
message: `Workflow path contains traversal: ${wfDoc.path}`,
|
|
337
|
+
source: { path: wfDoc.path },
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
return { artifact: undefined, diagnostics };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check for duplicate normalized paths
|
|
344
|
+
const normalized = relativePath.toLowerCase();
|
|
345
|
+
if (seenNormalizedPaths.has(normalized)) {
|
|
346
|
+
diagnostics.push(
|
|
347
|
+
createDiagnostic("RS_CANONICAL_DUPLICATE_WORKFLOW", {
|
|
348
|
+
message: `Duplicate normalized workflow path: ${relativePath}`,
|
|
349
|
+
source: { path: wfDoc.path },
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
return { artifact: undefined, diagnostics };
|
|
353
|
+
}
|
|
354
|
+
seenNormalizedPaths.add(normalized);
|
|
355
|
+
|
|
356
|
+
const wfContent =
|
|
357
|
+
typeof wfDoc.content === "string"
|
|
358
|
+
? wfDoc.content
|
|
359
|
+
: new TextDecoder().decode(wfDoc.content);
|
|
360
|
+
|
|
361
|
+
// Derive workflow name from filename (strip extension, normalize separators)
|
|
362
|
+
const name = relativePath
|
|
363
|
+
.replace(/\.[^./]+$/, "")
|
|
364
|
+
.replace(/[/-]/g, " ")
|
|
365
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
366
|
+
|
|
367
|
+
workflows.push({
|
|
368
|
+
name,
|
|
369
|
+
filename: relativePath,
|
|
370
|
+
content: wfContent.trim(),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// --- Step 7: Parse body.<harness>.md documents ---
|
|
375
|
+
const BODY_OVERRIDE_RE = /^body\.(.+)\.md$/;
|
|
376
|
+
const bodyOverrides: Record<string, string> = {};
|
|
377
|
+
|
|
378
|
+
for (const doc of documents) {
|
|
379
|
+
const match = doc.path.match(BODY_OVERRIDE_RE);
|
|
380
|
+
if (!match) continue;
|
|
381
|
+
|
|
382
|
+
const harness = match[1];
|
|
383
|
+
const harnessValidation = HarnessNameSchema.safeParse(harness);
|
|
384
|
+
if (!harnessValidation.success) {
|
|
385
|
+
diagnostics.push(
|
|
386
|
+
createDiagnostic("RS_CANONICAL_INVALID_BODY_OVERRIDE", {
|
|
387
|
+
message: `Invalid harness name "${harness}" in body override file: ${doc.path}`,
|
|
388
|
+
source: { path: doc.path },
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
// Invalid body overrides are diagnostics, not silent ignores
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const overrideContent =
|
|
396
|
+
typeof doc.content === "string"
|
|
397
|
+
? doc.content
|
|
398
|
+
: new TextDecoder().decode(doc.content);
|
|
399
|
+
|
|
400
|
+
// Strip frontmatter if present — only the body is used
|
|
401
|
+
const parsedOverride = matter(overrideContent);
|
|
402
|
+
bodyOverrides[harness] = parsedOverride.content.trim();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// --- Step 8: Assemble and validate the full artifact ---
|
|
406
|
+
const artifactName =
|
|
407
|
+
frontmatter.name || context.artifactNameHint || "unknown";
|
|
408
|
+
|
|
409
|
+
// sourcePath is a normalized logical source identifier, never an absolute path
|
|
410
|
+
const sourcePath = context.artifactNameHint || artifactName;
|
|
411
|
+
|
|
412
|
+
const artifact: KnowledgeArtifact = {
|
|
413
|
+
name: artifactName,
|
|
414
|
+
frontmatter,
|
|
415
|
+
body,
|
|
416
|
+
hooks,
|
|
417
|
+
mcpServers,
|
|
418
|
+
workflows,
|
|
419
|
+
sourcePath,
|
|
420
|
+
extraFields,
|
|
421
|
+
bodyOverrides,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const validated = KnowledgeArtifactSchema.safeParse(artifact);
|
|
425
|
+
if (!validated.success) {
|
|
426
|
+
for (const issue of validated.error.issues) {
|
|
427
|
+
diagnostics.push(
|
|
428
|
+
createDiagnostic("RS_CANONICAL_INVALID", {
|
|
429
|
+
message: `Artifact validation error: ${issue.path.join(".") || "root"}: ${issue.message}`,
|
|
430
|
+
source: { path: "knowledge.md" },
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return { artifact: undefined, diagnostics };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { artifact: validated.data, diagnostics };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
441
|
+
// Canonical Serializer
|
|
442
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Canonical frontmatter key-order table. Keys present in this list are
|
|
446
|
+
* rendered first (in this order); remaining keys follow in code-point order.
|
|
447
|
+
*/
|
|
448
|
+
const CANONICAL_KEY_ORDER: readonly string[] = [
|
|
449
|
+
"name",
|
|
450
|
+
"displayName",
|
|
451
|
+
"description",
|
|
452
|
+
"keywords",
|
|
453
|
+
"author",
|
|
454
|
+
"version",
|
|
455
|
+
"type",
|
|
456
|
+
"harnesses",
|
|
457
|
+
"inclusion",
|
|
458
|
+
"file_patterns",
|
|
459
|
+
"harness-config",
|
|
460
|
+
"categories",
|
|
461
|
+
"ecosystem",
|
|
462
|
+
"depends",
|
|
463
|
+
"enhances",
|
|
464
|
+
"id",
|
|
465
|
+
"license",
|
|
466
|
+
"maturity",
|
|
467
|
+
"trust",
|
|
468
|
+
"risk-level",
|
|
469
|
+
"audience",
|
|
470
|
+
"model-assumptions",
|
|
471
|
+
"successor",
|
|
472
|
+
"replaces",
|
|
473
|
+
"changelog",
|
|
474
|
+
"collections",
|
|
475
|
+
"inherit-hooks",
|
|
476
|
+
"visibility",
|
|
477
|
+
"priority",
|
|
478
|
+
] as const;
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Options for the canonical serializer.
|
|
482
|
+
*/
|
|
483
|
+
export interface CanonicalSerializerOptions {
|
|
484
|
+
/** Whether to emit hooks.yaml/mcp-servers.yaml when empty (default: true) */
|
|
485
|
+
readonly emitEmptyAuxiliaryFiles?: boolean;
|
|
486
|
+
/** Whether to emit body.<harness>.md files (default: true) */
|
|
487
|
+
readonly emitBodyOverrides?: boolean;
|
|
488
|
+
/** Whether to emit workflow files (default: true) */
|
|
489
|
+
readonly emitWorkflows?: boolean;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Output of the canonical serializer.
|
|
494
|
+
*/
|
|
495
|
+
export interface CanonicalSerializerOutput {
|
|
496
|
+
/** The generated translation plan, or undefined on blocking failure. */
|
|
497
|
+
readonly plan: TranslationPlan | undefined;
|
|
498
|
+
/** Structured diagnostics for any validation/collision issues. */
|
|
499
|
+
readonly diagnostics: TranslationDiagnostic[];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
503
|
+
// Deterministic YAML Rendering Helper
|
|
504
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Render an object to deterministic YAML with a custom key-order comparator.
|
|
508
|
+
*
|
|
509
|
+
* Uses js-yaml's dump() with:
|
|
510
|
+
* - sortKeys: custom comparison function based on priority table
|
|
511
|
+
* - lineWidth: 80
|
|
512
|
+
* - noRefs: true (disables aliases)
|
|
513
|
+
*
|
|
514
|
+
* Returns a string with exactly one trailing newline.
|
|
515
|
+
*/
|
|
516
|
+
export function renderDeterministicYaml(
|
|
517
|
+
data: unknown,
|
|
518
|
+
keyOrder?: readonly string[],
|
|
519
|
+
): string {
|
|
520
|
+
const prioritySet = new Map<string, number>();
|
|
521
|
+
if (keyOrder) {
|
|
522
|
+
for (let i = 0; i < keyOrder.length; i++) {
|
|
523
|
+
prioritySet.set(keyOrder[i], i);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const sortKeysFn = (a: string, b: string): number => {
|
|
528
|
+
const aIdx = prioritySet.get(a);
|
|
529
|
+
const bIdx = prioritySet.get(b);
|
|
530
|
+
|
|
531
|
+
if (aIdx !== undefined && bIdx !== undefined) {
|
|
532
|
+
return aIdx - bIdx;
|
|
533
|
+
}
|
|
534
|
+
if (aIdx !== undefined) return -1;
|
|
535
|
+
if (bIdx !== undefined) return 1;
|
|
536
|
+
return codePointCompare(a, b);
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const result = yaml.dump(data, {
|
|
540
|
+
sortKeys: sortKeysFn,
|
|
541
|
+
// js-yaml 5.x removed the `noCompatMode` dump option. Its reworked scalar
|
|
542
|
+
// styler quotes YAML-1.1-ambiguous scalars (yes/no/on/off/true/null) by
|
|
543
|
+
// default, which is safe: values round-trip identically. No replacement
|
|
544
|
+
// flag is needed for deterministic canonical output.
|
|
545
|
+
lineWidth: 80,
|
|
546
|
+
noRefs: true,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Ensure exactly one trailing newline
|
|
550
|
+
return result.replace(/\n*$/, "\n");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
554
|
+
// Canonical Serializer
|
|
555
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Serialize a validated KnowledgeArtifact into a deterministic TranslationPlan.
|
|
559
|
+
*
|
|
560
|
+
* This function is pure: no filesystem, process, clock, random, Git, or
|
|
561
|
+
* network access. It produces the same byte output for the same input.
|
|
562
|
+
*
|
|
563
|
+
* @param artifact - A validated KnowledgeArtifact to serialize
|
|
564
|
+
* @param options - Optional serializer options
|
|
565
|
+
* @returns A TranslationPlan (or undefined) plus structured diagnostics
|
|
566
|
+
*/
|
|
567
|
+
export function serializeCanonical(
|
|
568
|
+
artifact: KnowledgeArtifact,
|
|
569
|
+
options?: CanonicalSerializerOptions,
|
|
570
|
+
): CanonicalSerializerOutput {
|
|
571
|
+
const diagnostics: TranslationDiagnostic[] = [];
|
|
572
|
+
const opts: Required<CanonicalSerializerOptions> = {
|
|
573
|
+
emitEmptyAuxiliaryFiles: options?.emitEmptyAuxiliaryFiles ?? true,
|
|
574
|
+
emitBodyOverrides: options?.emitBodyOverrides ?? true,
|
|
575
|
+
emitWorkflows: options?.emitWorkflows ?? true,
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// --- Step 1: Validate the artifact via KnowledgeArtifactSchema ---
|
|
579
|
+
const validated = KnowledgeArtifactSchema.safeParse(artifact);
|
|
580
|
+
if (!validated.success) {
|
|
581
|
+
for (const issue of validated.error.issues) {
|
|
582
|
+
diagnostics.push(
|
|
583
|
+
createDiagnostic("RS_CANONICAL_INVALID", {
|
|
584
|
+
message: `Artifact validation error: ${issue.path.join(".") || "root"}: ${issue.message}`,
|
|
585
|
+
source: { path: "knowledge.md" },
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return { plan: undefined, diagnostics };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const art = validated.data;
|
|
593
|
+
|
|
594
|
+
// --- Step 2: Check for extra field collisions with canonical keys ---
|
|
595
|
+
const knownKeys = getKnownFrontmatterKeys();
|
|
596
|
+
for (const key of Object.keys(art.extraFields)) {
|
|
597
|
+
if (knownKeys.has(key)) {
|
|
598
|
+
diagnostics.push(
|
|
599
|
+
createDiagnostic("RS_EXTRA_FIELD_COLLISION", {
|
|
600
|
+
message: `Extra field "${key}" collides with canonical frontmatter key.`,
|
|
601
|
+
source: { path: "knowledge.md" },
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
604
|
+
return { plan: undefined, diagnostics };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// --- Step 3: Build frontmatter object for YAML rendering ---
|
|
609
|
+
const frontmatterData: Record<string, unknown> = {};
|
|
610
|
+
|
|
611
|
+
// Copy all canonical frontmatter fields (only those that are set/non-default)
|
|
612
|
+
const fm = art.frontmatter as Record<string, unknown>;
|
|
613
|
+
for (const [key, value] of Object.entries(fm)) {
|
|
614
|
+
if (value !== undefined) {
|
|
615
|
+
frontmatterData[key] = value;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Merge extra fields (already verified no collisions)
|
|
620
|
+
for (const [key, value] of Object.entries(art.extraFields)) {
|
|
621
|
+
frontmatterData[key] = value;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// --- Step 4: Render knowledge.md ---
|
|
625
|
+
const frontmatterYaml = renderDeterministicYaml(
|
|
626
|
+
frontmatterData,
|
|
627
|
+
CANONICAL_KEY_ORDER,
|
|
628
|
+
);
|
|
629
|
+
const knowledgeMd = `---\n${frontmatterYaml}---\n${art.body}\n`;
|
|
630
|
+
|
|
631
|
+
const outputFiles: OutputFile[] = [];
|
|
632
|
+
|
|
633
|
+
outputFiles.push({
|
|
634
|
+
relativePath: "knowledge.md",
|
|
635
|
+
content: knowledgeMd,
|
|
636
|
+
executable: false,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// --- Step 5: Render hooks.yaml ---
|
|
640
|
+
if (art.hooks.length > 0 || opts.emitEmptyAuxiliaryFiles) {
|
|
641
|
+
let hooksContent: string;
|
|
642
|
+
if (art.hooks.length === 0) {
|
|
643
|
+
hooksContent = "[]\n";
|
|
644
|
+
} else {
|
|
645
|
+
// Render each hook with canonical key ordering for hook fields
|
|
646
|
+
const hookKeyOrder = [
|
|
647
|
+
"name",
|
|
648
|
+
"description",
|
|
649
|
+
"event",
|
|
650
|
+
"condition",
|
|
651
|
+
"action",
|
|
652
|
+
"gate",
|
|
653
|
+
"postcondition",
|
|
654
|
+
"state",
|
|
655
|
+
];
|
|
656
|
+
hooksContent = renderDeterministicYaml(art.hooks, hookKeyOrder);
|
|
657
|
+
}
|
|
658
|
+
outputFiles.push({
|
|
659
|
+
relativePath: "hooks.yaml",
|
|
660
|
+
content: hooksContent,
|
|
661
|
+
executable: false,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// --- Step 6: Render mcp-servers.yaml ---
|
|
666
|
+
if (art.mcpServers.length > 0 || opts.emitEmptyAuxiliaryFiles) {
|
|
667
|
+
let mcpContent: string;
|
|
668
|
+
if (art.mcpServers.length === 0) {
|
|
669
|
+
mcpContent = "[]\n";
|
|
670
|
+
} else {
|
|
671
|
+
const mcpKeyOrder = [
|
|
672
|
+
"name",
|
|
673
|
+
"transport",
|
|
674
|
+
"command",
|
|
675
|
+
"args",
|
|
676
|
+
"url",
|
|
677
|
+
"env",
|
|
678
|
+
"timeout",
|
|
679
|
+
"autoApprove",
|
|
680
|
+
"disabled",
|
|
681
|
+
];
|
|
682
|
+
mcpContent = renderDeterministicYaml(art.mcpServers, mcpKeyOrder);
|
|
683
|
+
}
|
|
684
|
+
outputFiles.push({
|
|
685
|
+
relativePath: "mcp-servers.yaml",
|
|
686
|
+
content: mcpContent,
|
|
687
|
+
executable: false,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// --- Step 7: Render workflows ---
|
|
692
|
+
if (opts.emitWorkflows && art.workflows.length > 0) {
|
|
693
|
+
// Sort workflows by filename using code-point comparison
|
|
694
|
+
const sortedWorkflows = [...art.workflows].sort((a, b) =>
|
|
695
|
+
codePointCompare(a.filename, b.filename),
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
for (const wf of sortedWorkflows) {
|
|
699
|
+
const content = `${wf.content}\n`;
|
|
700
|
+
outputFiles.push({
|
|
701
|
+
relativePath: `workflows/${wf.filename}`,
|
|
702
|
+
content,
|
|
703
|
+
executable: false,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// --- Step 8: Render body overrides ---
|
|
709
|
+
if (opts.emitBodyOverrides && Object.keys(art.bodyOverrides).length > 0) {
|
|
710
|
+
// Sort by harness name using code-point comparison
|
|
711
|
+
const sortedHarnesses = Object.keys(art.bodyOverrides).sort(
|
|
712
|
+
codePointCompare,
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
for (const harness of sortedHarnesses) {
|
|
716
|
+
const content = `${art.bodyOverrides[harness]}\n`;
|
|
717
|
+
outputFiles.push({
|
|
718
|
+
relativePath: `body.${harness}.md`,
|
|
719
|
+
content,
|
|
720
|
+
executable: false,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// --- Step 9: Create the plan via createPlan ---
|
|
726
|
+
const plan = createPlan("kanon-canonical", "1.0.0", outputFiles);
|
|
727
|
+
|
|
728
|
+
return { plan, diagnostics };
|
|
729
|
+
}
|