@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,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rosetta Stone — Plan Validation, Construction, and Deterministic Ordering
|
|
3
|
+
*
|
|
4
|
+
* Pure PlanValidator: validates plan Zod schema; normalizes and validates each
|
|
5
|
+
* relative path; rejects duplicate normalized output paths; verifies one write
|
|
6
|
+
* operation references each output file exactly once; validates content kind and
|
|
7
|
+
* executable flags; sorts files and operations deterministically; and removes
|
|
8
|
+
* operations affected by blocking output diagnostics.
|
|
9
|
+
*
|
|
10
|
+
* CONSTRAINTS:
|
|
11
|
+
* - NO filesystem, process, clock, random, Git, or network imports
|
|
12
|
+
* - Pure functions only
|
|
13
|
+
*
|
|
14
|
+
* Requirements: 6.6, 8.7, 13.1, 13.2, 13.5, 13.6
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
OutputFile,
|
|
19
|
+
PlanOperation,
|
|
20
|
+
TranslationDiagnostic,
|
|
21
|
+
TranslationPlan,
|
|
22
|
+
} from "../schemas";
|
|
23
|
+
import { TranslationPlanSchema } from "../schemas";
|
|
24
|
+
import { codePointCompare } from "./contracts";
|
|
25
|
+
import { createDiagnostic } from "./diagnostics";
|
|
26
|
+
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
28
|
+
// Types
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Result of plan validation containing the normalized plan and any diagnostics.
|
|
33
|
+
*/
|
|
34
|
+
export interface PlanValidationResult {
|
|
35
|
+
readonly valid: boolean;
|
|
36
|
+
readonly plan: TranslationPlan | null;
|
|
37
|
+
readonly diagnostics: readonly TranslationDiagnostic[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
41
|
+
// Path Normalization
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Normalize a plan path according to deterministic normalization rules:
|
|
46
|
+
* - Apply Unicode NFC normalization
|
|
47
|
+
* - Replace backslashes with forward slashes
|
|
48
|
+
* - Split into segments, reject empty/`.`/`..` segments
|
|
49
|
+
* - Reject absolute, UNC, drive-letter prefixed paths
|
|
50
|
+
* - Reject NUL characters
|
|
51
|
+
* - Return normalized joined path
|
|
52
|
+
*
|
|
53
|
+
* Returns `{ normalized, error }` — if error is set, the path is invalid.
|
|
54
|
+
*/
|
|
55
|
+
export function normalizePlanPath(
|
|
56
|
+
path: string,
|
|
57
|
+
): { ok: true; normalized: string } | { ok: false; error: string } {
|
|
58
|
+
if (path.length === 0) {
|
|
59
|
+
return { ok: false, error: "Path must not be empty" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Reject NUL characters
|
|
63
|
+
if (path.includes("\0")) {
|
|
64
|
+
return { ok: false, error: "Path must not contain NUL character" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Apply NFC normalization
|
|
68
|
+
const nfc = path.normalize("NFC");
|
|
69
|
+
|
|
70
|
+
// Replace backslashes with forward slashes
|
|
71
|
+
const normalized = nfc.replace(/\\/g, "/");
|
|
72
|
+
|
|
73
|
+
// Reject absolute, UNC, drive-letter prefixed paths
|
|
74
|
+
if (normalized.startsWith("/")) {
|
|
75
|
+
return { ok: false, error: "Path must not be absolute" };
|
|
76
|
+
}
|
|
77
|
+
if (normalized.startsWith("//")) {
|
|
78
|
+
return { ok: false, error: "Path must not be a UNC path" };
|
|
79
|
+
}
|
|
80
|
+
if (/^[A-Za-z]:/.test(normalized)) {
|
|
81
|
+
return { ok: false, error: "Path must not contain a drive prefix" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Split and validate segments
|
|
85
|
+
const segments = normalized.split("/");
|
|
86
|
+
const validSegments: string[] = [];
|
|
87
|
+
|
|
88
|
+
for (const seg of segments) {
|
|
89
|
+
if (seg === "") {
|
|
90
|
+
return { ok: false, error: "Path must not contain empty segments" };
|
|
91
|
+
}
|
|
92
|
+
if (seg === ".") {
|
|
93
|
+
return { ok: false, error: "Path must not contain '.' segments" };
|
|
94
|
+
}
|
|
95
|
+
if (seg === "..") {
|
|
96
|
+
return { ok: false, error: "Path must not contain '..' traversal" };
|
|
97
|
+
}
|
|
98
|
+
validSegments.push(seg);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { ok: true, normalized: validSegments.join("/") };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
105
|
+
// Plan Validation
|
|
106
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate a TranslationPlan:
|
|
110
|
+
* - Validates the plan against TranslationPlanSchema (Zod)
|
|
111
|
+
* - Normalizes each output file path (NFC, no traversal, no absolute, no NUL, no empty segments)
|
|
112
|
+
* - Rejects duplicate normalized paths
|
|
113
|
+
* - Verifies each output file has exactly one write operation referencing it
|
|
114
|
+
* - Verifies each operation references a valid output file index
|
|
115
|
+
* - Validates content is present and non-empty for write operations
|
|
116
|
+
* - Returns PlanValidationResult with normalized plan + diagnostics
|
|
117
|
+
*/
|
|
118
|
+
export function validatePlan(plan: unknown): PlanValidationResult {
|
|
119
|
+
const diagnostics: TranslationDiagnostic[] = [];
|
|
120
|
+
|
|
121
|
+
// 1. Validate against Zod schema
|
|
122
|
+
const parseResult = TranslationPlanSchema.safeParse(plan);
|
|
123
|
+
if (!parseResult.success) {
|
|
124
|
+
diagnostics.push(
|
|
125
|
+
createDiagnostic("RS_PLAN_SCHEMA_INVALID", {
|
|
126
|
+
message: `Plan schema validation failed: ${parseResult.error.issues.map((i) => i.message).join("; ")}`,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
return { valid: false, plan: null, diagnostics };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const parsed = parseResult.data;
|
|
133
|
+
|
|
134
|
+
// 2. Normalize and validate each output file path
|
|
135
|
+
const normalizedPaths: string[] = [];
|
|
136
|
+
const pathSet = new Map<string, number>(); // normalized path -> first index
|
|
137
|
+
let _pathErrors = false;
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < parsed.outputFiles.length; i++) {
|
|
140
|
+
const file = parsed.outputFiles[i];
|
|
141
|
+
const result = normalizePlanPath(file.relativePath);
|
|
142
|
+
|
|
143
|
+
if (!result.ok) {
|
|
144
|
+
diagnostics.push(
|
|
145
|
+
createDiagnostic("RS_PLAN_INVALID_PATH", {
|
|
146
|
+
message: `Output file [${i}] path "${file.relativePath}": ${result.error}`,
|
|
147
|
+
source: { path: file.relativePath },
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
_pathErrors = true;
|
|
151
|
+
normalizedPaths.push(file.relativePath); // keep original for reporting
|
|
152
|
+
} else {
|
|
153
|
+
normalizedPaths.push(result.normalized);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. Check for duplicate normalized paths
|
|
158
|
+
for (let i = 0; i < normalizedPaths.length; i++) {
|
|
159
|
+
const np = normalizedPaths[i];
|
|
160
|
+
if (pathSet.has(np)) {
|
|
161
|
+
diagnostics.push(
|
|
162
|
+
createDiagnostic("RS_PLAN_DUPLICATE_PATH", {
|
|
163
|
+
message: `Duplicate normalized path "${np}" at indices [${pathSet.get(np)}, ${i}]`,
|
|
164
|
+
source: { path: np },
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
_pathErrors = true;
|
|
168
|
+
} else {
|
|
169
|
+
pathSet.set(np, i);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 4. Verify operation → output file references
|
|
174
|
+
const referencedIndices = new Set<number>();
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < parsed.operations.length; i++) {
|
|
177
|
+
const op = parsed.operations[i];
|
|
178
|
+
|
|
179
|
+
// Check that the operation references a valid output file index
|
|
180
|
+
if (
|
|
181
|
+
op.outputFileIndex < 0 ||
|
|
182
|
+
op.outputFileIndex >= parsed.outputFiles.length
|
|
183
|
+
) {
|
|
184
|
+
diagnostics.push(
|
|
185
|
+
createDiagnostic("RS_PLAN_ORPHAN_OPERATION", {
|
|
186
|
+
message: `Operation [${i}] references non-existent output file index ${op.outputFileIndex}`,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
} else {
|
|
190
|
+
referencedIndices.add(op.outputFileIndex);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 5. Validate content is present and non-empty for write operations
|
|
194
|
+
if (
|
|
195
|
+
op.kind === "write-file" &&
|
|
196
|
+
op.outputFileIndex < parsed.outputFiles.length
|
|
197
|
+
) {
|
|
198
|
+
const targetFile = parsed.outputFiles[op.outputFileIndex];
|
|
199
|
+
const content = targetFile.content;
|
|
200
|
+
if (content === undefined || content === null) {
|
|
201
|
+
diagnostics.push(
|
|
202
|
+
createDiagnostic("RS_PLAN_ORPHAN_OPERATION", {
|
|
203
|
+
message: `Operation [${i}] write-file references output file with no content`,
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
} else if (
|
|
207
|
+
(typeof content === "string" && content.length === 0) ||
|
|
208
|
+
(content instanceof Uint8Array && content.length === 0)
|
|
209
|
+
) {
|
|
210
|
+
diagnostics.push(
|
|
211
|
+
createDiagnostic("RS_PLAN_ORPHAN_OPERATION", {
|
|
212
|
+
message: `Operation [${i}] write-file references output file with empty content`,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 6. Verify each output file has exactly one write operation
|
|
220
|
+
for (let i = 0; i < parsed.outputFiles.length; i++) {
|
|
221
|
+
if (!referencedIndices.has(i)) {
|
|
222
|
+
diagnostics.push(
|
|
223
|
+
createDiagnostic("RS_PLAN_ORPHAN_FILE", {
|
|
224
|
+
message: `Output file [${i}] "${parsed.outputFiles[i].relativePath}" has no corresponding operation`,
|
|
225
|
+
source: { path: parsed.outputFiles[i].relativePath },
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check for multiple operations referencing the same output file
|
|
232
|
+
const refCounts = new Map<number, number>();
|
|
233
|
+
for (const op of parsed.operations) {
|
|
234
|
+
if (
|
|
235
|
+
op.outputFileIndex >= 0 &&
|
|
236
|
+
op.outputFileIndex < parsed.outputFiles.length
|
|
237
|
+
) {
|
|
238
|
+
refCounts.set(
|
|
239
|
+
op.outputFileIndex,
|
|
240
|
+
(refCounts.get(op.outputFileIndex) ?? 0) + 1,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const [idx, count] of refCounts) {
|
|
245
|
+
if (count > 1) {
|
|
246
|
+
diagnostics.push(
|
|
247
|
+
createDiagnostic("RS_PLAN_DUPLICATE_PATH", {
|
|
248
|
+
message: `Output file [${idx}] "${parsed.outputFiles[idx].relativePath}" is referenced by ${count} operations (expected exactly 1)`,
|
|
249
|
+
source: { path: parsed.outputFiles[idx].relativePath },
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (diagnostics.length > 0) {
|
|
256
|
+
return { valid: false, plan: parsed, diagnostics };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Return normalized plan with validated paths
|
|
260
|
+
const normalizedPlan: TranslationPlan = {
|
|
261
|
+
...parsed,
|
|
262
|
+
outputFiles: parsed.outputFiles.map((file, i) => ({
|
|
263
|
+
...file,
|
|
264
|
+
relativePath: normalizedPaths[i],
|
|
265
|
+
})),
|
|
266
|
+
operations: parsed.operations.map((op) => ({
|
|
267
|
+
...op,
|
|
268
|
+
relativePath: normalizedPaths[op.outputFileIndex],
|
|
269
|
+
})),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return { valid: true, plan: normalizedPlan, diagnostics: [] };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
276
|
+
// Deterministic Sorting
|
|
277
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Sort output files and operations deterministically by normalized path,
|
|
281
|
+
* then operation kind, using Unicode code-point comparison.
|
|
282
|
+
*
|
|
283
|
+
* Returns a new plan with reindexed operations pointing to the sorted files.
|
|
284
|
+
*/
|
|
285
|
+
export function sortPlanDeterministically(
|
|
286
|
+
plan: TranslationPlan,
|
|
287
|
+
): TranslationPlan {
|
|
288
|
+
// Create indexed entries for files
|
|
289
|
+
const indexedFiles = plan.outputFiles.map((file, idx) => ({
|
|
290
|
+
file,
|
|
291
|
+
originalIndex: idx,
|
|
292
|
+
}));
|
|
293
|
+
|
|
294
|
+
// Sort files by relativePath using code-point comparison
|
|
295
|
+
indexedFiles.sort((a, b) =>
|
|
296
|
+
codePointCompare(a.file.relativePath, b.file.relativePath),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Build old-to-new index mapping
|
|
300
|
+
const oldToNew = new Map<number, number>();
|
|
301
|
+
for (let newIdx = 0; newIdx < indexedFiles.length; newIdx++) {
|
|
302
|
+
oldToNew.set(indexedFiles[newIdx].originalIndex, newIdx);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Remap and sort operations
|
|
306
|
+
const remappedOps = plan.operations.map((op) => ({
|
|
307
|
+
...op,
|
|
308
|
+
outputFileIndex: oldToNew.get(op.outputFileIndex) ?? op.outputFileIndex,
|
|
309
|
+
relativePath:
|
|
310
|
+
indexedFiles[oldToNew.get(op.outputFileIndex) ?? op.outputFileIndex]?.file
|
|
311
|
+
.relativePath ?? op.relativePath,
|
|
312
|
+
}));
|
|
313
|
+
|
|
314
|
+
// Sort operations by relativePath (code-point), then kind (code-point)
|
|
315
|
+
remappedOps.sort((a, b) => {
|
|
316
|
+
const pathCmp = codePointCompare(a.relativePath, b.relativePath);
|
|
317
|
+
if (pathCmp !== 0) return pathCmp;
|
|
318
|
+
return codePointCompare(a.kind, b.kind);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
...plan,
|
|
323
|
+
outputFiles: indexedFiles.map((entry) => entry.file),
|
|
324
|
+
operations: remappedOps,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
329
|
+
// Operation Withholding
|
|
330
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Remove operations whose output files are affected by blocking diagnostics.
|
|
334
|
+
* Returns a copy with `applicationState` set appropriately.
|
|
335
|
+
*
|
|
336
|
+
* A diagnostic "affects" an output file if its source path matches the
|
|
337
|
+
* output file's relativePath.
|
|
338
|
+
*/
|
|
339
|
+
export function withholdBlockedOperations(
|
|
340
|
+
plan: TranslationPlan,
|
|
341
|
+
blockingDiagnostics: readonly TranslationDiagnostic[],
|
|
342
|
+
): TranslationPlan {
|
|
343
|
+
if (blockingDiagnostics.length === 0) {
|
|
344
|
+
return { ...plan, applicationState: "eligible" };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Collect paths affected by blocking diagnostics
|
|
348
|
+
const blockedPaths = new Set<string>();
|
|
349
|
+
for (const diag of blockingDiagnostics) {
|
|
350
|
+
if (diag.source?.path) {
|
|
351
|
+
blockedPaths.add(diag.source.path);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// If no paths are explicitly blocked, withhold the entire plan
|
|
356
|
+
if (blockedPaths.size === 0) {
|
|
357
|
+
return {
|
|
358
|
+
...plan,
|
|
359
|
+
applicationState: "withheld",
|
|
360
|
+
operations: [],
|
|
361
|
+
policyDiagnosticCodes: [
|
|
362
|
+
...new Set(blockingDiagnostics.map((d) => d.code)),
|
|
363
|
+
],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Filter out operations whose target file path is blocked
|
|
368
|
+
const survivingOps: PlanOperation[] = [];
|
|
369
|
+
const withheldCodes = new Set<string>();
|
|
370
|
+
|
|
371
|
+
for (const op of plan.operations) {
|
|
372
|
+
const file = plan.outputFiles[op.outputFileIndex];
|
|
373
|
+
if (file && blockedPaths.has(file.relativePath)) {
|
|
374
|
+
// Collect diagnostic codes that caused withholding
|
|
375
|
+
for (const diag of blockingDiagnostics) {
|
|
376
|
+
if (diag.source?.path === file.relativePath) {
|
|
377
|
+
withheldCodes.add(diag.code);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
survivingOps.push(op);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const newState =
|
|
386
|
+
survivingOps.length === plan.operations.length
|
|
387
|
+
? "eligible"
|
|
388
|
+
: survivingOps.length === 0
|
|
389
|
+
? "withheld"
|
|
390
|
+
: "policy-required";
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
...plan,
|
|
394
|
+
operations: survivingOps,
|
|
395
|
+
applicationState: newState,
|
|
396
|
+
policyDiagnosticCodes: [...withheldCodes],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
401
|
+
// Plan Construction Helper
|
|
402
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Options for createPlan helper.
|
|
406
|
+
*/
|
|
407
|
+
export interface CreatePlanOptions {
|
|
408
|
+
readonly variant?: string;
|
|
409
|
+
readonly applicationState?: "eligible" | "policy-required" | "withheld";
|
|
410
|
+
readonly policyDiagnosticCodes?: readonly string[];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Construct a valid TranslationPlan with deterministic ordering,
|
|
415
|
+
* default applicationState, and empty policy codes.
|
|
416
|
+
*
|
|
417
|
+
* Operations are auto-generated as one write-file per output file.
|
|
418
|
+
* The plan is deterministically sorted by normalized path.
|
|
419
|
+
*/
|
|
420
|
+
export function createPlan(
|
|
421
|
+
formatId: string,
|
|
422
|
+
canonicalSchemaVersion: string,
|
|
423
|
+
outputFiles: readonly OutputFile[],
|
|
424
|
+
options?: CreatePlanOptions,
|
|
425
|
+
): TranslationPlan {
|
|
426
|
+
// Generate one write-file operation per output file
|
|
427
|
+
const operations: PlanOperation[] = outputFiles.map((file, idx) => ({
|
|
428
|
+
kind: "write-file" as const,
|
|
429
|
+
relativePath: file.relativePath,
|
|
430
|
+
outputFileIndex: idx,
|
|
431
|
+
}));
|
|
432
|
+
|
|
433
|
+
const plan: TranslationPlan = {
|
|
434
|
+
schemaVersion: "1.0",
|
|
435
|
+
formatId,
|
|
436
|
+
...(options?.variant !== undefined && { variant: options.variant }),
|
|
437
|
+
canonicalSchemaVersion,
|
|
438
|
+
outputFiles: [...outputFiles],
|
|
439
|
+
operations,
|
|
440
|
+
applicationState: options?.applicationState ?? "eligible",
|
|
441
|
+
policyDiagnosticCodes: options?.policyDiagnosticCodes
|
|
442
|
+
? [...options.policyDiagnosticCodes]
|
|
443
|
+
: [],
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Sort deterministically
|
|
447
|
+
return sortPlanDeterministically(plan);
|
|
448
|
+
}
|