@smeltjs/core 0.2.0 → 0.4.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 (348) hide show
  1. package/README.md +3 -3
  2. package/dist/agents/guide.d.ts +68 -0
  3. package/dist/agents/guide.d.ts.map +1 -0
  4. package/dist/agents/guide.js +72 -0
  5. package/dist/agents/guide.js.map +1 -0
  6. package/dist/agents/instructions.d.ts +133 -0
  7. package/dist/agents/instructions.d.ts.map +1 -0
  8. package/dist/agents/instructions.js +212 -0
  9. package/dist/agents/instructions.js.map +1 -0
  10. package/dist/agents/lint.d.ts +154 -0
  11. package/dist/agents/lint.d.ts.map +1 -0
  12. package/dist/agents/lint.js +761 -0
  13. package/dist/agents/lint.js.map +1 -0
  14. package/dist/agents/split.d.ts +113 -0
  15. package/dist/agents/split.d.ts.map +1 -0
  16. package/dist/agents/split.js +234 -0
  17. package/dist/agents/split.js.map +1 -0
  18. package/dist/apply.d.ts.map +1 -1
  19. package/dist/apply.js.map +1 -1
  20. package/dist/cache/prefix.d.ts.map +1 -1
  21. package/dist/cache/prefix.js.map +1 -1
  22. package/dist/cli/agents.d.ts +39 -0
  23. package/dist/cli/agents.d.ts.map +1 -0
  24. package/dist/cli/agents.js +121 -0
  25. package/dist/cli/agents.js.map +1 -0
  26. package/dist/cli/args.d.ts +17 -70
  27. package/dist/cli/args.d.ts.map +1 -1
  28. package/dist/cli/args.js +18 -338
  29. package/dist/cli/args.js.map +1 -1
  30. package/dist/cli/bin.d.ts.map +1 -1
  31. package/dist/cli/bin.js.map +1 -1
  32. package/dist/cli/config.d.ts +60 -0
  33. package/dist/cli/config.d.ts.map +1 -1
  34. package/dist/cli/config.js +89 -2
  35. package/dist/cli/config.js.map +1 -1
  36. package/dist/cli/hooks.d.ts +55 -51
  37. package/dist/cli/hooks.d.ts.map +1 -1
  38. package/dist/cli/hooks.js +206 -658
  39. package/dist/cli/hooks.js.map +1 -1
  40. package/dist/cli/init.d.ts +30 -15
  41. package/dist/cli/init.d.ts.map +1 -1
  42. package/dist/cli/init.js +167 -38
  43. package/dist/cli/init.js.map +1 -1
  44. package/dist/cli/report.d.ts +23 -0
  45. package/dist/cli/report.d.ts.map +1 -1
  46. package/dist/cli/report.js +142 -4
  47. package/dist/cli/report.js.map +1 -1
  48. package/dist/cli/run.d.ts +29 -90
  49. package/dist/cli/run.d.ts.map +1 -1
  50. package/dist/cli/run.js +34 -342
  51. package/dist/cli/run.js.map +1 -1
  52. package/dist/cli/shell.d.ts +106 -0
  53. package/dist/cli/shell.d.ts.map +1 -0
  54. package/dist/cli/shell.js +100 -0
  55. package/dist/cli/shell.js.map +1 -0
  56. package/dist/cli/subcommands/agents.d.ts +72 -0
  57. package/dist/cli/subcommands/agents.d.ts.map +1 -0
  58. package/dist/cli/subcommands/agents.js +162 -0
  59. package/dist/cli/subcommands/agents.js.map +1 -0
  60. package/dist/cli/subcommands/flags.d.ts +144 -0
  61. package/dist/cli/subcommands/flags.d.ts.map +1 -0
  62. package/dist/cli/subcommands/flags.js +220 -0
  63. package/dist/cli/subcommands/flags.js.map +1 -0
  64. package/dist/cli/subcommands/hooks.d.ts +18 -0
  65. package/dist/cli/subcommands/hooks.d.ts.map +1 -0
  66. package/dist/cli/subcommands/hooks.js +80 -0
  67. package/dist/cli/subcommands/hooks.js.map +1 -0
  68. package/dist/cli/subcommands/init.d.ts +19 -0
  69. package/dist/cli/subcommands/init.d.ts.map +1 -0
  70. package/dist/cli/subcommands/init.js +44 -0
  71. package/dist/cli/subcommands/init.js.map +1 -0
  72. package/dist/cli/subcommands/map.d.ts +75 -0
  73. package/dist/cli/subcommands/map.d.ts.map +1 -0
  74. package/dist/cli/subcommands/map.js +132 -0
  75. package/dist/cli/subcommands/map.js.map +1 -0
  76. package/dist/cli/subcommands/registry.d.ts +81 -0
  77. package/dist/cli/subcommands/registry.d.ts.map +1 -0
  78. package/dist/cli/subcommands/registry.js +141 -0
  79. package/dist/cli/subcommands/registry.js.map +1 -0
  80. package/dist/cli/subcommands/retrieve.d.ts +74 -0
  81. package/dist/cli/subcommands/retrieve.d.ts.map +1 -0
  82. package/dist/cli/subcommands/retrieve.js +84 -0
  83. package/dist/cli/subcommands/retrieve.js.map +1 -0
  84. package/dist/cli/subcommands/smelt.d.ts +107 -0
  85. package/dist/cli/subcommands/smelt.d.ts.map +1 -0
  86. package/dist/cli/subcommands/smelt.js +304 -0
  87. package/dist/cli/subcommands/smelt.js.map +1 -0
  88. package/dist/cli/subcommands/stats.d.ts +43 -0
  89. package/dist/cli/subcommands/stats.d.ts.map +1 -0
  90. package/dist/cli/subcommands/stats.js +62 -0
  91. package/dist/cli/subcommands/stats.js.map +1 -0
  92. package/dist/cli/subcommands/subcommand.d.ts +113 -0
  93. package/dist/cli/subcommands/subcommand.d.ts.map +1 -0
  94. package/dist/cli/subcommands/subcommand.js +3 -0
  95. package/dist/cli/subcommands/subcommand.js.map +1 -0
  96. package/dist/cli/usage.d.ts +3 -0
  97. package/dist/cli/usage.d.ts.map +1 -0
  98. package/dist/cli/usage.js +95 -0
  99. package/dist/cli/usage.js.map +1 -0
  100. package/dist/detect.d.ts.map +1 -1
  101. package/dist/detect.js.map +1 -1
  102. package/dist/errors.d.ts +21 -1
  103. package/dist/errors.d.ts.map +1 -1
  104. package/dist/errors.js +33 -1
  105. package/dist/errors.js.map +1 -1
  106. package/dist/harness/aider.d.ts +10 -0
  107. package/dist/harness/aider.d.ts.map +1 -0
  108. package/dist/harness/aider.js +21 -0
  109. package/dist/harness/aider.js.map +1 -0
  110. package/dist/harness/claude-code.d.ts +3 -0
  111. package/dist/harness/claude-code.d.ts.map +1 -0
  112. package/dist/harness/claude-code.js +68 -0
  113. package/dist/harness/claude-code.js.map +1 -0
  114. package/dist/harness/cline.d.ts +3 -0
  115. package/dist/harness/cline.d.ts.map +1 -0
  116. package/dist/harness/cline.js +54 -0
  117. package/dist/harness/cline.js.map +1 -0
  118. package/dist/harness/codex.d.ts +3 -0
  119. package/dist/harness/codex.d.ts.map +1 -0
  120. package/dist/harness/codex.js +92 -0
  121. package/dist/harness/codex.js.map +1 -0
  122. package/dist/harness/cursor.d.ts +3 -0
  123. package/dist/harness/cursor.d.ts.map +1 -0
  124. package/dist/harness/cursor.js +54 -0
  125. package/dist/harness/cursor.js.map +1 -0
  126. package/dist/harness/gemini.d.ts +3 -0
  127. package/dist/harness/gemini.d.ts.map +1 -0
  128. package/dist/harness/gemini.js +60 -0
  129. package/dist/harness/gemini.js.map +1 -0
  130. package/dist/harness/grok.d.ts +3 -0
  131. package/dist/harness/grok.d.ts.map +1 -0
  132. package/dist/harness/grok.js +47 -0
  133. package/dist/harness/grok.js.map +1 -0
  134. package/dist/harness/hermes.d.ts +3 -0
  135. package/dist/harness/hermes.d.ts.map +1 -0
  136. package/dist/harness/hermes.js +72 -0
  137. package/dist/harness/hermes.js.map +1 -0
  138. package/dist/harness/kilocode.d.ts +3 -0
  139. package/dist/harness/kilocode.d.ts.map +1 -0
  140. package/dist/harness/kilocode.js +35 -0
  141. package/dist/harness/kilocode.js.map +1 -0
  142. package/dist/harness/opencode.d.ts +3 -0
  143. package/dist/harness/opencode.d.ts.map +1 -0
  144. package/dist/harness/opencode.js +97 -0
  145. package/dist/harness/opencode.js.map +1 -0
  146. package/dist/harness/paths.d.ts +17 -0
  147. package/dist/harness/paths.d.ts.map +1 -0
  148. package/dist/harness/paths.js +45 -0
  149. package/dist/harness/paths.js.map +1 -0
  150. package/dist/harness/profile.d.ts +209 -0
  151. package/dist/harness/profile.d.ts.map +1 -0
  152. package/dist/harness/profile.js +41 -0
  153. package/dist/harness/profile.js.map +1 -0
  154. package/dist/harness/registry.d.ts +100 -0
  155. package/dist/harness/registry.d.ts.map +1 -0
  156. package/dist/harness/registry.js +144 -0
  157. package/dist/harness/registry.js.map +1 -0
  158. package/dist/harness/snippet.d.ts +19 -0
  159. package/dist/harness/snippet.d.ts.map +1 -0
  160. package/dist/harness/snippet.js +41 -0
  161. package/dist/harness/snippet.js.map +1 -0
  162. package/dist/hash.d.ts.map +1 -1
  163. package/dist/hash.js.map +1 -1
  164. package/dist/hooks/guard-core.d.ts +14 -14
  165. package/dist/hooks/guard-core.d.ts.map +1 -1
  166. package/dist/hooks/guard-core.js +15 -37
  167. package/dist/hooks/guard-core.js.map +1 -1
  168. package/dist/hooks/shim.d.ts +95 -4
  169. package/dist/hooks/shim.d.ts.map +1 -1
  170. package/dist/hooks/shim.js +87 -2
  171. package/dist/hooks/shim.js.map +1 -1
  172. package/dist/hooks/shims/claude-code.d.ts +5 -18
  173. package/dist/hooks/shims/claude-code.d.ts.map +1 -1
  174. package/dist/hooks/shims/claude-code.js +9 -56
  175. package/dist/hooks/shims/claude-code.js.map +1 -1
  176. package/dist/hooks/shims/cline.d.ts +5 -12
  177. package/dist/hooks/shims/cline.d.ts.map +1 -1
  178. package/dist/hooks/shims/cline.js +9 -34
  179. package/dist/hooks/shims/cline.js.map +1 -1
  180. package/dist/hooks/shims/codex.d.ts +5 -18
  181. package/dist/hooks/shims/codex.d.ts.map +1 -1
  182. package/dist/hooks/shims/codex.js +9 -51
  183. package/dist/hooks/shims/codex.js.map +1 -1
  184. package/dist/hooks/shims/cursor.d.ts +5 -14
  185. package/dist/hooks/shims/cursor.d.ts.map +1 -1
  186. package/dist/hooks/shims/cursor.js +9 -42
  187. package/dist/hooks/shims/cursor.js.map +1 -1
  188. package/dist/hooks/shims/gemini.d.ts +5 -18
  189. package/dist/hooks/shims/gemini.d.ts.map +1 -1
  190. package/dist/hooks/shims/gemini.js +9 -48
  191. package/dist/hooks/shims/gemini.js.map +1 -1
  192. package/dist/hooks/shims/grok.d.ts +5 -13
  193. package/dist/hooks/shims/grok.d.ts.map +1 -1
  194. package/dist/hooks/shims/grok.js +9 -32
  195. package/dist/hooks/shims/grok.js.map +1 -1
  196. package/dist/hooks/shims/hermes.d.ts +5 -17
  197. package/dist/hooks/shims/hermes.d.ts.map +1 -1
  198. package/dist/hooks/shims/hermes.js +9 -45
  199. package/dist/hooks/shims/hermes.js.map +1 -1
  200. package/dist/index.d.ts +43 -76
  201. package/dist/index.d.ts.map +1 -1
  202. package/dist/index.js +21 -67
  203. package/dist/index.js.map +1 -1
  204. package/dist/lang/bash.d.ts.map +1 -1
  205. package/dist/lang/bash.js.map +1 -1
  206. package/dist/lang/c.d.ts.map +1 -1
  207. package/dist/lang/c.js.map +1 -1
  208. package/dist/lang/c_sharp.d.ts.map +1 -1
  209. package/dist/lang/c_sharp.js.map +1 -1
  210. package/dist/lang/cpp.d.ts.map +1 -1
  211. package/dist/lang/cpp.js.map +1 -1
  212. package/dist/lang/go.d.ts.map +1 -1
  213. package/dist/lang/go.js.map +1 -1
  214. package/dist/lang/java.d.ts.map +1 -1
  215. package/dist/lang/java.js.map +1 -1
  216. package/dist/lang/javascript.d.ts.map +1 -1
  217. package/dist/lang/javascript.js.map +1 -1
  218. package/dist/lang/kotlin.d.ts.map +1 -1
  219. package/dist/lang/kotlin.js.map +1 -1
  220. package/dist/lang/php.d.ts.map +1 -1
  221. package/dist/lang/php.js.map +1 -1
  222. package/dist/lang/profile.d.ts.map +1 -1
  223. package/dist/lang/profile.js.map +1 -1
  224. package/dist/lang/python.d.ts.map +1 -1
  225. package/dist/lang/python.js.map +1 -1
  226. package/dist/lang/registry.d.ts.map +1 -1
  227. package/dist/lang/registry.js.map +1 -1
  228. package/dist/lang/ruby.d.ts.map +1 -1
  229. package/dist/lang/ruby.js.map +1 -1
  230. package/dist/lang/rust.d.ts.map +1 -1
  231. package/dist/lang/rust.js.map +1 -1
  232. package/dist/lang/swift.d.ts.map +1 -1
  233. package/dist/lang/swift.js.map +1 -1
  234. package/dist/lang/tsx.d.ts.map +1 -1
  235. package/dist/lang/tsx.js.map +1 -1
  236. package/dist/lang/typescript.d.ts.map +1 -1
  237. package/dist/lang/typescript.js.map +1 -1
  238. package/dist/net/policy.d.ts +18 -1
  239. package/dist/net/policy.d.ts.map +1 -1
  240. package/dist/net/policy.js +6 -4
  241. package/dist/net/policy.js.map +1 -1
  242. package/dist/ops/index.d.ts +39 -0
  243. package/dist/ops/index.d.ts.map +1 -0
  244. package/dist/ops/index.js +37 -0
  245. package/dist/ops/index.js.map +1 -0
  246. package/dist/ops/inputs.d.ts +170 -0
  247. package/dist/ops/inputs.d.ts.map +1 -0
  248. package/dist/ops/inputs.js +142 -0
  249. package/dist/ops/inputs.js.map +1 -0
  250. package/dist/ops/verbs.d.ts +136 -0
  251. package/dist/ops/verbs.d.ts.map +1 -0
  252. package/dist/ops/verbs.js +76 -0
  253. package/dist/ops/verbs.js.map +1 -0
  254. package/dist/plan/auto.d.ts +69 -0
  255. package/dist/plan/auto.d.ts.map +1 -0
  256. package/dist/plan/auto.js +70 -0
  257. package/dist/plan/auto.js.map +1 -0
  258. package/dist/plan/budget.d.ts +21 -0
  259. package/dist/plan/budget.d.ts.map +1 -0
  260. package/dist/plan/budget.js +26 -0
  261. package/dist/plan/budget.js.map +1 -0
  262. package/dist/plan/grammar.d.ts +14 -0
  263. package/dist/plan/grammar.d.ts.map +1 -1
  264. package/dist/plan/grammar.js +47 -6
  265. package/dist/plan/grammar.js.map +1 -1
  266. package/dist/plan/lexical.d.ts.map +1 -1
  267. package/dist/plan/lexical.js +1 -7
  268. package/dist/plan/lexical.js.map +1 -1
  269. package/dist/plan/planners.d.ts +35 -2
  270. package/dist/plan/planners.d.ts.map +1 -1
  271. package/dist/plan/planners.js +35 -2
  272. package/dist/plan/planners.js.map +1 -1
  273. package/dist/plan/structural.d.ts +19 -5
  274. package/dist/plan/structural.d.ts.map +1 -1
  275. package/dist/plan/structural.js +180 -33
  276. package/dist/plan/structural.js.map +1 -1
  277. package/dist/repomap/cache.d.ts +48 -0
  278. package/dist/repomap/cache.d.ts.map +1 -1
  279. package/dist/repomap/cache.js +112 -13
  280. package/dist/repomap/cache.js.map +1 -1
  281. package/dist/repomap/io.d.ts +24 -0
  282. package/dist/repomap/io.d.ts.map +1 -0
  283. package/dist/repomap/io.js +34 -0
  284. package/dist/repomap/io.js.map +1 -0
  285. package/dist/repomap/map.d.ts +69 -6
  286. package/dist/repomap/map.d.ts.map +1 -1
  287. package/dist/repomap/map.js +104 -22
  288. package/dist/repomap/map.js.map +1 -1
  289. package/dist/repomap/rank.d.ts +26 -2
  290. package/dist/repomap/rank.d.ts.map +1 -1
  291. package/dist/repomap/rank.js +20 -0
  292. package/dist/repomap/rank.js.map +1 -1
  293. package/dist/repomap/reader.d.ts +73 -0
  294. package/dist/repomap/reader.d.ts.map +1 -0
  295. package/dist/repomap/reader.js +30 -0
  296. package/dist/repomap/reader.js.map +1 -0
  297. package/dist/repomap/tags.d.ts.map +1 -1
  298. package/dist/repomap/tags.js.map +1 -1
  299. package/dist/retrieve.d.ts +8 -0
  300. package/dist/retrieve.d.ts.map +1 -1
  301. package/dist/retrieve.js +12 -1
  302. package/dist/retrieve.js.map +1 -1
  303. package/dist/smelter.d.ts +79 -0
  304. package/dist/smelter.d.ts.map +1 -0
  305. package/dist/smelter.js +65 -0
  306. package/dist/smelter.js.map +1 -0
  307. package/dist/stages.d.ts.map +1 -1
  308. package/dist/stages.js +2 -2
  309. package/dist/stages.js.map +1 -1
  310. package/dist/stats.d.ts +2 -1
  311. package/dist/stats.d.ts.map +1 -1
  312. package/dist/stats.js +2 -1
  313. package/dist/stats.js.map +1 -1
  314. package/dist/store-dir.d.ts +43 -4
  315. package/dist/store-dir.d.ts.map +1 -1
  316. package/dist/store-dir.js +48 -5
  317. package/dist/store-dir.js.map +1 -1
  318. package/dist/store.d.ts +6 -0
  319. package/dist/store.d.ts.map +1 -1
  320. package/dist/store.js +6 -0
  321. package/dist/store.js.map +1 -1
  322. package/dist/text/json-edit.d.ts +71 -0
  323. package/dist/text/json-edit.d.ts.map +1 -0
  324. package/dist/text/json-edit.js +219 -0
  325. package/dist/text/json-edit.js.map +1 -0
  326. package/dist/types.d.ts +35 -14
  327. package/dist/types.d.ts.map +1 -1
  328. package/dist/types.js.map +1 -1
  329. package/grammars/tree-sitter-bash.wasm +0 -0
  330. package/grammars/tree-sitter-c.wasm +0 -0
  331. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  332. package/grammars/tree-sitter-cpp.wasm +0 -0
  333. package/grammars/tree-sitter-go.wasm +0 -0
  334. package/grammars/tree-sitter-java.wasm +0 -0
  335. package/grammars/tree-sitter-javascript.wasm +0 -0
  336. package/grammars/tree-sitter-kotlin.wasm +0 -0
  337. package/grammars/tree-sitter-php.wasm +0 -0
  338. package/grammars/tree-sitter-python.wasm +0 -0
  339. package/grammars/tree-sitter-ruby.wasm +0 -0
  340. package/grammars/tree-sitter-rust.wasm +0 -0
  341. package/grammars/tree-sitter-swift.wasm +0 -0
  342. package/grammars/tree-sitter-tsx.wasm +0 -0
  343. package/grammars/tree-sitter-typescript.wasm +0 -0
  344. package/package.json +11 -11
  345. package/dist/cli/resolve.d.ts +0 -103
  346. package/dist/cli/resolve.d.ts.map +0 -1
  347. package/dist/cli/resolve.js +0 -106
  348. package/dist/cli/resolve.js.map +0 -1
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What a plan will cost once its markers land — the arithmetic both planners do.
3
+ *
4
+ * Two planners now read `budgetBytes`, and both answer the same question with it:
5
+ * *given these elisions, how big is the output?* The lexical planner asks it to pick a
6
+ * ladder rung; the structural planner asks it to decide whether its budget rung is
7
+ * needed at all. Written twice it would be two answers to one question, which is how
8
+ * one planner ends up believing a marker costs something the other does not — the
9
+ * exact fork the {@link MarkerPricing} seam exists to prevent, one level up.
10
+ *
11
+ * Nothing here estimates. Every byte comes from `pricing.costBytes`, the seam
12
+ * `apply.ts` builds from the exact builder `applyPlan` will use.
13
+ */
14
+ /** The exact UTF-8 cost of the marker this elision would earn. Asked, not guessed. */
15
+ export function markerBytes(elision, pricing) {
16
+ return pricing.costBytes(elision.reason, elision.range.end - elision.range.start);
17
+ }
18
+ /** What one elision saves: the bytes it removes, less the marker that replaces them. */
19
+ export function savingBytes(elision, pricing) {
20
+ return elision.range.end - elision.range.start - markerBytes(elision, pricing);
21
+ }
22
+ /** The output size these elisions predict, markers included. */
23
+ export function predictOutputBytes(inputBytes, elisions, pricing) {
24
+ return elisions.reduce((bytes, elision) => bytes - savingBytes(elision, pricing), inputBytes);
25
+ }
26
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/plan/budget.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,sFAAsF;AACtF,MAAM,UAAU,WAAW,CAAC,OAAuB,EAAE,OAAsB;IACzE,OAAO,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACpF,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,WAAW,CAAC,OAAuB,EAAE,OAAsB;IACzE,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AACjF,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,kBAAkB,CAChC,UAAkB,EAClB,QAAmC,EACnC,OAAsB;IAEtB,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,KAAK,GAAG,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC;AAChG,CAAC","sourcesContent":["import type { MarkerPricing, PlannedElision } from '../types.ts';\n\n/**\n * What a plan will cost once its markers land — the arithmetic both planners do.\n *\n * Two planners now read `budgetBytes`, and both answer the same question with it:\n * *given these elisions, how big is the output?* The lexical planner asks it to pick a\n * ladder rung; the structural planner asks it to decide whether its budget rung is\n * needed at all. Written twice it would be two answers to one question, which is how\n * one planner ends up believing a marker costs something the other does not — the\n * exact fork the {@link MarkerPricing} seam exists to prevent, one level up.\n *\n * Nothing here estimates. Every byte comes from `pricing.costBytes`, the seam\n * `apply.ts` builds from the exact builder `applyPlan` will use.\n */\n\n/** The exact UTF-8 cost of the marker this elision would earn. Asked, not guessed. */\nexport function markerBytes(elision: PlannedElision, pricing: MarkerPricing): number {\n return pricing.costBytes(elision.reason, elision.range.end - elision.range.start);\n}\n\n/** What one elision saves: the bytes it removes, less the marker that replaces them. */\nexport function savingBytes(elision: PlannedElision, pricing: MarkerPricing): number {\n return elision.range.end - elision.range.start - markerBytes(elision, pricing);\n}\n\n/** The output size these elisions predict, markers included. */\nexport function predictOutputBytes(\n inputBytes: number,\n elisions: readonly PlannedElision[],\n pricing: MarkerPricing,\n): number {\n return elisions.reduce((bytes, elision) => bytes - savingBytes(elision, pricing), inputBytes);\n}\n"]}
@@ -31,6 +31,20 @@ export declare function grammarPath(language: LanguageId): string;
31
31
  * ordinary happy path. Reading the file ourselves removes that capability instead of
32
32
  * documenting it, and {@link assertLocalResource} rejects a remote path before we get
33
33
  * that far.
34
+ *
35
+ * **Every failure here is a `GrammarUnavailableError`.** The consumer contract makes
36
+ * exactly one promise about errors — every error smelt throws is an `instanceof
37
+ * SmeltError` — and this function is on the path of both `smelt()` and
38
+ * `buildRepoMap()`, for every file in a language smelt claims to parse. Only
39
+ * *resolution* used to be inside the contract: `grammarPath` throws for a grammar it
40
+ * cannot find, and {@link existsSync} then reports a file's **presence**, never its
41
+ * readability. So a grammar that resolved and then would not load leaked the raw
42
+ * error — `EACCES` from an unreadable `.wasm`, a V8 `CompileError` or `RangeError`
43
+ * from a truncated one, a half-extracted tarball — straight past a caller's
44
+ * documented `catch`. Each step is wrapped instead, naming the language and the path
45
+ * and keeping the original as `cause`: bringing the failure inside the contract, not
46
+ * hiding what Node or V8 said. A promise with one undocumented exception is no
47
+ * promise at all.
34
48
  */
35
49
  export declare function loadGrammar(language: LanguageId): Promise<Language>;
36
50
  /** Reset the grammar cache. Tests use it; production has no reason to. */
@@ -1 +1 @@
1
- {"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/plan/grammar.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAU,MAAM,iBAAiB,CAAC;AAKnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAEnC,CAAC;AAkBhC;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAexD;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAYzE;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC"}
1
+ {"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/plan/grammar.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAU,MAAM,iBAAiB,CAAC;AAKnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAEnC,CAAC;AAkBhC;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAexD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,WAAW,CAAC,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqBzE;AA4BD,0EAA0E;AAC1E,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC","sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { fileURLToPath } from 'node:url';\n\nimport { Language, Parser } from 'web-tree-sitter';\n\nimport { GrammarUnavailableError, SmeltError } from '../errors.ts';\nimport { LANGUAGE_PROFILES } from '../lang/registry.ts';\nimport { assertLocalResource } from '../net/policy.ts';\nimport type { LanguageId } from '../types.ts';\n\n/**\n * Grammar file for each language smelt claims to parse — the registry's `wasm`\n * facts, as a map. Derived from `LANGUAGE_PROFILES` (which is\n * `Record<LanguageId, LanguageProfile>`, so adding a `LanguageId` without a profile —\n * and its grammar — is a type error and the two cannot drift). It stays exported\n * because `scripts/bundle-grammars.mjs` and the attribution generator both read it —\n * a hand-written second list of grammar filenames would be exactly the drift the\n * registry exists to prevent.\n */\nexport const WASM_BY_LANGUAGE: Readonly<Record<LanguageId, string>> = Object.fromEntries(\n Object.values(LANGUAGE_PROFILES).map((profile) => [profile.id, profile.wasm]),\n) as Record<LanguageId, string>;\n\n/**\n * Where the bundled grammars live, relative to this module.\n *\n * From `dist/plan/grammar.js` and from `src/plan/grammar.ts` alike, `../../grammars/`\n * is this package's own `grammars/` directory — filled by `pnpm build` and shipped\n * inside the npm tarball. That is what makes \"zero native compilation, works offline\"\n * true rather than aspirational: whoever installs the package has the parsers, with no\n * post-install download and no optional peer dependency to remember. It is also\n * redistribution, which is why `THIRD-PARTY.md` exists and is generated.\n */\nconst BUNDLED_GRAMMAR_DIR = new URL('../../grammars/', import.meta.url);\n\nconst require = createRequire(import.meta.url);\nconst cache = new Map<LanguageId, Language>();\nlet runtimeReady: Promise<void> | undefined;\n\n/**\n * Resolve a grammar to a path on this machine.\n *\n * The copy bundled in this package wins; `tree-sitter-wasms` is the fallback, for a\n * source checkout that has not run `pnpm build` yet. Note what this function does *not*\n * do: it never constructs a URL from a version string, a CDN base, or anything else.\n * Grammars come off disk — either the ones shipped here or the ones a package manager\n * already installed. A \"fetch the grammar on first use\" cache is the most natural way\n * to break Law 1 without noticing, because it works perfectly on the machine that\n * wrote it.\n */\nexport function grammarPath(language: LanguageId): string {\n const file = WASM_BY_LANGUAGE[language];\n\n const bundled = fileURLToPath(new URL(file, BUNDLED_GRAMMAR_DIR));\n if (existsSync(bundled)) return bundled;\n\n try {\n return require.resolve(`tree-sitter-wasms/out/${file}`);\n } catch {\n throw new GrammarUnavailableError(\n `smelt: no grammar for \"${language}\". The bundled copy is missing (run ` +\n `\\`pnpm build\\` in a source checkout) and \\`tree-sitter-wasms\\` is not installed ` +\n `either. Pass \\`language: 'unknown'\\` to use the lexical planner.`,\n );\n }\n}\n\n/**\n * Load a grammar, from disk, once.\n *\n * The bytes are read here and handed to tree-sitter as a `Uint8Array` rather than\n * passing it a path. `Language.load()` accepts `string | URL`, and a `URL` with an\n * `https:` scheme would make it fetch — inside the elision path, from a dependency's\n * ordinary happy path. Reading the file ourselves removes that capability instead of\n * documenting it, and {@link assertLocalResource} rejects a remote path before we get\n * that far.\n *\n * **Every failure here is a `GrammarUnavailableError`.** The consumer contract makes\n * exactly one promise about errors — every error smelt throws is an `instanceof\n * SmeltError` — and this function is on the path of both `smelt()` and\n * `buildRepoMap()`, for every file in a language smelt claims to parse. Only\n * *resolution* used to be inside the contract: `grammarPath` throws for a grammar it\n * cannot find, and {@link existsSync} then reports a file's **presence**, never its\n * readability. So a grammar that resolved and then would not load leaked the raw\n * error — `EACCES` from an unreadable `.wasm`, a V8 `CompileError` or `RangeError`\n * from a truncated one, a half-extracted tarball — straight past a caller's\n * documented `catch`. Each step is wrapped instead, naming the language and the path\n * and keeping the original as `cause`: bringing the failure inside the contract, not\n * hiding what Node or V8 said. A promise with one undocumented exception is no\n * promise at all.\n */\nexport async function loadGrammar(language: LanguageId): Promise<Language> {\n const cached = cache.get(language);\n if (cached !== undefined) return cached;\n\n const path = fileURLToPath(assertLocalResource(grammarPath(language)).href);\n\n const ready = (runtimeReady ??= Parser.init());\n await inContract(\n () => ready,\n `smelt: the tree-sitter WASM runtime would not start, so no grammar can be loaded`,\n );\n const bytes = await inContract(\n () => readFile(path),\n `smelt: the grammar for \"${language}\" could not be read from \"${path}\"`,\n );\n const grammar = await inContract(\n () => Language.load(new Uint8Array(bytes)),\n `smelt: the file at \"${path}\" is not a loadable tree-sitter grammar for \"${language}\"`,\n );\n cache.set(language, grammar);\n return grammar;\n}\n\n/**\n * Run one step of the load and keep its failure inside the consumer contract.\n *\n * A {@link SmeltError} passes through untouched — `assertLocalResource` and\n * `grammarPath` already refuse in smelt's own currency, and rewrapping would bury a\n * sentence written deliberately under a generic one.\n */\nasync function inContract<T>(run: () => Promise<T>, what: string): Promise<T> {\n try {\n return await run();\n } catch (error) {\n if (error instanceof SmeltError) throw error;\n throw new GrammarUnavailableError(`${what}: ${describeFailure(error)}.`, { cause: error });\n }\n}\n\n/**\n * What Node or V8 actually said. Never invented, never swallowed — the wasm cases\n * (`CompileError`, `RangeError`) carry no `errno`, and their message is the whole\n * diagnosis.\n */\nfunction describeFailure(cause: unknown): string {\n if (cause instanceof Error && cause.message !== '') return cause.message;\n return String(cause);\n}\n\n/** Reset the grammar cache. Tests use it; production has no reason to. */\nexport function clearGrammarCache(): void {\n cache.clear();\n runtimeReady = undefined;\n}\n"]}
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { Language, Parser } from 'web-tree-sitter';
6
- import { GrammarUnavailableError } from '../errors.js';
6
+ import { GrammarUnavailableError, SmeltError } from '../errors.js';
7
7
  import { LANGUAGE_PROFILES } from '../lang/registry.js';
8
8
  import { assertLocalResource } from '../net/policy.js';
9
9
  /**
@@ -64,19 +64,60 @@ export function grammarPath(language) {
64
64
  * ordinary happy path. Reading the file ourselves removes that capability instead of
65
65
  * documenting it, and {@link assertLocalResource} rejects a remote path before we get
66
66
  * that far.
67
+ *
68
+ * **Every failure here is a `GrammarUnavailableError`.** The consumer contract makes
69
+ * exactly one promise about errors — every error smelt throws is an `instanceof
70
+ * SmeltError` — and this function is on the path of both `smelt()` and
71
+ * `buildRepoMap()`, for every file in a language smelt claims to parse. Only
72
+ * *resolution* used to be inside the contract: `grammarPath` throws for a grammar it
73
+ * cannot find, and {@link existsSync} then reports a file's **presence**, never its
74
+ * readability. So a grammar that resolved and then would not load leaked the raw
75
+ * error — `EACCES` from an unreadable `.wasm`, a V8 `CompileError` or `RangeError`
76
+ * from a truncated one, a half-extracted tarball — straight past a caller's
77
+ * documented `catch`. Each step is wrapped instead, naming the language and the path
78
+ * and keeping the original as `cause`: bringing the failure inside the contract, not
79
+ * hiding what Node or V8 said. A promise with one undocumented exception is no
80
+ * promise at all.
67
81
  */
68
82
  export async function loadGrammar(language) {
69
83
  const cached = cache.get(language);
70
84
  if (cached !== undefined)
71
85
  return cached;
72
- runtimeReady ??= Parser.init();
73
- await runtimeReady;
74
- const resolved = assertLocalResource(grammarPath(language));
75
- const bytes = await readFile(fileURLToPath(resolved));
76
- const grammar = await Language.load(new Uint8Array(bytes));
86
+ const path = fileURLToPath(assertLocalResource(grammarPath(language)).href);
87
+ const ready = (runtimeReady ??= Parser.init());
88
+ await inContract(() => ready, `smelt: the tree-sitter WASM runtime would not start, so no grammar can be loaded`);
89
+ const bytes = await inContract(() => readFile(path), `smelt: the grammar for "${language}" could not be read from "${path}"`);
90
+ const grammar = await inContract(() => Language.load(new Uint8Array(bytes)), `smelt: the file at "${path}" is not a loadable tree-sitter grammar for "${language}"`);
77
91
  cache.set(language, grammar);
78
92
  return grammar;
79
93
  }
94
+ /**
95
+ * Run one step of the load and keep its failure inside the consumer contract.
96
+ *
97
+ * A {@link SmeltError} passes through untouched — `assertLocalResource` and
98
+ * `grammarPath` already refuse in smelt's own currency, and rewrapping would bury a
99
+ * sentence written deliberately under a generic one.
100
+ */
101
+ async function inContract(run, what) {
102
+ try {
103
+ return await run();
104
+ }
105
+ catch (error) {
106
+ if (error instanceof SmeltError)
107
+ throw error;
108
+ throw new GrammarUnavailableError(`${what}: ${describeFailure(error)}.`, { cause: error });
109
+ }
110
+ }
111
+ /**
112
+ * What Node or V8 actually said. Never invented, never swallowed — the wasm cases
113
+ * (`CompileError`, `RangeError`) carry no `errno`, and their message is the whole
114
+ * diagnosis.
115
+ */
116
+ function describeFailure(cause) {
117
+ if (cause instanceof Error && cause.message !== '')
118
+ return cause.message;
119
+ return String(cause);
120
+ }
80
121
  /** Reset the grammar cache. Tests use it; production has no reason to. */
81
122
  export function clearGrammarCache() {
82
123
  cache.clear();
@@ -1 +1 @@
1
- {"version":3,"file":"grammar.js","sourceRoot":"","sources":["../../src/plan/grammar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGvD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAyC,MAAM,CAAC,WAAW,CACtF,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAChD,CAAC;AAEhC;;;;;;;;;GASG;AACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,iBAAiB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAExE,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;AAC9C,IAAI,YAAuC,CAAC;AAE5C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,QAAoB;IAC9C,MAAM,IAAI,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAExC,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAClE,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAExC,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,OAAO,CAAC,yBAAyB,IAAI,EAAE,CAAC,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,uBAAuB,CAC/B,0BAA0B,QAAQ,sCAAsC;YACtE,kFAAkF;YAClF,kEAAkE,CACrE,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAoB;IACpD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IAExC,YAAY,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC;IAC/B,MAAM,YAAY,CAAC;IAEnB,MAAM,QAAQ,GAAG,mBAAmB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3D,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC7B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,iBAAiB;IAC/B,KAAK,CAAC,KAAK,EAAE,CAAC;IACd,YAAY,GAAG,SAAS,CAAC;AAC3B,CAAC"}
1
+ {"version":3,"file":"grammar.js","sourceRoot":"","sources":["../../src/plan/grammar.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { fileURLToPath } from 'node:url';\n\nimport { Language, Parser } from 'web-tree-sitter';\n\nimport { GrammarUnavailableError, SmeltError } from '../errors.ts';\nimport { LANGUAGE_PROFILES } from '../lang/registry.ts';\nimport { assertLocalResource } from '../net/policy.ts';\nimport type { LanguageId } from '../types.ts';\n\n/**\n * Grammar file for each language smelt claims to parse — the registry's `wasm`\n * facts, as a map. Derived from `LANGUAGE_PROFILES` (which is\n * `Record<LanguageId, LanguageProfile>`, so adding a `LanguageId` without a profile —\n * and its grammar — is a type error and the two cannot drift). It stays exported\n * because `scripts/bundle-grammars.mjs` and the attribution generator both read it —\n * a hand-written second list of grammar filenames would be exactly the drift the\n * registry exists to prevent.\n */\nexport const WASM_BY_LANGUAGE: Readonly<Record<LanguageId, string>> = Object.fromEntries(\n Object.values(LANGUAGE_PROFILES).map((profile) => [profile.id, profile.wasm]),\n) as Record<LanguageId, string>;\n\n/**\n * Where the bundled grammars live, relative to this module.\n *\n * From `dist/plan/grammar.js` and from `src/plan/grammar.ts` alike, `../../grammars/`\n * is this package's own `grammars/` directory — filled by `pnpm build` and shipped\n * inside the npm tarball. That is what makes \"zero native compilation, works offline\"\n * true rather than aspirational: whoever installs the package has the parsers, with no\n * post-install download and no optional peer dependency to remember. It is also\n * redistribution, which is why `THIRD-PARTY.md` exists and is generated.\n */\nconst BUNDLED_GRAMMAR_DIR = new URL('../../grammars/', import.meta.url);\n\nconst require = createRequire(import.meta.url);\nconst cache = new Map<LanguageId, Language>();\nlet runtimeReady: Promise<void> | undefined;\n\n/**\n * Resolve a grammar to a path on this machine.\n *\n * The copy bundled in this package wins; `tree-sitter-wasms` is the fallback, for a\n * source checkout that has not run `pnpm build` yet. Note what this function does *not*\n * do: it never constructs a URL from a version string, a CDN base, or anything else.\n * Grammars come off disk — either the ones shipped here or the ones a package manager\n * already installed. A \"fetch the grammar on first use\" cache is the most natural way\n * to break Law 1 without noticing, because it works perfectly on the machine that\n * wrote it.\n */\nexport function grammarPath(language: LanguageId): string {\n const file = WASM_BY_LANGUAGE[language];\n\n const bundled = fileURLToPath(new URL(file, BUNDLED_GRAMMAR_DIR));\n if (existsSync(bundled)) return bundled;\n\n try {\n return require.resolve(`tree-sitter-wasms/out/${file}`);\n } catch {\n throw new GrammarUnavailableError(\n `smelt: no grammar for \"${language}\". The bundled copy is missing (run ` +\n `\\`pnpm build\\` in a source checkout) and \\`tree-sitter-wasms\\` is not installed ` +\n `either. Pass \\`language: 'unknown'\\` to use the lexical planner.`,\n );\n }\n}\n\n/**\n * Load a grammar, from disk, once.\n *\n * The bytes are read here and handed to tree-sitter as a `Uint8Array` rather than\n * passing it a path. `Language.load()` accepts `string | URL`, and a `URL` with an\n * `https:` scheme would make it fetch — inside the elision path, from a dependency's\n * ordinary happy path. Reading the file ourselves removes that capability instead of\n * documenting it, and {@link assertLocalResource} rejects a remote path before we get\n * that far.\n *\n * **Every failure here is a `GrammarUnavailableError`.** The consumer contract makes\n * exactly one promise about errors — every error smelt throws is an `instanceof\n * SmeltError` — and this function is on the path of both `smelt()` and\n * `buildRepoMap()`, for every file in a language smelt claims to parse. Only\n * *resolution* used to be inside the contract: `grammarPath` throws for a grammar it\n * cannot find, and {@link existsSync} then reports a file's **presence**, never its\n * readability. So a grammar that resolved and then would not load leaked the raw\n * error — `EACCES` from an unreadable `.wasm`, a V8 `CompileError` or `RangeError`\n * from a truncated one, a half-extracted tarball — straight past a caller's\n * documented `catch`. Each step is wrapped instead, naming the language and the path\n * and keeping the original as `cause`: bringing the failure inside the contract, not\n * hiding what Node or V8 said. A promise with one undocumented exception is no\n * promise at all.\n */\nexport async function loadGrammar(language: LanguageId): Promise<Language> {\n const cached = cache.get(language);\n if (cached !== undefined) return cached;\n\n const path = fileURLToPath(assertLocalResource(grammarPath(language)).href);\n\n const ready = (runtimeReady ??= Parser.init());\n await inContract(\n () => ready,\n `smelt: the tree-sitter WASM runtime would not start, so no grammar can be loaded`,\n );\n const bytes = await inContract(\n () => readFile(path),\n `smelt: the grammar for \"${language}\" could not be read from \"${path}\"`,\n );\n const grammar = await inContract(\n () => Language.load(new Uint8Array(bytes)),\n `smelt: the file at \"${path}\" is not a loadable tree-sitter grammar for \"${language}\"`,\n );\n cache.set(language, grammar);\n return grammar;\n}\n\n/**\n * Run one step of the load and keep its failure inside the consumer contract.\n *\n * A {@link SmeltError} passes through untouched — `assertLocalResource` and\n * `grammarPath` already refuse in smelt's own currency, and rewrapping would bury a\n * sentence written deliberately under a generic one.\n */\nasync function inContract<T>(run: () => Promise<T>, what: string): Promise<T> {\n try {\n return await run();\n } catch (error) {\n if (error instanceof SmeltError) throw error;\n throw new GrammarUnavailableError(`${what}: ${describeFailure(error)}.`, { cause: error });\n }\n}\n\n/**\n * What Node or V8 actually said. Never invented, never swallowed — the wasm cases\n * (`CompileError`, `RangeError`) carry no `errno`, and their message is the whole\n * diagnosis.\n */\nfunction describeFailure(cause: unknown): string {\n if (cause instanceof Error && cause.message !== '') return cause.message;\n return String(cause);\n}\n\n/** Reset the grammar cache. Tests use it; production has no reason to. */\nexport function clearGrammarCache(): void {\n cache.clear();\n runtimeReady = undefined;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"lexical.d.ts","sourceRoot":"","sources":["../../src/plan/lexical.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAiB,SAAS,EAAkB,OAAO,EAAE,MAAM,aAAa,CAAC;AAElG,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAK/C,MAAM,WAAW,qBAAqB;IACpC,yFAAyF;IACzF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAUD;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAe,YAAW,OAAO;;IAC5C,QAAQ,CAAC,EAAE,gBAAsB;IAGjC,YAAY,OAAO,GAAE,qBAA0B,EAE9C;IAED,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAE3C;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,qBAA0B,GAAG,WAAW,CAsC9F"}
1
+ {"version":3,"file":"lexical.d.ts","sourceRoot":"","sources":["../../src/plan/lexical.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAiB,SAAS,EAAkB,OAAO,EAAE,MAAM,aAAa,CAAC;AAIlG,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAK/C,MAAM,WAAW,qBAAqB;IACpC,yFAAyF;IACzF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAUD;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAe,YAAW,OAAO;;IAC5C,QAAQ,CAAC,EAAE,gBAAsB;IAGjC,YAAY,OAAO,GAAE,qBAA0B,EAE9C;IAED,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAE3C;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,qBAA0B,GAAG,WAAW,CAsC9F","sourcesContent":["import { MissingMarkerPricingError } from '../errors.ts';\nimport type { ElisionPlan, MarkerPricing, PlanInput, PlannedElision, Planner } from '../types.ts';\n\nimport { markerBytes, predictOutputBytes } from './budget.ts';\n\nexport const LEXICAL_PLANNER_ID = 'lexical/v1';\n\n/** How hard the head/tail strategy squeezes, in order, when the budget is not met. */\nconst HEAD_TAIL_LADDER: readonly number[] = [1, 0.5, 0.25, 0.1, 0.05];\n\nexport interface LexicalPlannerOptions {\n /** Lines of context kept either side of a focus match. Shrinks under budget pressure. */\n readonly contextLines?: number;\n /** Never collapse a run shorter than this, however tempting. */\n readonly minRunLines?: number;\n /** With no focus terms: lines kept at the top. */\n readonly headLines?: number;\n /** With no focus terms: lines kept at the bottom. */\n readonly tailLines?: number;\n /** Focus matching is substring, case-insensitive by default. */\n readonly caseSensitive?: boolean;\n}\n\ninterface Line {\n /** Byte offset of the first byte of the line. */\n readonly start: number;\n /** Byte offset one past the last byte of the line, *excluding* its newline. */\n readonly end: number;\n readonly text: string;\n}\n\n/**\n * The fallback planner, and the reference implementation of the whole pipeline.\n *\n * It knows nothing about syntax: it keeps the lines you were looking for plus a window\n * of context, and collapses the runs in between. That is a weak strategy on code and a\n * perfectly good one on log files, stack traces, JSON dumps and every other thing a\n * coding agent shovels into a prompt — which is why it is the fallback rather than an\n * embarrassment. It also runs on languages smelt has no grammar for, so\n * `language: 'unknown'` is never a dead end.\n *\n * Everything here is deterministic and rule-named. Same input, same plan, and every\n * elision can say which rule produced it.\n */\nexport class LexicalPlanner implements Planner {\n readonly id = LEXICAL_PLANNER_ID;\n readonly #options: LexicalPlannerOptions;\n\n constructor(options: LexicalPlannerOptions = {}) {\n this.#options = options;\n }\n\n plan(input: PlanInput): Promise<ElisionPlan> {\n return Promise.resolve(planLexical(input, this.#options));\n }\n}\n\n/**\n * The synchronous core, exported because it is worth testing and reusing directly.\n *\n * `budgetBytes` is a target, not a guarantee. If the smallest context window still\n * exceeds it, the plan comes back over budget rather than eliding the matches the\n * caller asked to keep — an optimizer that silently drops the thing you searched for is\n * the exact failure this design is built to prevent. Callers who need a hard ceiling\n * check `outputBytes` and decide; smelt will not decide for them.\n *\n * Profitability and budget prediction are **measured, not estimated**: every candidate\n * elision is priced through the input's {@link MarkerPricing} — the seam `apply.ts`\n * builds from the exact builder `applyPlan` will use, comment leader and custom\n * builder included. A guessed constant here once under-counted real ~105-byte markers\n * as 64, so a plan could be \"chosen as fitting\" and then come back over budget after\n * the markers landed.\n */\nexport function planLexical(input: PlanInput, options: LexicalPlannerOptions = {}): ElisionPlan {\n const pricing = requirePricing(input);\n const lines = splitLines(input.text);\n const focus = (input.focus ?? []).filter((term) => term.length > 0);\n const minRunLines = options.minRunLines ?? 3;\n\n const attempts: readonly (readonly PlannedElision[])[] =\n focus.length > 0\n ? ladder(options.contextLines ?? 4).map((context) =>\n collapse(lines, keepByFocus(lines, focus, context, options.caseSensitive ?? false), {\n minRunLines,\n rule: 'focus-window',\n pricing,\n }),\n )\n : HEAD_TAIL_LADDER.map((shrink) =>\n collapse(\n lines,\n keepByHeadTail(\n lines,\n Math.max(3, Math.round((options.headLines ?? 40) * shrink)),\n Math.max(3, Math.round((options.tailLines ?? 20) * shrink)),\n ),\n { minRunLines, rule: 'head-tail', pricing },\n ),\n );\n\n const inputBytes = Buffer.byteLength(input.text, 'utf8');\n const chosen =\n attempts.find(\n (elisions) => predictOutputBytes(inputBytes, elisions, pricing) <= input.budgetBytes,\n ) ?? attempts[attempts.length - 1]!;\n\n return {\n planner: LEXICAL_PLANNER_ID,\n language: input.language,\n elisions: chosen,\n };\n}\n\n/** Context-window sizes to try, largest first. */\nfunction ladder(start: number): readonly number[] {\n const sizes: number[] = [];\n for (let n = start; n >= 0; n -= 1) sizes.push(n);\n if (sizes.length === 0) sizes.push(0);\n return sizes;\n}\n\n/**\n * The runtime backstop for JS callers: TypeScript makes `pricing` required, but a JS\n * caller can omit it, and the honest answer is a named refusal rather than a planner\n * quietly pricing markers itself — the inversion the seam removed.\n */\nfunction requirePricing(input: PlanInput): MarkerPricing {\n const pricing: MarkerPricing | undefined = input.pricing;\n if (pricing === undefined) throw new MissingMarkerPricingError(LEXICAL_PLANNER_ID);\n return pricing;\n}\n\nfunction splitLines(text: string): readonly Line[] {\n const lines: Line[] = [];\n let start = 0;\n let byte = 0;\n const raw = text.split('\\n');\n for (let i = 0; i < raw.length; i += 1) {\n const content = raw[i]!;\n const contentBytes = Buffer.byteLength(content, 'utf8');\n byte = start + contentBytes;\n lines.push({ start, end: byte, text: content });\n // +1 for the '\\n' we split on; the final fragment has none.\n start = byte + 1;\n }\n return lines;\n}\n\nfunction keepByFocus(\n lines: readonly Line[],\n focus: readonly string[],\n contextLines: number,\n caseSensitive: boolean,\n): boolean[] {\n const needles = caseSensitive ? focus : focus.map((t) => t.toLowerCase());\n const keep: boolean[] = Array.from({ length: lines.length }, () => false);\n\n for (let i = 0; i < lines.length; i += 1) {\n const haystack = caseSensitive ? lines[i]!.text : lines[i]!.text.toLowerCase();\n if (!needles.some((needle) => haystack.includes(needle))) continue;\n const from = Math.max(0, i - contextLines);\n const to = Math.min(lines.length - 1, i + contextLines);\n for (let j = from; j <= to; j += 1) keep[j] = true;\n }\n return keep;\n}\n\nfunction keepByHeadTail(lines: readonly Line[], head: number, tail: number): boolean[] {\n const keep: boolean[] = Array.from({ length: lines.length }, () => false);\n for (let i = 0; i < Math.min(head, lines.length); i += 1) keep[i] = true;\n for (let i = Math.max(0, lines.length - tail); i < lines.length; i += 1) keep[i] = true;\n return keep;\n}\n\nfunction collapse(\n lines: readonly Line[],\n keep: readonly boolean[],\n config: { readonly minRunLines: number; readonly rule: string; readonly pricing: MarkerPricing },\n): readonly PlannedElision[] {\n const elisions: PlannedElision[] = [];\n let runStart = -1;\n\n const flush = (endExclusive: number): void => {\n if (runStart < 0) return;\n const count = endExclusive - runStart;\n const range = { start: lines[runStart]!.start, end: lines[endExclusive - 1]!.end };\n runStart = -1;\n if (count < config.minRunLines) return;\n const candidate: PlannedElision = {\n range,\n reason: {\n rule: config.rule,\n explanation: `collapsed ${String(count)} ${count === 1 ? 'line' : 'lines'}${describe(\n config.rule,\n )}`,\n },\n };\n // Profitability, measured rather than estimated: a marker that costs at least as\n // many bytes as it removes grows the output — same rule, same seam, as the\n // structural planner.\n if (range.end - range.start <= markerBytes(candidate, config.pricing)) return;\n elisions.push(candidate);\n };\n\n for (let i = 0; i < lines.length; i += 1) {\n if (keep[i] === true) {\n flush(i);\n } else if (runStart < 0) {\n runStart = i;\n }\n }\n flush(lines.length);\n return elisions;\n}\n\nfunction describe(rule: string): string {\n return rule === 'focus-window' ? ' with no match for the focus terms' : ' from the middle';\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { MissingMarkerPricingError } from '../errors.js';
2
+ import { markerBytes, predictOutputBytes } from './budget.js';
2
3
  export const LEXICAL_PLANNER_ID = 'lexical/v1';
3
4
  /** How hard the head/tail strategy squeezes, in order, when the budget is not met. */
4
5
  const HEAD_TAIL_LADDER = [1, 0.5, 0.25, 0.1, 0.05];
@@ -81,13 +82,6 @@ function requirePricing(input) {
81
82
  throw new MissingMarkerPricingError(LEXICAL_PLANNER_ID);
82
83
  return pricing;
83
84
  }
84
- /** The exact UTF-8 cost of the marker this elision would earn. Asked, not guessed. */
85
- function markerBytes(elision, pricing) {
86
- return pricing.costBytes(elision.reason, elision.range.end - elision.range.start);
87
- }
88
- function predictOutputBytes(inputBytes, elisions, pricing) {
89
- return elisions.reduce((bytes, elision) => bytes - (elision.range.end - elision.range.start) + markerBytes(elision, pricing), inputBytes);
90
- }
91
85
  function splitLines(text) {
92
86
  const lines = [];
93
87
  let start = 0;
@@ -1 +1 @@
1
- {"version":3,"file":"lexical.js","sourceRoot":"","sources":["../../src/plan/lexical.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lexical.js","sourceRoot":"","sources":["../../src/plan/lexical.ts"],"names":[],"mappings":"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","sourcesContent":["import { MissingMarkerPricingError } from '../errors.ts';\nimport type { ElisionPlan, MarkerPricing, PlanInput, PlannedElision, Planner } from '../types.ts';\n\nimport { markerBytes, predictOutputBytes } from './budget.ts';\n\nexport const LEXICAL_PLANNER_ID = 'lexical/v1';\n\n/** How hard the head/tail strategy squeezes, in order, when the budget is not met. */\nconst HEAD_TAIL_LADDER: readonly number[] = [1, 0.5, 0.25, 0.1, 0.05];\n\nexport interface LexicalPlannerOptions {\n /** Lines of context kept either side of a focus match. Shrinks under budget pressure. */\n readonly contextLines?: number;\n /** Never collapse a run shorter than this, however tempting. */\n readonly minRunLines?: number;\n /** With no focus terms: lines kept at the top. */\n readonly headLines?: number;\n /** With no focus terms: lines kept at the bottom. */\n readonly tailLines?: number;\n /** Focus matching is substring, case-insensitive by default. */\n readonly caseSensitive?: boolean;\n}\n\ninterface Line {\n /** Byte offset of the first byte of the line. */\n readonly start: number;\n /** Byte offset one past the last byte of the line, *excluding* its newline. */\n readonly end: number;\n readonly text: string;\n}\n\n/**\n * The fallback planner, and the reference implementation of the whole pipeline.\n *\n * It knows nothing about syntax: it keeps the lines you were looking for plus a window\n * of context, and collapses the runs in between. That is a weak strategy on code and a\n * perfectly good one on log files, stack traces, JSON dumps and every other thing a\n * coding agent shovels into a prompt — which is why it is the fallback rather than an\n * embarrassment. It also runs on languages smelt has no grammar for, so\n * `language: 'unknown'` is never a dead end.\n *\n * Everything here is deterministic and rule-named. Same input, same plan, and every\n * elision can say which rule produced it.\n */\nexport class LexicalPlanner implements Planner {\n readonly id = LEXICAL_PLANNER_ID;\n readonly #options: LexicalPlannerOptions;\n\n constructor(options: LexicalPlannerOptions = {}) {\n this.#options = options;\n }\n\n plan(input: PlanInput): Promise<ElisionPlan> {\n return Promise.resolve(planLexical(input, this.#options));\n }\n}\n\n/**\n * The synchronous core, exported because it is worth testing and reusing directly.\n *\n * `budgetBytes` is a target, not a guarantee. If the smallest context window still\n * exceeds it, the plan comes back over budget rather than eliding the matches the\n * caller asked to keep — an optimizer that silently drops the thing you searched for is\n * the exact failure this design is built to prevent. Callers who need a hard ceiling\n * check `outputBytes` and decide; smelt will not decide for them.\n *\n * Profitability and budget prediction are **measured, not estimated**: every candidate\n * elision is priced through the input's {@link MarkerPricing} — the seam `apply.ts`\n * builds from the exact builder `applyPlan` will use, comment leader and custom\n * builder included. A guessed constant here once under-counted real ~105-byte markers\n * as 64, so a plan could be \"chosen as fitting\" and then come back over budget after\n * the markers landed.\n */\nexport function planLexical(input: PlanInput, options: LexicalPlannerOptions = {}): ElisionPlan {\n const pricing = requirePricing(input);\n const lines = splitLines(input.text);\n const focus = (input.focus ?? []).filter((term) => term.length > 0);\n const minRunLines = options.minRunLines ?? 3;\n\n const attempts: readonly (readonly PlannedElision[])[] =\n focus.length > 0\n ? ladder(options.contextLines ?? 4).map((context) =>\n collapse(lines, keepByFocus(lines, focus, context, options.caseSensitive ?? false), {\n minRunLines,\n rule: 'focus-window',\n pricing,\n }),\n )\n : HEAD_TAIL_LADDER.map((shrink) =>\n collapse(\n lines,\n keepByHeadTail(\n lines,\n Math.max(3, Math.round((options.headLines ?? 40) * shrink)),\n Math.max(3, Math.round((options.tailLines ?? 20) * shrink)),\n ),\n { minRunLines, rule: 'head-tail', pricing },\n ),\n );\n\n const inputBytes = Buffer.byteLength(input.text, 'utf8');\n const chosen =\n attempts.find(\n (elisions) => predictOutputBytes(inputBytes, elisions, pricing) <= input.budgetBytes,\n ) ?? attempts[attempts.length - 1]!;\n\n return {\n planner: LEXICAL_PLANNER_ID,\n language: input.language,\n elisions: chosen,\n };\n}\n\n/** Context-window sizes to try, largest first. */\nfunction ladder(start: number): readonly number[] {\n const sizes: number[] = [];\n for (let n = start; n >= 0; n -= 1) sizes.push(n);\n if (sizes.length === 0) sizes.push(0);\n return sizes;\n}\n\n/**\n * The runtime backstop for JS callers: TypeScript makes `pricing` required, but a JS\n * caller can omit it, and the honest answer is a named refusal rather than a planner\n * quietly pricing markers itself — the inversion the seam removed.\n */\nfunction requirePricing(input: PlanInput): MarkerPricing {\n const pricing: MarkerPricing | undefined = input.pricing;\n if (pricing === undefined) throw new MissingMarkerPricingError(LEXICAL_PLANNER_ID);\n return pricing;\n}\n\nfunction splitLines(text: string): readonly Line[] {\n const lines: Line[] = [];\n let start = 0;\n let byte = 0;\n const raw = text.split('\\n');\n for (let i = 0; i < raw.length; i += 1) {\n const content = raw[i]!;\n const contentBytes = Buffer.byteLength(content, 'utf8');\n byte = start + contentBytes;\n lines.push({ start, end: byte, text: content });\n // +1 for the '\\n' we split on; the final fragment has none.\n start = byte + 1;\n }\n return lines;\n}\n\nfunction keepByFocus(\n lines: readonly Line[],\n focus: readonly string[],\n contextLines: number,\n caseSensitive: boolean,\n): boolean[] {\n const needles = caseSensitive ? focus : focus.map((t) => t.toLowerCase());\n const keep: boolean[] = Array.from({ length: lines.length }, () => false);\n\n for (let i = 0; i < lines.length; i += 1) {\n const haystack = caseSensitive ? lines[i]!.text : lines[i]!.text.toLowerCase();\n if (!needles.some((needle) => haystack.includes(needle))) continue;\n const from = Math.max(0, i - contextLines);\n const to = Math.min(lines.length - 1, i + contextLines);\n for (let j = from; j <= to; j += 1) keep[j] = true;\n }\n return keep;\n}\n\nfunction keepByHeadTail(lines: readonly Line[], head: number, tail: number): boolean[] {\n const keep: boolean[] = Array.from({ length: lines.length }, () => false);\n for (let i = 0; i < Math.min(head, lines.length); i += 1) keep[i] = true;\n for (let i = Math.max(0, lines.length - tail); i < lines.length; i += 1) keep[i] = true;\n return keep;\n}\n\nfunction collapse(\n lines: readonly Line[],\n keep: readonly boolean[],\n config: { readonly minRunLines: number; readonly rule: string; readonly pricing: MarkerPricing },\n): readonly PlannedElision[] {\n const elisions: PlannedElision[] = [];\n let runStart = -1;\n\n const flush = (endExclusive: number): void => {\n if (runStart < 0) return;\n const count = endExclusive - runStart;\n const range = { start: lines[runStart]!.start, end: lines[endExclusive - 1]!.end };\n runStart = -1;\n if (count < config.minRunLines) return;\n const candidate: PlannedElision = {\n range,\n reason: {\n rule: config.rule,\n explanation: `collapsed ${String(count)} ${count === 1 ? 'line' : 'lines'}${describe(\n config.rule,\n )}`,\n },\n };\n // Profitability, measured rather than estimated: a marker that costs at least as\n // many bytes as it removes grows the output — same rule, same seam, as the\n // structural planner.\n if (range.end - range.start <= markerBytes(candidate, config.pricing)) return;\n elisions.push(candidate);\n };\n\n for (let i = 0; i < lines.length; i += 1) {\n if (keep[i] === true) {\n flush(i);\n } else if (runStart < 0) {\n runStart = i;\n }\n }\n flush(lines.length);\n return elisions;\n}\n\nfunction describe(rule: string): string {\n return rule === 'focus-window' ? ' with no match for the focus terms' : ' from the middle';\n}\n"]}
@@ -4,6 +4,7 @@ import type { StructuralPlannerOptions } from './structural.ts';
4
4
  /**
5
5
  * The option bags a strategy factory may draw from — the same fields
6
6
  * `SmelterConfig` carries, so the config object itself can be handed to a factory.
7
+ * `auto` draws from both, because it may run either planner.
7
8
  */
8
9
  export interface PlannerFactoryOptions {
9
10
  readonly lexical?: LexicalPlannerOptions;
@@ -13,23 +14,55 @@ export interface PlannerFactoryOptions {
13
14
  * The one registry of planner strategies — string in, constructed {@link Planner} out.
14
15
  *
15
16
  * This object is the single place the strategy names live. `createSmelter` builds from
16
- * it, `--strategy` and `smelt.config.json` validation accept exactly its keys, and the
17
- * `--help` text renders its keys so a strategy cannot exist in one of those faces and
17
+ * it, `--strategy` and `smelt.config.json` validation accept exactly its keys, the
18
+ * `--help` text and the `init` wizard render its keys, and the `smelt_file` tool's
19
+ * JSON Schema enumerates them — so a strategy cannot exist in one of those faces and
18
20
  * be missing from another. Before this registry the pair was restated in three places,
19
21
  * which is how help text rots.
20
22
  *
21
23
  * `'structural'` parses every language named in {@link STRUCTURAL_LANGUAGES} with a
22
24
  * bundled grammar and throws {@link GrammarUnavailableError} for anything else — never
23
25
  * a silent lexical fallback. See {@link StructuralPlanner}.
26
+ *
27
+ * `'auto'` picks between the two on the language and **labels what ran**: its plans
28
+ * come back as `lexical/v1` or `structural/v1`, never as `auto`. It is a selector, not
29
+ * a fallback — an explicit `'structural'` on an unsupported language still refuses,
30
+ * because a caller who named the planner asked for its guarantees. See
31
+ * {@link AutoPlanner}, whose doc comment carries the reasoning.
32
+ *
33
+ * Key order is the order every rendered list uses, so append rather than reorder.
24
34
  */
25
35
  export declare const PLANNERS: {
26
36
  readonly lexical: (options: PlannerFactoryOptions) => Planner;
27
37
  readonly structural: (options: PlannerFactoryOptions) => Planner;
38
+ readonly auto: (options: PlannerFactoryOptions) => Planner;
28
39
  };
29
40
  /** Which planner a smelter uses, named by string. Exactly the keys of {@link PLANNERS}. */
30
41
  export type Strategy = keyof typeof PLANNERS;
31
42
  /** The registry's keys, in declaration order, for help text and error messages. */
32
43
  export declare const STRATEGIES: readonly Strategy[];
44
+ /**
45
+ * The strategy a caller who names none gets — the registry's own default, beside the
46
+ * names it defaults among.
47
+ *
48
+ * Every `?? 'lexical'` in this repository reads this constant instead: `createSmelter`,
49
+ * the `smelt` verb's merge, the `init` wizard's starting choice, and the MCP server's
50
+ * `smelt_file`. The names were already derived from {@link PLANNERS} while the default
51
+ * stayed hand-typed in four places across two packages — so the one fact every caller
52
+ * needs was the one the registry did not carry, and a changed default would have moved
53
+ * on some faces and not others.
54
+ *
55
+ * `'lexical'` is the default because it works on any text: `'structural'` refuses a
56
+ * language it has no grammar for rather than approximating (see {@link StructuralPlanner}),
57
+ * which is right when a caller asked for it and wrong as the answer to "no preference".
58
+ *
59
+ * `'auto'` refuses nothing either, and is the better answer for a caller smelting a
60
+ * mixed stream — but it stays **opt-in**. Promoting it would change which planner runs,
61
+ * and therefore what `result.planner` says, for every existing caller who never named a
62
+ * strategy: a behaviour change delivered to people who asked for nothing. A caller who
63
+ * wants it says so, in a flag, a config, or a tool argument.
64
+ */
65
+ export declare const DEFAULT_STRATEGY: Strategy;
33
66
  /** The one membership test `--strategy` and config validation both use. */
34
67
  export declare function isStrategy(value: string): value is Strategy;
35
68
  //# sourceMappingURL=planners.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"planners.d.ts","sourceRoot":"","sources":["../../src/plan/planners.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC;IACzC,QAAQ,CAAC,UAAU,CAAC,EAAE,wBAAwB,CAAC;CAChD;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,QAAQ;gCACA,qBAAqB,KAAG,OAAO;mCAC5B,qBAAqB,KAAG,OAAO;CAEyB,CAAC;AAEjF,2FAA2F;AAC3F,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,QAAQ,CAAC;AAE7C,mFAAmF;AACnF,eAAO,MAAM,UAAU,EAA4B,SAAS,QAAQ,EAAE,CAAC;AAEvE,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,QAAQ,CAE3D"}
1
+ {"version":3,"file":"planners.d.ts","sourceRoot":"","sources":["../../src/plan/planners.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC;IACzC,QAAQ,CAAC,UAAU,CAAC,EAAE,wBAAwB,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,QAAQ;gCACA,qBAAqB,KAAG,OAAO;mCAC5B,qBAAqB,KAAG,OAAO;6BAErC,qBAAqB,KAAG,OAAO;CAC+B,CAAC;AAEjF,2FAA2F;AAC3F,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,QAAQ,CAAC;AAE7C,mFAAmF;AACnF,eAAO,MAAM,UAAU,EAA4B,SAAS,QAAQ,EAAE,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAoB,CAAC;AAEpD,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,QAAQ,CAE3D","sourcesContent":["import type { Planner } from '../types.ts';\n\nimport { AutoPlanner } from './auto.ts';\nimport { LexicalPlanner } from './lexical.ts';\nimport type { LexicalPlannerOptions } from './lexical.ts';\nimport { StructuralPlanner } from './structural.ts';\nimport type { StructuralPlannerOptions } from './structural.ts';\n\n/**\n * The option bags a strategy factory may draw from — the same fields\n * `SmelterConfig` carries, so the config object itself can be handed to a factory.\n * `auto` draws from both, because it may run either planner.\n */\nexport interface PlannerFactoryOptions {\n readonly lexical?: LexicalPlannerOptions;\n readonly structural?: StructuralPlannerOptions;\n}\n\n/**\n * The one registry of planner strategies — string in, constructed {@link Planner} out.\n *\n * This object is the single place the strategy names live. `createSmelter` builds from\n * it, `--strategy` and `smelt.config.json` validation accept exactly its keys, the\n * `--help` text and the `init` wizard render its keys, and the `smelt_file` tool's\n * JSON Schema enumerates them — so a strategy cannot exist in one of those faces and\n * be missing from another. Before this registry the pair was restated in three places,\n * which is how help text rots.\n *\n * `'structural'` parses every language named in {@link STRUCTURAL_LANGUAGES} with a\n * bundled grammar and throws {@link GrammarUnavailableError} for anything else — never\n * a silent lexical fallback. See {@link StructuralPlanner}.\n *\n * `'auto'` picks between the two on the language and **labels what ran**: its plans\n * come back as `lexical/v1` or `structural/v1`, never as `auto`. It is a selector, not\n * a fallback — an explicit `'structural'` on an unsupported language still refuses,\n * because a caller who named the planner asked for its guarantees. See\n * {@link AutoPlanner}, whose doc comment carries the reasoning.\n *\n * Key order is the order every rendered list uses, so append rather than reorder.\n */\nexport const PLANNERS = {\n lexical: (options: PlannerFactoryOptions): Planner => new LexicalPlanner(options.lexical ?? {}),\n structural: (options: PlannerFactoryOptions): Planner =>\n new StructuralPlanner(options.structural ?? {}),\n auto: (options: PlannerFactoryOptions): Planner => new AutoPlanner(options),\n} as const satisfies Record<string, (options: PlannerFactoryOptions) => Planner>;\n\n/** Which planner a smelter uses, named by string. Exactly the keys of {@link PLANNERS}. */\nexport type Strategy = keyof typeof PLANNERS;\n\n/** The registry's keys, in declaration order, for help text and error messages. */\nexport const STRATEGIES = Object.keys(PLANNERS) as readonly Strategy[];\n\n/**\n * The strategy a caller who names none gets — the registry's own default, beside the\n * names it defaults among.\n *\n * Every `?? 'lexical'` in this repository reads this constant instead: `createSmelter`,\n * the `smelt` verb's merge, the `init` wizard's starting choice, and the MCP server's\n * `smelt_file`. The names were already derived from {@link PLANNERS} while the default\n * stayed hand-typed in four places across two packages — so the one fact every caller\n * needs was the one the registry did not carry, and a changed default would have moved\n * on some faces and not others.\n *\n * `'lexical'` is the default because it works on any text: `'structural'` refuses a\n * language it has no grammar for rather than approximating (see {@link StructuralPlanner}),\n * which is right when a caller asked for it and wrong as the answer to \"no preference\".\n *\n * `'auto'` refuses nothing either, and is the better answer for a caller smelting a\n * mixed stream — but it stays **opt-in**. Promoting it would change which planner runs,\n * and therefore what `result.planner` says, for every existing caller who never named a\n * strategy: a behaviour change delivered to people who asked for nothing. A caller who\n * wants it says so, in a flag, a config, or a tool argument.\n */\nexport const DEFAULT_STRATEGY: Strategy = 'lexical';\n\n/** The one membership test `--strategy` and config validation both use. */\nexport function isStrategy(value: string): value is Strategy {\n return Object.hasOwn(PLANNERS, value);\n}\n"]}
@@ -1,24 +1,57 @@
1
+ import { AutoPlanner } from './auto.js';
1
2
  import { LexicalPlanner } from './lexical.js';
2
3
  import { StructuralPlanner } from './structural.js';
3
4
  /**
4
5
  * The one registry of planner strategies — string in, constructed {@link Planner} out.
5
6
  *
6
7
  * This object is the single place the strategy names live. `createSmelter` builds from
7
- * it, `--strategy` and `smelt.config.json` validation accept exactly its keys, and the
8
- * `--help` text renders its keys so a strategy cannot exist in one of those faces and
8
+ * it, `--strategy` and `smelt.config.json` validation accept exactly its keys, the
9
+ * `--help` text and the `init` wizard render its keys, and the `smelt_file` tool's
10
+ * JSON Schema enumerates them — so a strategy cannot exist in one of those faces and
9
11
  * be missing from another. Before this registry the pair was restated in three places,
10
12
  * which is how help text rots.
11
13
  *
12
14
  * `'structural'` parses every language named in {@link STRUCTURAL_LANGUAGES} with a
13
15
  * bundled grammar and throws {@link GrammarUnavailableError} for anything else — never
14
16
  * a silent lexical fallback. See {@link StructuralPlanner}.
17
+ *
18
+ * `'auto'` picks between the two on the language and **labels what ran**: its plans
19
+ * come back as `lexical/v1` or `structural/v1`, never as `auto`. It is a selector, not
20
+ * a fallback — an explicit `'structural'` on an unsupported language still refuses,
21
+ * because a caller who named the planner asked for its guarantees. See
22
+ * {@link AutoPlanner}, whose doc comment carries the reasoning.
23
+ *
24
+ * Key order is the order every rendered list uses, so append rather than reorder.
15
25
  */
16
26
  export const PLANNERS = {
17
27
  lexical: (options) => new LexicalPlanner(options.lexical ?? {}),
18
28
  structural: (options) => new StructuralPlanner(options.structural ?? {}),
29
+ auto: (options) => new AutoPlanner(options),
19
30
  };
20
31
  /** The registry's keys, in declaration order, for help text and error messages. */
21
32
  export const STRATEGIES = Object.keys(PLANNERS);
33
+ /**
34
+ * The strategy a caller who names none gets — the registry's own default, beside the
35
+ * names it defaults among.
36
+ *
37
+ * Every `?? 'lexical'` in this repository reads this constant instead: `createSmelter`,
38
+ * the `smelt` verb's merge, the `init` wizard's starting choice, and the MCP server's
39
+ * `smelt_file`. The names were already derived from {@link PLANNERS} while the default
40
+ * stayed hand-typed in four places across two packages — so the one fact every caller
41
+ * needs was the one the registry did not carry, and a changed default would have moved
42
+ * on some faces and not others.
43
+ *
44
+ * `'lexical'` is the default because it works on any text: `'structural'` refuses a
45
+ * language it has no grammar for rather than approximating (see {@link StructuralPlanner}),
46
+ * which is right when a caller asked for it and wrong as the answer to "no preference".
47
+ *
48
+ * `'auto'` refuses nothing either, and is the better answer for a caller smelting a
49
+ * mixed stream — but it stays **opt-in**. Promoting it would change which planner runs,
50
+ * and therefore what `result.planner` says, for every existing caller who never named a
51
+ * strategy: a behaviour change delivered to people who asked for nothing. A caller who
52
+ * wants it says so, in a flag, a config, or a tool argument.
53
+ */
54
+ export const DEFAULT_STRATEGY = 'lexical';
22
55
  /** The one membership test `--strategy` and config validation both use. */
23
56
  export function isStrategy(value) {
24
57
  return Object.hasOwn(PLANNERS, value);
@@ -1 +1 @@
1
- {"version":3,"file":"planners.js","sourceRoot":"","sources":["../../src/plan/planners.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAYpD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,OAAO,EAAE,CAAC,OAA8B,EAAW,EAAE,CAAC,IAAI,cAAc,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;IAC/F,UAAU,EAAE,CAAC,OAA8B,EAAW,EAAE,CACtD,IAAI,iBAAiB,CAAC,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC;CAC6B,CAAC;AAKjF,mFAAmF;AACnF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAwB,CAAC;AAEvE,2EAA2E;AAC3E,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AACxC,CAAC"}
1
+ {"version":3,"file":"planners.js","sourceRoot":"","sources":["../../src/plan/planners.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAapD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,OAAO,EAAE,CAAC,OAA8B,EAAW,EAAE,CAAC,IAAI,cAAc,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;IAC/F,UAAU,EAAE,CAAC,OAA8B,EAAW,EAAE,CACtD,IAAI,iBAAiB,CAAC,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC;IACjD,IAAI,EAAE,CAAC,OAA8B,EAAW,EAAE,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC;CACG,CAAC;AAKjF,mFAAmF;AACnF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAwB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAa,SAAS,CAAC;AAEpD,2EAA2E;AAC3E,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AACxC,CAAC","sourcesContent":["import type { Planner } from '../types.ts';\n\nimport { AutoPlanner } from './auto.ts';\nimport { LexicalPlanner } from './lexical.ts';\nimport type { LexicalPlannerOptions } from './lexical.ts';\nimport { StructuralPlanner } from './structural.ts';\nimport type { StructuralPlannerOptions } from './structural.ts';\n\n/**\n * The option bags a strategy factory may draw from — the same fields\n * `SmelterConfig` carries, so the config object itself can be handed to a factory.\n * `auto` draws from both, because it may run either planner.\n */\nexport interface PlannerFactoryOptions {\n readonly lexical?: LexicalPlannerOptions;\n readonly structural?: StructuralPlannerOptions;\n}\n\n/**\n * The one registry of planner strategies — string in, constructed {@link Planner} out.\n *\n * This object is the single place the strategy names live. `createSmelter` builds from\n * it, `--strategy` and `smelt.config.json` validation accept exactly its keys, the\n * `--help` text and the `init` wizard render its keys, and the `smelt_file` tool's\n * JSON Schema enumerates them — so a strategy cannot exist in one of those faces and\n * be missing from another. Before this registry the pair was restated in three places,\n * which is how help text rots.\n *\n * `'structural'` parses every language named in {@link STRUCTURAL_LANGUAGES} with a\n * bundled grammar and throws {@link GrammarUnavailableError} for anything else — never\n * a silent lexical fallback. See {@link StructuralPlanner}.\n *\n * `'auto'` picks between the two on the language and **labels what ran**: its plans\n * come back as `lexical/v1` or `structural/v1`, never as `auto`. It is a selector, not\n * a fallback — an explicit `'structural'` on an unsupported language still refuses,\n * because a caller who named the planner asked for its guarantees. See\n * {@link AutoPlanner}, whose doc comment carries the reasoning.\n *\n * Key order is the order every rendered list uses, so append rather than reorder.\n */\nexport const PLANNERS = {\n lexical: (options: PlannerFactoryOptions): Planner => new LexicalPlanner(options.lexical ?? {}),\n structural: (options: PlannerFactoryOptions): Planner =>\n new StructuralPlanner(options.structural ?? {}),\n auto: (options: PlannerFactoryOptions): Planner => new AutoPlanner(options),\n} as const satisfies Record<string, (options: PlannerFactoryOptions) => Planner>;\n\n/** Which planner a smelter uses, named by string. Exactly the keys of {@link PLANNERS}. */\nexport type Strategy = keyof typeof PLANNERS;\n\n/** The registry's keys, in declaration order, for help text and error messages. */\nexport const STRATEGIES = Object.keys(PLANNERS) as readonly Strategy[];\n\n/**\n * The strategy a caller who names none gets — the registry's own default, beside the\n * names it defaults among.\n *\n * Every `?? 'lexical'` in this repository reads this constant instead: `createSmelter`,\n * the `smelt` verb's merge, the `init` wizard's starting choice, and the MCP server's\n * `smelt_file`. The names were already derived from {@link PLANNERS} while the default\n * stayed hand-typed in four places across two packages — so the one fact every caller\n * needs was the one the registry did not carry, and a changed default would have moved\n * on some faces and not others.\n *\n * `'lexical'` is the default because it works on any text: `'structural'` refuses a\n * language it has no grammar for rather than approximating (see {@link StructuralPlanner}),\n * which is right when a caller asked for it and wrong as the answer to \"no preference\".\n *\n * `'auto'` refuses nothing either, and is the better answer for a caller smelting a\n * mixed stream — but it stays **opt-in**. Promoting it would change which planner runs,\n * and therefore what `result.planner` says, for every existing caller who never named a\n * strategy: a behaviour change delivered to people who asked for nothing. A caller who\n * wants it says so, in a flag, a config, or a tool argument.\n */\nexport const DEFAULT_STRATEGY: Strategy = 'lexical';\n\n/** The one membership test `--strategy` and config validation both use. */\nexport function isStrategy(value: string): value is Strategy {\n return Object.hasOwn(PLANNERS, value);\n}\n"]}
@@ -54,15 +54,29 @@ export declare class StructuralPlanner implements Planner {
54
54
  * reused directly. Deterministic: same text, same language, same focus, same options —
55
55
  * byte-identical plan.
56
56
  *
57
- * `budgetBytes` is a target, not a guarantee, for the same reason it is in the lexical
58
- * planner: once every non-matching sibling run is collapsed there is nothing left to
59
- * cut except the declarations the caller asked to keep, and an optimizer that silently
60
- * drops the thing you searched for is the exact failure this design refuses. Callers
61
- * who need a hard ceiling check `outputBytes` and decide.
57
+ * `budgetBytes` is a target, not a guarantee. The plan is built in two passes: every
58
+ * maximal run of non-matching siblings is collapsed where that pays for its marker,
59
+ * and then only if the plan is still over budget each run the first pass refused
60
+ * is re-asked as its own best profitable sub-run (the budget rung, in
61
+ * {@link planFromTree}). Under the last of those there is nothing left to cut except
62
+ * the declarations the caller asked to keep, and an optimizer that silently drops the
63
+ * thing you searched for is the exact failure this design refuses. Callers who need a
64
+ * hard ceiling check `outputBytes` and decide.
62
65
  *
63
66
  * @throws {GrammarUnavailableError} when the language is not one this planner parses,
64
67
  * when the grammar cannot be loaded, or when the parser produces no tree. Never a
65
68
  * lexical fallback.
66
69
  */
67
70
  export declare function planStructural(input: PlanInput, options?: StructuralPlannerOptions): Promise<ElisionPlan>;
71
+ /**
72
+ * Whether this planner has a bundled grammar for the language — the one membership
73
+ * test, shared with the `auto` strategy (`plan/auto.ts`).
74
+ *
75
+ * The refusal above and auto's selection are the same question asked for opposite
76
+ * reasons, and two spellings of it would be two answers: a language auto routed to
77
+ * `structural` that `assertStructuralLanguage` then refused would be a
78
+ * `GrammarUnavailableError` raised by the strategy whose entire job is not raising
79
+ * one.
80
+ */
81
+ export declare function isStructuralLanguage(language: PlanInput['language']): language is StructuralLanguage;
68
82
  //# sourceMappingURL=structural.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"structural.d.ts","sourceRoot":"","sources":["../../src/plan/structural.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EAEV,SAAS,EAET,OAAO,EACR,MAAM,aAAa,CAAC;AAIrB,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAErD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,UAAU,EAA0B,CAAC;AAEjF;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,CAAC;AAQ5C,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AA+BD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,iBAAkB,YAAW,OAAO;IAC/C,QAAQ,CAAC,EAAE,mBAAyB;IACpC,QAAQ,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAE3C,YAAY,OAAO,GAAE,wBAA6B,EAEjD;IAED,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAE3C;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,WAAW,CAAC,CA8BtB"}
1
+ {"version":3,"file":"structural.d.ts","sourceRoot":"","sources":["../../src/plan/structural.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EAEV,SAAS,EAET,OAAO,EACR,MAAM,aAAa,CAAC;AAKrB,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAErD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,UAAU,EAA0B,CAAC;AAEjF;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,CAAC;AAQ5C,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AA+BD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,iBAAkB,YAAW,OAAO;IAC/C,QAAQ,CAAC,EAAE,mBAAyB;IACpC,QAAQ,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAE3C,YAAY,OAAO,GAAE,wBAA6B,EAEjD;IAED,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAE3C;CACF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,WAAW,CAAC,CA8BtB;AAiBD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,SAAS,CAAC,UAAU,CAAC,GAC9B,QAAQ,IAAI,kBAAkB,CAKhC","sourcesContent":["import { Parser } from 'web-tree-sitter';\nimport type { Node, Tree } from 'web-tree-sitter';\n\nimport { GrammarUnavailableError, MissingMarkerPricingError } from '../errors.ts';\nimport type { LanguageStructure } from '../lang/profile.ts';\nimport { profileFor, structuralLanguages } from '../lang/registry.ts';\nimport type {\n ElisionPlan,\n LanguageId,\n MarkerPricing,\n PlanInput,\n PlannedElision,\n Planner,\n} from '../types.ts';\n\nimport { predictOutputBytes, savingBytes } from './budget.ts';\nimport { loadGrammar } from './grammar.ts';\n\nexport const STRUCTURAL_PLANNER_ID = 'structural/v1';\n\n/**\n * The languages this planner actually parses — a derived view of the registry: every\n * {@link LanguageProfile} that carries a `structure` section, in registry order. A\n * language appears here only once its node kinds are mapped in its profile\n * (`src/lang/<id>.ts`), because claiming a language before that would produce markers\n * that mislabel what they collapsed.\n *\n * Exported for the totality guard (`test/guards/structural-totality.test.ts`): every\n * language named here must have a fixture, a snapshot and a doc-comment case, so a\n * language cannot be claimed without tests.\n */\nexport const STRUCTURAL_LANGUAGES: readonly LanguageId[] = structuralLanguages();\n\n/**\n * Historically the union of the claimed ids; every `LanguageId` carries a profile\n * now, so the compile-time totality this type enforced lives on the registry\n * (`Record<LanguageId, LanguageProfile>`) instead.\n */\nexport type StructuralLanguage = LanguageId;\n\n/**\n * Every elision this planner produces carries this rule id, and the profitability\n * check below prices the marker that rule would earn — so the two must not drift.\n */\nconst SIBLING_COLLAPSE_RULE = 'sibling-collapse';\n\nexport interface StructuralPlannerOptions {\n /**\n * Never collapse a sibling group smaller than this. Defaults to 1 — the byte\n * profitability check already refuses collapses that would not pay for their marker.\n */\n readonly minSiblings?: number;\n /** Focus matching is substring, case-insensitive by default — same as lexical. */\n readonly caseSensitive?: boolean;\n}\n\n/**\n * One top-level declaration, together with the doc comment attached to it. The unit is\n * the atom of every decision here: a unit is kept whole or collapsed whole, which is\n * what makes \"a kept declaration keeps its signature line and attached doc comment\"\n * true by construction rather than by patching ranges afterwards.\n *\n * All positions are UTF-16 code-unit indices (what web-tree-sitter reports for a JS\n * string); they are converted to UTF-8 byte offsets in one place, at the end.\n */\ninterface Unit {\n /** Start of the unit — the first attached comment or attribute if there is one. */\n readonly start: number;\n /** End of the declaration node. */\n readonly end: number;\n /** Human word for the declaration's kind, e.g. `'function'`. */\n readonly kind: string;\n /**\n * A unit the planner must never collapse, matched or not. Every pin follows one\n * law: collapsing it would silently change what the survivor *is*, not what it\n * contains. Go's `//go:build` constraint governs which builds see the whole file;\n * shebang lines (bash, ruby, python as comments; javascript, typescript, tsx,\n * kotlin and swift as their grammars' own shebang nodes) and ruby's\n * `# frozen_string_literal:` magic comment govern how the file is executed at all;\n * php's `<?php` open tag is what makes the rest of the file php; and c/c++'s\n * `#pragma once` governs what including the file means.\n */\n readonly pinned?: boolean;\n}\n\n/**\n * The structural planner — the reason smelt exists.\n *\n * It parses with the language's tree-sitter grammar, finds the top-level declarations\n * whose text matches the caller's focus, keeps each match whole — signature line,\n * attached doc comment, body — and collapses each contiguous run of non-matching\n * *siblings* into one marker that names them from the parse tree: `collapsed 3 sibling\n * functions`. Structure is what makes that explanation possible; a line window can only\n * ever say \"collapsed 40 lines\".\n *\n * It throws {@link GrammarUnavailableError} rather than falling back to the lexical\n * planner, and that is the deliberate part. A silent fallback would mean a caller who\n * asked for structural planning, and whose grammar failed to load, gets line-window\n * output labelled `structural/v1` — plausible, wrong, and undetectable from the\n * outside. A caller who wants the fallback asks for it, by planning lexically itself.\n */\nexport class StructuralPlanner implements Planner {\n readonly id = STRUCTURAL_PLANNER_ID;\n readonly options: StructuralPlannerOptions;\n\n constructor(options: StructuralPlannerOptions = {}) {\n this.options = options;\n }\n\n plan(input: PlanInput): Promise<ElisionPlan> {\n return planStructural(input, this.options);\n }\n}\n\n/**\n * The planner as a function, exported like {@link planLexical} so it can be tested and\n * reused directly. Deterministic: same text, same language, same focus, same options —\n * byte-identical plan.\n *\n * `budgetBytes` is a target, not a guarantee. The plan is built in two passes: every\n * maximal run of non-matching siblings is collapsed where that pays for its marker,\n * and then — only if the plan is still over budget — each run the first pass refused\n * is re-asked as its own best profitable sub-run (the budget rung, in\n * {@link planFromTree}). Under the last of those there is nothing left to cut except\n * the declarations the caller asked to keep, and an optimizer that silently drops the\n * thing you searched for is the exact failure this design refuses. Callers who need a\n * hard ceiling check `outputBytes` and decide.\n *\n * @throws {GrammarUnavailableError} when the language is not one this planner parses,\n * when the grammar cannot be loaded, or when the parser produces no tree. Never a\n * lexical fallback.\n */\nexport async function planStructural(\n input: PlanInput,\n options: StructuralPlannerOptions = {},\n): Promise<ElisionPlan> {\n const language = assertStructuralLanguage(input.language);\n // The runtime backstop for JS callers: TypeScript makes `pricing` required, but a\n // JS caller can omit it, and the honest answer is a named refusal rather than the\n // planner quietly pricing markers itself — the inversion the seam removed.\n const pricing: MarkerPricing | undefined = input.pricing;\n if (pricing === undefined) throw new MissingMarkerPricingError(STRUCTURAL_PLANNER_ID);\n const grammar = await loadGrammar(language);\n\n const parser = new Parser();\n let tree: Tree | null = null;\n try {\n parser.setLanguage(grammar);\n tree = parser.parse(input.text);\n if (tree === null) {\n throw new GrammarUnavailableError(\n `smelt: the ${language} parser returned no tree. Structural planning cannot ` +\n `proceed, and it does not fall back to lexical — a caller who wants the ` +\n `fallback plans lexically itself.`,\n );\n }\n return {\n planner: STRUCTURAL_PLANNER_ID,\n language: input.language,\n elisions: planFromTree(tree, input, options, language),\n };\n } finally {\n tree?.delete();\n parser.delete();\n }\n}\n\nfunction assertStructuralLanguage(language: PlanInput['language']): StructuralLanguage {\n if (isStructuralLanguage(language)) return language;\n const named = `${STRUCTURAL_LANGUAGES.slice(0, -1).join(', ')} and ${\n STRUCTURAL_LANGUAGES[STRUCTURAL_LANGUAGES.length - 1]\n }`;\n throw new GrammarUnavailableError(\n `smelt: structural planning covers ${named} in this ` +\n `slice; got \"${language}\". It does not fall back to the lexical planner — output ` +\n `labelled structural/v1 that is really line windows would be undetectable from ` +\n `the outside. Use \\`strategy: 'auto'\\` for a mixed stream that is sometimes not ` +\n `code — it picks the planner from the language and labels the one it ran — or ` +\n `the lexical planner explicitly if that is what you want.`,\n );\n}\n\n/**\n * Whether this planner has a bundled grammar for the language — the one membership\n * test, shared with the `auto` strategy (`plan/auto.ts`).\n *\n * The refusal above and auto's selection are the same question asked for opposite\n * reasons, and two spellings of it would be two answers: a language auto routed to\n * `structural` that `assertStructuralLanguage` then refused would be a\n * `GrammarUnavailableError` raised by the strategy whose entire job is not raising\n * one.\n */\nexport function isStructuralLanguage(\n language: PlanInput['language'],\n): language is StructuralLanguage {\n for (const supported of STRUCTURAL_LANGUAGES) {\n if (language === supported) return true;\n }\n return false;\n}\n\nfunction planFromTree(\n tree: Tree,\n input: PlanInput,\n options: StructuralPlannerOptions,\n language: StructuralLanguage,\n): readonly PlannedElision[] {\n const profile = profileFor(language);\n // assertStructuralLanguage admits only languages whose profile carries a\n // `structure` section — that is the definition of a structural language — so the\n // assertion states a fact the seam already proved.\n const structure = profile.structure!;\n const pricing = input.pricing;\n // The marker lands as a line comment in every structural language (the profile's\n // markerLeader), and a line comment comments out everything to the end of its\n // line — so a collapse is only legal where nothing kept follows on the marker's\n // own line. See the flush below.\n const markerIsLineComment = profile.markerLeader !== undefined;\n const units = unitsOf(tree.rootNode, input.text, structure);\n const matched = matchUnits(units, input, options);\n const minSiblings = options.minSiblings ?? 1;\n const toByte = utf8OffsetIndex(\n input.text,\n units.flatMap((unit) => [unit.start, unit.end]),\n );\n\n /**\n * One contiguous group of siblings as an elision — or nothing, when collapsing it\n * would be illegal or would not pay for its marker. Every cut in this planner,\n * first pass and budget rung alike, is minted here: one legality rule, one price,\n * one explanation, so the rung cannot cut something the first pass would refuse.\n */\n const collapse = (group: readonly Unit[]): PlannedElision | undefined => {\n if (group.length === 0 || group.length < minSiblings) return undefined;\n // A line-comment marker swallows the rest of its line. When the group's last unit\n // ends mid-line — python's `stmt_a(); stmt_b()` puts two top-level statements on\n // one line — the marker's `# ` leader would comment out the *kept* code after it.\n // Refusing the collapse is the honest move; extending the range would elide code\n // no unit accounted for.\n if (markerIsLineComment && !restOfLineIsBlank(input.text, group[group.length - 1]!.end)) {\n return undefined;\n }\n const start = toByte.get(group[0]!.start)!;\n const end = toByte.get(group[group.length - 1]!.end)!;\n const candidate: PlannedElision = {\n range: { start, end },\n reason: { rule: SIBLING_COLLAPSE_RULE, explanation: explain(group) },\n };\n // Profitability, priced rather than estimated: ask the MarkerPricing seam for the\n // exact cost of the marker this cut would earn — the explanation's length varies\n // with kind diversity, and the pricing carries the language's comment leader (or a\n // caller's custom builder), so a fixed estimate can pass a cut whose marker is\n // bigger than what it removes, and a marker that costs more than it removes grows\n // the output.\n return savingBytes(candidate, pricing) > 0 ? candidate : undefined;\n };\n\n /**\n * The UTF-8 bytes a sub-run `[from, to)` spans — read off the same offset index\n * `collapse` mints ranges from, so it is the exact upper bound on what collapsing\n * that sub-run could save, in O(1) and without slicing. The budget rung's span\n * bound depends on it being both exact and cheap.\n */\n const spanBytes = (group: readonly Unit[], from: number, to: number): number =>\n toByte.get(group[to - 1]!.end)! - toByte.get(group[from]!.start)!;\n\n // The first pass: every maximal run of siblings no focus term matched and no rule\n // pinned, collapsed whole. A run that earns no cut is remembered, not forgotten —\n // the budget rung below is the only reader of that list.\n const elisions: PlannedElision[] = [];\n const refused: (readonly Unit[])[] = [];\n for (const group of maximalRuns(units, matched)) {\n const cut = collapse(group);\n if (cut === undefined) refused.push(group);\n else elisions.push(cut);\n }\n\n // THE BUDGET RUNG — the structural answer to the lexical planner's context ladder.\n //\n // Until this existed, `planStructural` never read `input.budgetBytes` at all: it\n // took every profitable maximal run and stopped, which is right until the plan\n // comes back over budget with a profitable cut still on the table. The review's\n // case: 158 bytes, a budget of 120, and a run of three sibling classes worth 92\n // bytes left whole — because the *maximal* run also swallowed a `VERSION = 1`\n // statement above them, and a mixed-kind explanation (\"1 statement, 3 classes\")\n // priced a 106-byte marker against a 105-byte cut. The planner returned over budget\n // having elided nothing, while the three classes alone priced an 82-byte marker\n // against 92 bytes — a real saving of 10 bytes nobody was offered.\n //\n // So, and only when the plan is still over budget, each refused run is re-asked as\n // its own best profitable sub-run, earliest run first, stopping the moment the plan\n // fits. Four properties hold by construction rather than by care:\n //\n // - **A focus match is never cut.** A matched unit is not in any run, so no\n // sub-run of any run can contain one. Same for a pinned unit.\n // - **The output never grows.** Every candidate goes through `collapse`, which\n // mints nothing whose marker is not strictly cheaper than the bytes it removes.\n // - **It stays deterministic.** Sub-runs are enumerated start-ascending and\n // length-descending, and a tie in saving keeps the one already held — the\n // earliest start, then the longest. Both of the rung's bounds read that same\n // order, so a bounded sweep and an exhaustive one answer alike.\n // - **It stays bounded.** A refused run can be the whole file — the mid-line\n // refusal is about one character, not about size — so the sweep stops at the\n // first cut that fits the budget and skips every candidate whose span already\n // proves it cannot win. See {@link bestSubRun}.\n //\n // Budget pressure is the trigger, not profitability, because a maximal run is the\n // better *explanation*: one marker naming everything it hid beats two naming halves\n // of it. Trading that away is worth it to fit a budget and not worth it otherwise.\n const inputBytes = Buffer.byteLength(input.text, 'utf8');\n for (const group of refused) {\n const currentBytes = predictOutputBytes(inputBytes, elisions, pricing);\n if (currentBytes <= input.budgetBytes) break;\n // What this run would have to save to land the plan under budget — the sweep's\n // stopping condition, so a run that can fit the budget is priced a handful of\n // times instead of exhaustively. See {@link bestSubRun}.\n const enough = currentBytes - input.budgetBytes;\n const cut = bestSubRun(group, collapse, pricing, spanBytes, enough);\n if (cut !== undefined) elisions.push(cut);\n }\n\n return elisions.toSorted((a, b) => a.range.start - b.range.start);\n}\n\n/**\n * The runs the first pass collapses: every maximal contiguous group of units that no\n * focus term matched and no rule pinned. A matched or pinned unit ends the run before\n * it and starts none — which is what makes \"never cut a focus-matched declaration\" a\n * property of the *shape* of the data rather than a check anyone has to remember.\n */\nfunction maximalRuns(\n units: readonly Unit[],\n matched: readonly boolean[],\n): readonly (readonly Unit[])[] {\n const runs: Unit[][] = [];\n let run: Unit[] = [];\n for (let i = 0; i < units.length; i += 1) {\n if (matched[i] === true || units[i]!.pinned === true) {\n if (run.length > 0) runs.push(run);\n run = [];\n } else {\n run.push(units[i]!);\n }\n }\n if (run.length > 0) runs.push(run);\n return runs;\n}\n\n/**\n * The best cut available inside one refused run: the contiguous sub-run whose real\n * marker is cheapest against the bytes it removes.\n *\n * The enumeration is bounded, and it has to be. A run reaches here refused for any of\n * three reasons, and only one of them is \"it lost to a ~100-byte marker\": a run is also\n * refused when it holds fewer than `minSiblings` units, and — the one that bites —\n * when its last unit ends mid-line, because a line-comment marker would swallow the\n * kept code after it. That last rule is a property of one character in the file, not of\n * the run's size, so a *whole-file* run can land here: one bash script whose last line\n * is `cleanup_tmp; deploy_release --now` refuses its only maximal run, and an\n * unbounded `(from, to)` sweep over its thousands of units — each candidate slicing,\n * explaining and pricing — is cubic. Measured, before this bound: 19.5 KB / 373 ms,\n * 39 KB / 2.3 s, 79 KB / 17.2 s, 159 KB / 131 s. That is a CPU loop on tool-controlled\n * input, on the path the caller takes precisely when it is over budget — the normal\n * case. Two bounds, neither of which changes which cut a bounded sweep would have\n * found by exhaustion:\n *\n * - **`enough`** — the saving that lands the plan under budget. The rung exists to\n * fit a budget; the first candidate that does is the answer, and enumeration order\n * (start-ascending, length-descending) makes it the earliest, longest such cut.\n * This is what turns the whole-file case into two priced candidates.\n * - **The span bound** — a cut can never save more than the bytes it spans, so once\n * a candidate's span has shrunk to `bestSaving` or below, neither it nor any\n * shorter candidate from the same start can win, and once the *longest* candidate\n * from a start is that small, no later start can win either. Spans shrink\n * monotonically along both loops, so both breaks are exact, not heuristic: they\n * skip only candidates already proven to lose.\n */\nfunction bestSubRun(\n group: readonly Unit[],\n collapse: (group: readonly Unit[]) => PlannedElision | undefined,\n pricing: MarkerPricing,\n spanBytes: (group: readonly Unit[], from: number, to: number) => number,\n enough: number,\n): PlannedElision | undefined {\n let best: PlannedElision | undefined;\n let bestSaving = 0;\n for (let from = 0; from < group.length; from += 1) {\n if (spanBytes(group, from, group.length) <= bestSaving) break;\n for (let to = group.length; to > from; to -= 1) {\n if (spanBytes(group, from, to) <= bestSaving) break;\n const candidate = collapse(group.slice(from, to));\n if (candidate === undefined) continue;\n const saving = savingBytes(candidate, pricing);\n if (saving > bestSaving) {\n best = candidate;\n bestSaving = saving;\n if (saving >= enough) return best;\n }\n }\n }\n return best;\n}\n\n/** True when nothing but whitespace follows `index` on its own line. */\nfunction restOfLineIsBlank(text: string, index: number): boolean {\n for (let i = index; i < text.length; i += 1) {\n const ch = text[i]!;\n if (ch === '\\n') return true;\n if (!/\\s/.test(ch)) return false;\n }\n return true;\n}\n\n/**\n * Group the root's named children into units: each declaration plus the comment block\n * — and, in rust, the outer attributes — attached to it. A comment attaches when only\n * blank-free whitespace separates it from what follows — one newline at most — so a doc\n * comment travels with its declaration, while a comment left floating above a blank\n * line stands alone.\n *\n * Attributes attach *unconditionally*: tree-sitter-rust parses `#[inline]` as a\n * top-level sibling of the item it decorates, but the language's own rule is that an\n * outer attribute modifies the next item, blank lines or not. So once an attribute is\n * pending, the whole pending prefix (doc comments included) rides forward to the next\n * declaration — treating it as its own unit would let a collapse strip `#[derive(…)]`\n * and the doc comment above it off a kept declaration.\n */\nfunction unitsOf(root: Node, text: string, structure: LanguageStructure): readonly Unit[] {\n const units: Unit[] = [];\n /** Comments and attributes waiting to attach to the next declaration. */\n let pending: Node[] = [];\n /** Once true, blank lines no longer detach the pending prefix — see above. */\n let pendingHasAttribute = false;\n\n const flushPending = (): void => {\n if (pending.length === 0) return;\n units.push({\n start: pending[0]!.startIndex,\n end: pending[pending.length - 1]!.endIndex,\n kind: pendingHasAttribute ? 'attribute' : 'comment',\n pinned: !pendingHasAttribute && pending.some((node) => pinnedComment(node, text, structure)),\n });\n pending = [];\n pendingHasAttribute = false;\n };\n\n for (const child of root.namedChildren) {\n if (child === null) continue;\n if (structure.ridesBackwardTypes?.has(child.type) === true && units.length > 0) {\n // A node that is grammatically part of the *previous* statement, parsed as a\n // top-level sibling: tree-sitter-ruby emits a heredoc's body as a sibling of\n // the statement holding its `<<~SQL` opener. Splitting them lets a collapse\n // keep the opener and cut the body — an unterminated heredoc that swallows\n // every kept declaration after it, without a single ERROR node to show for\n // it. The body extends the preceding unit instead, so opener and body are\n // kept whole or collapsed whole, always.\n flushPending();\n const last = units[units.length - 1]!;\n units[units.length - 1] = { ...last, end: child.endIndex };\n continue;\n }\n if (\n structure.pinnedTypes?.has(child.type) === true ||\n pinnedDirective(child, text, structure)\n ) {\n // A pinned node (php's `<?php` tag, a `#!` shebang line, c's `#pragma once`)\n // is its own uncollapsible unit — nothing attaches to it, and no run may\n // swallow it.\n flushPending();\n units.push({\n start: child.startIndex,\n end: child.endIndex,\n kind: kindOf(child, structure),\n pinned: true,\n });\n continue;\n }\n const isComment = structure.commentTypes.has(child.type);\n const isAttribute = structure.attributeTypes.has(child.type);\n if (isComment || isAttribute) {\n if (\n pending.length > 0 &&\n !pendingHasAttribute &&\n !adjacent(text, pending[pending.length - 1]!.endIndex, child.startIndex)\n ) {\n flushPending();\n }\n pending.push(child);\n pendingHasAttribute ||= isAttribute;\n continue;\n }\n\n // tree-sitter-kotlin extends the import_list node over a doc comment that\n // follows it, so the KDoc of the first documented declaration after the imports\n // would be collapsed *with the imports* — a kept declaration losing its attached\n // doc comment. Split such trailing comments off: the unit ends at its last\n // non-comment token, and the comments ride forward like any other pending block.\n const { end: childEnd, trailing } =\n structure.trailingCommentSplitTypes?.has(child.type) === true\n ? splitTrailingComments(child, structure)\n : { end: child.endIndex, trailing: [] };\n\n const attached =\n pending.length > 0 &&\n (pendingHasAttribute ||\n adjacent(text, pending[pending.length - 1]!.endIndex, child.startIndex));\n if (attached && pending.some((node) => pinnedComment(node, text, structure))) {\n // A pinned comment (`//go:build`) governs the file, not the declaration it\n // happens to touch — keep it a standalone, uncollapsible unit either way.\n flushPending();\n units.push({\n start: child.startIndex,\n end: childEnd,\n kind: kindOf(child, structure),\n });\n } else if (attached) {\n units.push({\n start: pending[0]!.startIndex,\n end: childEnd,\n kind: kindOf(child, structure),\n });\n pending = [];\n pendingHasAttribute = false;\n } else {\n flushPending();\n units.push({\n start: child.startIndex,\n end: childEnd,\n kind: kindOf(child, structure),\n });\n }\n for (const comment of trailing) pending.push(comment);\n }\n flushPending();\n return units;\n}\n\n/** Whether this comment node is one the language pins to the file. See {@link Unit}. */\nfunction pinnedComment(node: Node, text: string, structure: LanguageStructure): boolean {\n if (structure.pinnedCommentPattern === undefined) return false;\n if (!structure.commentTypes.has(node.type)) return false;\n return structure.pinnedCommentPattern.test(text.slice(node.startIndex, node.endIndex));\n}\n\n/**\n * Whether this non-comment node is pinned by a per-type text pattern. C's\n * `#pragma once` is a `preproc_call` like any other pragma, but collapsing it\n * silently changes header inclusion semantics — the same class as `//go:build`, on a\n * node kind a comment pattern cannot reach.\n */\nfunction pinnedDirective(node: Node, text: string, structure: LanguageStructure): boolean {\n const pattern = structure.pinnedPatternsByType?.[node.type];\n if (pattern === undefined) return false;\n return pattern.test(text.slice(node.startIndex, node.endIndex));\n}\n\n/**\n * The end of `node`'s last non-comment token, and every comment node after it. Used\n * for node types the grammar extends over comments that belong to what follows —\n * kotlin's import_list swallowing the next declaration's KDoc.\n */\nfunction splitTrailingComments(\n node: Node,\n structure: LanguageStructure,\n): { readonly end: number; readonly trailing: readonly Node[] } {\n const comments: Node[] = [];\n let end = node.startIndex;\n const walk = (current: Node): void => {\n if (structure.commentTypes.has(current.type)) {\n comments.push(current);\n return;\n }\n let hasChildren = false;\n for (const child of current.children) {\n if (child === null) continue;\n hasChildren = true;\n walk(child);\n }\n if (!hasChildren && current.endIndex > end) end = current.endIndex;\n };\n for (const child of node.children) {\n if (child !== null) walk(child);\n }\n return { end, trailing: comments.filter((comment) => comment.startIndex >= end) };\n}\n\n/** Nothing but whitespace between the two indices, and at most one newline. */\nfunction adjacent(text: string, end: number, start: number): boolean {\n const between = text.slice(end, start);\n if (!/^\\s*$/.test(between)) return false;\n return (between.match(/\\n/g) ?? []).length <= 1;\n}\n\nfunction matchUnits(\n units: readonly Unit[],\n input: PlanInput,\n options: StructuralPlannerOptions,\n): readonly boolean[] {\n const caseSensitive = options.caseSensitive ?? false;\n const focus = (input.focus ?? []).filter((term) => term.length > 0);\n const needles = caseSensitive ? focus : focus.map((term) => term.toLowerCase());\n\n return units.map((unit) => {\n if (needles.length === 0) return false;\n const raw = input.text.slice(unit.start, unit.end);\n const haystack = caseSensitive ? raw : raw.toLowerCase();\n return needles.some((needle) => haystack.includes(needle));\n });\n}\n\n/**\n * Law 2, for this rule: the explanation names the *kind* and the *count*, read off the\n * parse tree — `collapsed 3 sibling functions` — never a line count as the claim. A\n * mixed run says what it mixed: `collapsed 4 sibling declarations (3 functions,\n * 1 class)` — and when the run holds anything that is not a declaration (a statement,\n * a floating comment, an unparsed region), the heading says `nodes`, because calling\n * a parse error a declaration would be the marker lying about the tree.\n */\nfunction explain(group: readonly Unit[]): string {\n const counts = new Map<string, number>();\n for (const unit of group) counts.set(unit.kind, (counts.get(unit.kind) ?? 0) + 1);\n\n const total = group.length;\n if (counts.size === 1) {\n const kind = group[0]!.kind;\n return `collapsed ${String(total)} sibling ${countNoun(kind, total)}`;\n }\n const parts = [...counts.entries()].map(\n ([kind, count]) => `${String(count)} ${countNoun(kind, count)}`,\n );\n const heading = [...counts.keys()].every((kind) => !NON_DECLARATION_KINDS.has(kind))\n ? 'declarations'\n : 'nodes';\n return `collapsed ${String(total)} sibling ${heading} (${parts.join(', ')})`;\n}\n\n/** The kind labels that are not declarations, so a mixed heading never overclaims. */\nconst NON_DECLARATION_KINDS: ReadonlySet<string> = new Set([\n 'statement',\n 'comment',\n 'unparsed region',\n 'package clause',\n 'package header',\n 'package declaration',\n 'attribute',\n 'command',\n 'variable assignment',\n 'include directive',\n 'shebang',\n 'php tag',\n 'html section',\n 'heredoc body',\n 'preprocessor directive',\n 'preprocessor conditional',\n]);\n\nfunction countNoun(kind: string, count: number): string {\n if (count === 1) return kind;\n if (/[^aeiou]y$/.test(kind)) return `${kind.slice(0, -1)}ies`;\n return /(?:s|sh|ch)$/.test(kind) ? `${kind}es` : `${kind}s`;\n}\n\n/**\n * The kind of a declaration, unwrapping wrappers (`export …`, `@decorator`) so the\n * marker names what it hides. What the language's map does not name is still labelled\n * honestly — see {@link LanguageStructure.kindLabels} for the labelling rules.\n */\nfunction kindOf(node: Node, structure: LanguageStructure): string {\n const wrapperFallback = structure.wrapperTypes[node.type];\n if (wrapperFallback !== undefined) {\n for (const child of node.namedChildren) {\n if (child === null || structure.commentTypes.has(child.type)) continue;\n const label = structure.kindLabels[child.type];\n if (label !== undefined) return label;\n }\n return wrapperFallback;\n }\n const label = structure.kindLabels[node.type];\n if (label !== undefined) return label;\n if (node.type === 'ERROR') return 'unparsed region';\n if (node.type.endsWith('_statement') || node.type === 'statement_block') return 'statement';\n return 'declaration';\n}\n\n/**\n *\n * web-tree-sitter reports positions in code units of the JS string it parsed;\n * {@link ElisionPlan} ranges are UTF-8 bytes. Because every converted index is a parse\n * node boundary, a range can never split a multi-byte character — a node boundary is\n * always a character boundary. One forward pass, so the conversion is linear.\n */\nfunction utf8OffsetIndex(text: string, indices: readonly number[]): ReadonlyMap<number, number> {\n const sorted = [...new Set(indices)].toSorted((a, b) => a - b);\n const map = new Map<number, number>();\n let previousIndex = 0;\n let previousByte = 0;\n for (const index of sorted) {\n previousByte += Buffer.byteLength(text.slice(previousIndex, index), 'utf8');\n previousIndex = index;\n map.set(index, previousByte);\n }\n return map;\n}\n"]}