@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.
Files changed (199) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -0
  4. package/bridge/mcp-server.cjs +14171 -0
  5. package/package.json +98 -0
  6. package/src/adapters/capabilities.ts +178 -0
  7. package/src/adapters/claude-code.ts +110 -0
  8. package/src/adapters/cline.ts +98 -0
  9. package/src/adapters/codex.ts +173 -0
  10. package/src/adapters/copilot.ts +106 -0
  11. package/src/adapters/cursor.ts +97 -0
  12. package/src/adapters/degradation.ts +95 -0
  13. package/src/adapters/index.ts +324 -0
  14. package/src/adapters/kiro-frontmatter.ts +139 -0
  15. package/src/adapters/kiro-inclusion.ts +86 -0
  16. package/src/adapters/kiro.ts +412 -0
  17. package/src/adapters/qdeveloper.ts +115 -0
  18. package/src/adapters/types.ts +81 -0
  19. package/src/adapters/windsurf.ts +96 -0
  20. package/src/admin.ts +283 -0
  21. package/src/asset-conventions.ts +118 -0
  22. package/src/attribution-backfill.ts +319 -0
  23. package/src/attribution-report.ts +95 -0
  24. package/src/attribution.ts +239 -0
  25. package/src/backends/github.ts +194 -0
  26. package/src/backends/http.ts +122 -0
  27. package/src/backends/index.ts +39 -0
  28. package/src/backends/local.ts +47 -0
  29. package/src/backends/s3.ts +157 -0
  30. package/src/backends/types.ts +59 -0
  31. package/src/base-cache.ts +270 -0
  32. package/src/browse-ui.ts +3754 -0
  33. package/src/browse.ts +1038 -0
  34. package/src/build.ts +1108 -0
  35. package/src/catalog.ts +204 -0
  36. package/src/cli-deprecated.ts +29 -0
  37. package/src/cli.ts +773 -0
  38. package/src/collection-admin.ts +287 -0
  39. package/src/collection-builder.ts +464 -0
  40. package/src/collections.ts +116 -0
  41. package/src/compatibility.ts +105 -0
  42. package/src/config.ts +743 -0
  43. package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
  44. package/src/eval.ts +1169 -0
  45. package/src/file-writer.ts +61 -0
  46. package/src/format-registry.ts +141 -0
  47. package/src/guild/auto-updater.ts +163 -0
  48. package/src/guild/backend-resolver.ts +49 -0
  49. package/src/guild/cli.ts +592 -0
  50. package/src/guild/collection-expander.ts +47 -0
  51. package/src/guild/global-cache.ts +247 -0
  52. package/src/guild/hook-generator.ts +100 -0
  53. package/src/guild/manifest.ts +154 -0
  54. package/src/guild/path-utils.ts +12 -0
  55. package/src/guild/sync.ts +622 -0
  56. package/src/guild/version-resolver.ts +42 -0
  57. package/src/help/metadata.ts +445 -0
  58. package/src/help/renderer.ts +265 -0
  59. package/src/help/typo-suggester.ts +25 -0
  60. package/src/hooks/expression.ts +493 -0
  61. package/src/hooks/pipeline.ts +141 -0
  62. package/src/import.ts +773 -0
  63. package/src/importers/claude-code.ts +134 -0
  64. package/src/importers/cline.ts +103 -0
  65. package/src/importers/codex.ts +140 -0
  66. package/src/importers/copilot.ts +103 -0
  67. package/src/importers/cursor.ts +105 -0
  68. package/src/importers/index.ts +390 -0
  69. package/src/importers/kiro.ts +110 -0
  70. package/src/importers/qdeveloper.ts +103 -0
  71. package/src/importers/types.ts +54 -0
  72. package/src/importers/windsurf.ts +104 -0
  73. package/src/install.ts +1005 -0
  74. package/src/manifest-admin.ts +306 -0
  75. package/src/mcp-bridge.ts +240 -0
  76. package/src/mutation/delta.ts +50 -0
  77. package/src/mutation/history.ts +66 -0
  78. package/src/mutation/operators.ts +524 -0
  79. package/src/mutation/runner.ts +332 -0
  80. package/src/new.ts +106 -0
  81. package/src/outcomes/collision.ts +127 -0
  82. package/src/outcomes/normalize.ts +208 -0
  83. package/src/outcomes/registry.ts +173 -0
  84. package/src/parser.ts +446 -0
  85. package/src/provenance-backfill-cli.ts +319 -0
  86. package/src/provenance-backfill.ts +520 -0
  87. package/src/publish.ts +354 -0
  88. package/src/reconcile-orchestrator.ts +502 -0
  89. package/src/reconcile-report-renderer.ts +176 -0
  90. package/src/resolve-body.ts +15 -0
  91. package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
  92. package/src/rosetta/builtins/contracts.ts +1033 -0
  93. package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
  94. package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
  95. package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
  96. package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
  97. package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
  98. package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
  99. package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
  100. package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
  101. package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
  102. package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
  103. package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
  104. package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
  105. package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
  106. package/src/rosetta/builtins/sources/cline-native.ts +176 -0
  107. package/src/rosetta/builtins/sources/codex-native.ts +343 -0
  108. package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
  109. package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
  110. package/src/rosetta/builtins/sources/index.ts +95 -0
  111. package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
  112. package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
  113. package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
  114. package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
  115. package/src/rosetta/builtins/sources/superpowers.ts +240 -0
  116. package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
  117. package/src/rosetta/builtins/targets/claude-code.ts +181 -0
  118. package/src/rosetta/builtins/targets/cline.ts +87 -0
  119. package/src/rosetta/builtins/targets/codex.ts +226 -0
  120. package/src/rosetta/builtins/targets/copilot.ts +103 -0
  121. package/src/rosetta/builtins/targets/cursor.ts +87 -0
  122. package/src/rosetta/builtins/targets/index.ts +60 -0
  123. package/src/rosetta/builtins/targets/kiro.ts +278 -0
  124. package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
  125. package/src/rosetta/builtins/targets/windsurf.ts +87 -0
  126. package/src/rosetta/canonical.ts +729 -0
  127. package/src/rosetta/compatibility.ts +432 -0
  128. package/src/rosetta/contracts.ts +329 -0
  129. package/src/rosetta/detector.ts +724 -0
  130. package/src/rosetta/diagnostics.ts +630 -0
  131. package/src/rosetta/engine-bootstrap.ts +103 -0
  132. package/src/rosetta/engine.ts +744 -0
  133. package/src/rosetta/index.ts +381 -0
  134. package/src/rosetta/inspection.ts +530 -0
  135. package/src/rosetta/plan.ts +448 -0
  136. package/src/rosetta/provenance-digest.ts +369 -0
  137. package/src/rosetta/reconcile.ts +812 -0
  138. package/src/rosetta/redaction.ts +467 -0
  139. package/src/rosetta/registry.ts +712 -0
  140. package/src/rosetta/renderers.ts +571 -0
  141. package/src/rosetta/request-guard.ts +335 -0
  142. package/src/rosetta/resolution.ts +419 -0
  143. package/src/rosetta/source-accounting.ts +233 -0
  144. package/src/rosetta/templates.ts +129 -0
  145. package/src/rosetta-cli.ts +717 -0
  146. package/src/rosetta-docs-generator.ts +793 -0
  147. package/src/rosetta-profiles-cli.ts +367 -0
  148. package/src/schemas.ts +1712 -0
  149. package/src/spec-coordination.ts +1141 -0
  150. package/src/temper.ts +747 -0
  151. package/src/template-bundle-loader.ts +312 -0
  152. package/src/template-engine.ts +53 -0
  153. package/src/translation-application-policy.ts +496 -0
  154. package/src/translation-orchestrator.ts +1013 -0
  155. package/src/translation-plan-applier.ts +473 -0
  156. package/src/tutorial.ts +305 -0
  157. package/src/validate.ts +1093 -0
  158. package/src/versioning.ts +553 -0
  159. package/src/wizard.ts +660 -0
  160. package/src/workspace.ts +237 -0
  161. package/templates/eval-contexts/claude-code.md.njk +6 -0
  162. package/templates/eval-contexts/cline.md.njk +6 -0
  163. package/templates/eval-contexts/copilot.md.njk +6 -0
  164. package/templates/eval-contexts/cursor.md.njk +6 -0
  165. package/templates/eval-contexts/kiro.md.njk +10 -0
  166. package/templates/eval-contexts/qdeveloper.md.njk +6 -0
  167. package/templates/eval-contexts/windsurf.md.njk +6 -0
  168. package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
  169. package/templates/harness-adapters/_base/base.md.njk +16 -0
  170. package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
  171. package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
  172. package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
  173. package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
  174. package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
  175. package/templates/harness-adapters/cline/hook.sh.njk +4 -0
  176. package/templates/harness-adapters/cline/mcp.json.njk +1 -0
  177. package/templates/harness-adapters/cline/rule.md.njk +1 -0
  178. package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
  179. package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
  180. package/templates/harness-adapters/codex/skill.md.njk +27 -0
  181. package/templates/harness-adapters/copilot/agents.md.njk +1 -0
  182. package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
  183. package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
  184. package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
  185. package/templates/harness-adapters/cursor/rule.md.njk +6 -0
  186. package/templates/harness-adapters/kiro/hook.json.njk +1 -0
  187. package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
  188. package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
  189. package/templates/harness-adapters/kiro/power.md.njk +12 -0
  190. package/templates/harness-adapters/kiro/steering.md.njk +16 -0
  191. package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
  192. package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
  193. package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
  194. package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
  195. package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
  196. package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
  197. package/templates/knowledge/hooks.yaml.njk +4 -0
  198. package/templates/knowledge/knowledge.md.njk +53 -0
  199. 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
+ }