@smeltjs/core 0.1.0 → 0.3.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 (319) hide show
  1. package/README.md +4 -4
  2. package/dist/apply.d.ts.map +1 -1
  3. package/dist/apply.js.map +1 -1
  4. package/dist/cache/prefix.d.ts.map +1 -1
  5. package/dist/cache/prefix.js.map +1 -1
  6. package/dist/cli/args.d.ts +16 -59
  7. package/dist/cli/args.d.ts.map +1 -1
  8. package/dist/cli/args.js +18 -277
  9. package/dist/cli/args.js.map +1 -1
  10. package/dist/cli/bin.d.ts.map +1 -1
  11. package/dist/cli/bin.js.map +1 -1
  12. package/dist/cli/config.d.ts +62 -0
  13. package/dist/cli/config.d.ts.map +1 -1
  14. package/dist/cli/config.js +87 -2
  15. package/dist/cli/config.js.map +1 -1
  16. package/dist/cli/hooks.d.ts +148 -0
  17. package/dist/cli/hooks.d.ts.map +1 -0
  18. package/dist/cli/hooks.js +835 -0
  19. package/dist/cli/hooks.js.map +1 -0
  20. package/dist/cli/init.d.ts +30 -13
  21. package/dist/cli/init.d.ts.map +1 -1
  22. package/dist/cli/init.js +173 -38
  23. package/dist/cli/init.js.map +1 -1
  24. package/dist/cli/report.d.ts.map +1 -1
  25. package/dist/cli/report.js +2 -2
  26. package/dist/cli/report.js.map +1 -1
  27. package/dist/cli/run.d.ts +25 -88
  28. package/dist/cli/run.d.ts.map +1 -1
  29. package/dist/cli/run.js +32 -322
  30. package/dist/cli/run.js.map +1 -1
  31. package/dist/cli/shell.d.ts +106 -0
  32. package/dist/cli/shell.d.ts.map +1 -0
  33. package/dist/cli/shell.js +100 -0
  34. package/dist/cli/shell.js.map +1 -0
  35. package/dist/cli/subcommands/flags.d.ts +141 -0
  36. package/dist/cli/subcommands/flags.d.ts.map +1 -0
  37. package/dist/cli/subcommands/flags.js +208 -0
  38. package/dist/cli/subcommands/flags.js.map +1 -0
  39. package/dist/cli/subcommands/hooks.d.ts +18 -0
  40. package/dist/cli/subcommands/hooks.d.ts.map +1 -0
  41. package/dist/cli/subcommands/hooks.js +64 -0
  42. package/dist/cli/subcommands/hooks.js.map +1 -0
  43. package/dist/cli/subcommands/init.d.ts +19 -0
  44. package/dist/cli/subcommands/init.d.ts.map +1 -0
  45. package/dist/cli/subcommands/init.js +44 -0
  46. package/dist/cli/subcommands/init.js.map +1 -0
  47. package/dist/cli/subcommands/map.d.ts +75 -0
  48. package/dist/cli/subcommands/map.d.ts.map +1 -0
  49. package/dist/cli/subcommands/map.js +132 -0
  50. package/dist/cli/subcommands/map.js.map +1 -0
  51. package/dist/cli/subcommands/registry.d.ts +80 -0
  52. package/dist/cli/subcommands/registry.d.ts.map +1 -0
  53. package/dist/cli/subcommands/registry.js +138 -0
  54. package/dist/cli/subcommands/registry.js.map +1 -0
  55. package/dist/cli/subcommands/retrieve.d.ts +74 -0
  56. package/dist/cli/subcommands/retrieve.d.ts.map +1 -0
  57. package/dist/cli/subcommands/retrieve.js +84 -0
  58. package/dist/cli/subcommands/retrieve.js.map +1 -0
  59. package/dist/cli/subcommands/smelt.d.ts +107 -0
  60. package/dist/cli/subcommands/smelt.d.ts.map +1 -0
  61. package/dist/cli/subcommands/smelt.js +304 -0
  62. package/dist/cli/subcommands/smelt.js.map +1 -0
  63. package/dist/cli/subcommands/stats.d.ts +43 -0
  64. package/dist/cli/subcommands/stats.d.ts.map +1 -0
  65. package/dist/cli/subcommands/stats.js +62 -0
  66. package/dist/cli/subcommands/stats.js.map +1 -0
  67. package/dist/cli/subcommands/subcommand.d.ts +113 -0
  68. package/dist/cli/subcommands/subcommand.d.ts.map +1 -0
  69. package/dist/cli/subcommands/subcommand.js +3 -0
  70. package/dist/cli/subcommands/subcommand.js.map +1 -0
  71. package/dist/cli/usage.d.ts +3 -0
  72. package/dist/cli/usage.d.ts.map +1 -0
  73. package/dist/cli/usage.js +95 -0
  74. package/dist/cli/usage.js.map +1 -0
  75. package/dist/detect.d.ts.map +1 -1
  76. package/dist/detect.js.map +1 -1
  77. package/dist/errors.d.ts +21 -1
  78. package/dist/errors.d.ts.map +1 -1
  79. package/dist/errors.js +33 -1
  80. package/dist/errors.js.map +1 -1
  81. package/dist/harness/aider.d.ts +10 -0
  82. package/dist/harness/aider.d.ts.map +1 -0
  83. package/dist/harness/aider.js +21 -0
  84. package/dist/harness/aider.js.map +1 -0
  85. package/dist/harness/claude-code.d.ts +3 -0
  86. package/dist/harness/claude-code.d.ts.map +1 -0
  87. package/dist/harness/claude-code.js +68 -0
  88. package/dist/harness/claude-code.js.map +1 -0
  89. package/dist/harness/cline.d.ts +3 -0
  90. package/dist/harness/cline.d.ts.map +1 -0
  91. package/dist/harness/cline.js +54 -0
  92. package/dist/harness/cline.js.map +1 -0
  93. package/dist/harness/codex.d.ts +3 -0
  94. package/dist/harness/codex.d.ts.map +1 -0
  95. package/dist/harness/codex.js +91 -0
  96. package/dist/harness/codex.js.map +1 -0
  97. package/dist/harness/cursor.d.ts +3 -0
  98. package/dist/harness/cursor.d.ts.map +1 -0
  99. package/dist/harness/cursor.js +54 -0
  100. package/dist/harness/cursor.js.map +1 -0
  101. package/dist/harness/gemini.d.ts +3 -0
  102. package/dist/harness/gemini.d.ts.map +1 -0
  103. package/dist/harness/gemini.js +59 -0
  104. package/dist/harness/gemini.js.map +1 -0
  105. package/dist/harness/grok.d.ts +3 -0
  106. package/dist/harness/grok.d.ts.map +1 -0
  107. package/dist/harness/grok.js +46 -0
  108. package/dist/harness/grok.js.map +1 -0
  109. package/dist/harness/hermes.d.ts +3 -0
  110. package/dist/harness/hermes.d.ts.map +1 -0
  111. package/dist/harness/hermes.js +71 -0
  112. package/dist/harness/hermes.js.map +1 -0
  113. package/dist/harness/kilocode.d.ts +3 -0
  114. package/dist/harness/kilocode.d.ts.map +1 -0
  115. package/dist/harness/kilocode.js +35 -0
  116. package/dist/harness/kilocode.js.map +1 -0
  117. package/dist/harness/opencode.d.ts +3 -0
  118. package/dist/harness/opencode.d.ts.map +1 -0
  119. package/dist/harness/opencode.js +97 -0
  120. package/dist/harness/opencode.js.map +1 -0
  121. package/dist/harness/paths.d.ts +17 -0
  122. package/dist/harness/paths.d.ts.map +1 -0
  123. package/dist/harness/paths.js +45 -0
  124. package/dist/harness/paths.js.map +1 -0
  125. package/dist/harness/profile.d.ts +184 -0
  126. package/dist/harness/profile.d.ts.map +1 -0
  127. package/dist/harness/profile.js +26 -0
  128. package/dist/harness/profile.js.map +1 -0
  129. package/dist/harness/registry.d.ts +51 -0
  130. package/dist/harness/registry.d.ts.map +1 -0
  131. package/dist/harness/registry.js +87 -0
  132. package/dist/harness/registry.js.map +1 -0
  133. package/dist/harness/snippet.d.ts +19 -0
  134. package/dist/harness/snippet.d.ts.map +1 -0
  135. package/dist/harness/snippet.js +41 -0
  136. package/dist/harness/snippet.js.map +1 -0
  137. package/dist/hash.d.ts.map +1 -1
  138. package/dist/hash.js.map +1 -1
  139. package/dist/hooks/guard-core.d.ts +164 -0
  140. package/dist/hooks/guard-core.d.ts.map +1 -0
  141. package/dist/hooks/guard-core.js +491 -0
  142. package/dist/hooks/guard-core.js.map +1 -0
  143. package/dist/hooks/shim.d.ts +176 -0
  144. package/dist/hooks/shim.d.ts.map +1 -0
  145. package/dist/hooks/shim.js +192 -0
  146. package/dist/hooks/shim.js.map +1 -0
  147. package/dist/hooks/shims/claude-code.d.ts +10 -0
  148. package/dist/hooks/shims/claude-code.d.ts.map +1 -0
  149. package/dist/hooks/shims/claude-code.js +14 -0
  150. package/dist/hooks/shims/claude-code.js.map +1 -0
  151. package/dist/hooks/shims/cline.d.ts +10 -0
  152. package/dist/hooks/shims/cline.d.ts.map +1 -0
  153. package/dist/hooks/shims/cline.js +14 -0
  154. package/dist/hooks/shims/cline.js.map +1 -0
  155. package/dist/hooks/shims/codex.d.ts +10 -0
  156. package/dist/hooks/shims/codex.d.ts.map +1 -0
  157. package/dist/hooks/shims/codex.js +14 -0
  158. package/dist/hooks/shims/codex.js.map +1 -0
  159. package/dist/hooks/shims/cursor.d.ts +10 -0
  160. package/dist/hooks/shims/cursor.d.ts.map +1 -0
  161. package/dist/hooks/shims/cursor.js +14 -0
  162. package/dist/hooks/shims/cursor.js.map +1 -0
  163. package/dist/hooks/shims/gemini.d.ts +10 -0
  164. package/dist/hooks/shims/gemini.d.ts.map +1 -0
  165. package/dist/hooks/shims/gemini.js +14 -0
  166. package/dist/hooks/shims/gemini.js.map +1 -0
  167. package/dist/hooks/shims/grok.d.ts +10 -0
  168. package/dist/hooks/shims/grok.d.ts.map +1 -0
  169. package/dist/hooks/shims/grok.js +14 -0
  170. package/dist/hooks/shims/grok.js.map +1 -0
  171. package/dist/hooks/shims/hermes.d.ts +10 -0
  172. package/dist/hooks/shims/hermes.d.ts.map +1 -0
  173. package/dist/hooks/shims/hermes.js +14 -0
  174. package/dist/hooks/shims/hermes.js.map +1 -0
  175. package/dist/index.d.ts +33 -76
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +13 -67
  178. package/dist/index.js.map +1 -1
  179. package/dist/lang/bash.d.ts.map +1 -1
  180. package/dist/lang/bash.js.map +1 -1
  181. package/dist/lang/c.d.ts.map +1 -1
  182. package/dist/lang/c.js.map +1 -1
  183. package/dist/lang/c_sharp.d.ts.map +1 -1
  184. package/dist/lang/c_sharp.js.map +1 -1
  185. package/dist/lang/cpp.d.ts.map +1 -1
  186. package/dist/lang/cpp.js.map +1 -1
  187. package/dist/lang/go.d.ts.map +1 -1
  188. package/dist/lang/go.js.map +1 -1
  189. package/dist/lang/java.d.ts.map +1 -1
  190. package/dist/lang/java.js.map +1 -1
  191. package/dist/lang/javascript.d.ts.map +1 -1
  192. package/dist/lang/javascript.js.map +1 -1
  193. package/dist/lang/kotlin.d.ts.map +1 -1
  194. package/dist/lang/kotlin.js.map +1 -1
  195. package/dist/lang/php.d.ts.map +1 -1
  196. package/dist/lang/php.js.map +1 -1
  197. package/dist/lang/profile.d.ts.map +1 -1
  198. package/dist/lang/profile.js.map +1 -1
  199. package/dist/lang/python.d.ts.map +1 -1
  200. package/dist/lang/python.js.map +1 -1
  201. package/dist/lang/registry.d.ts.map +1 -1
  202. package/dist/lang/registry.js.map +1 -1
  203. package/dist/lang/ruby.d.ts.map +1 -1
  204. package/dist/lang/ruby.js.map +1 -1
  205. package/dist/lang/rust.d.ts.map +1 -1
  206. package/dist/lang/rust.js.map +1 -1
  207. package/dist/lang/swift.d.ts.map +1 -1
  208. package/dist/lang/swift.js.map +1 -1
  209. package/dist/lang/tsx.d.ts.map +1 -1
  210. package/dist/lang/tsx.js.map +1 -1
  211. package/dist/lang/typescript.d.ts.map +1 -1
  212. package/dist/lang/typescript.js.map +1 -1
  213. package/dist/net/policy.d.ts +18 -1
  214. package/dist/net/policy.d.ts.map +1 -1
  215. package/dist/net/policy.js +7 -4
  216. package/dist/net/policy.js.map +1 -1
  217. package/dist/ops/index.d.ts +39 -0
  218. package/dist/ops/index.d.ts.map +1 -0
  219. package/dist/ops/index.js +37 -0
  220. package/dist/ops/index.js.map +1 -0
  221. package/dist/ops/inputs.d.ts +170 -0
  222. package/dist/ops/inputs.d.ts.map +1 -0
  223. package/dist/ops/inputs.js +142 -0
  224. package/dist/ops/inputs.js.map +1 -0
  225. package/dist/ops/verbs.d.ts +136 -0
  226. package/dist/ops/verbs.d.ts.map +1 -0
  227. package/dist/ops/verbs.js +76 -0
  228. package/dist/ops/verbs.js.map +1 -0
  229. package/dist/plan/auto.d.ts +69 -0
  230. package/dist/plan/auto.d.ts.map +1 -0
  231. package/dist/plan/auto.js +70 -0
  232. package/dist/plan/auto.js.map +1 -0
  233. package/dist/plan/budget.d.ts +21 -0
  234. package/dist/plan/budget.d.ts.map +1 -0
  235. package/dist/plan/budget.js +26 -0
  236. package/dist/plan/budget.js.map +1 -0
  237. package/dist/plan/grammar.d.ts +14 -0
  238. package/dist/plan/grammar.d.ts.map +1 -1
  239. package/dist/plan/grammar.js +47 -6
  240. package/dist/plan/grammar.js.map +1 -1
  241. package/dist/plan/lexical.d.ts.map +1 -1
  242. package/dist/plan/lexical.js +1 -7
  243. package/dist/plan/lexical.js.map +1 -1
  244. package/dist/plan/planners.d.ts +35 -2
  245. package/dist/plan/planners.d.ts.map +1 -1
  246. package/dist/plan/planners.js +35 -2
  247. package/dist/plan/planners.js.map +1 -1
  248. package/dist/plan/structural.d.ts +19 -5
  249. package/dist/plan/structural.d.ts.map +1 -1
  250. package/dist/plan/structural.js +180 -33
  251. package/dist/plan/structural.js.map +1 -1
  252. package/dist/repomap/cache.d.ts +48 -0
  253. package/dist/repomap/cache.d.ts.map +1 -1
  254. package/dist/repomap/cache.js +112 -13
  255. package/dist/repomap/cache.js.map +1 -1
  256. package/dist/repomap/io.d.ts +24 -0
  257. package/dist/repomap/io.d.ts.map +1 -0
  258. package/dist/repomap/io.js +34 -0
  259. package/dist/repomap/io.js.map +1 -0
  260. package/dist/repomap/map.d.ts +69 -6
  261. package/dist/repomap/map.d.ts.map +1 -1
  262. package/dist/repomap/map.js +104 -22
  263. package/dist/repomap/map.js.map +1 -1
  264. package/dist/repomap/rank.d.ts +26 -2
  265. package/dist/repomap/rank.d.ts.map +1 -1
  266. package/dist/repomap/rank.js +20 -0
  267. package/dist/repomap/rank.js.map +1 -1
  268. package/dist/repomap/reader.d.ts +73 -0
  269. package/dist/repomap/reader.d.ts.map +1 -0
  270. package/dist/repomap/reader.js +30 -0
  271. package/dist/repomap/reader.js.map +1 -0
  272. package/dist/repomap/tags.d.ts.map +1 -1
  273. package/dist/repomap/tags.js.map +1 -1
  274. package/dist/retrieve.d.ts +8 -0
  275. package/dist/retrieve.d.ts.map +1 -1
  276. package/dist/retrieve.js +12 -1
  277. package/dist/retrieve.js.map +1 -1
  278. package/dist/smelter.d.ts +79 -0
  279. package/dist/smelter.d.ts.map +1 -0
  280. package/dist/smelter.js +65 -0
  281. package/dist/smelter.js.map +1 -0
  282. package/dist/stages.d.ts.map +1 -1
  283. package/dist/stages.js +2 -2
  284. package/dist/stages.js.map +1 -1
  285. package/dist/stats.d.ts +2 -1
  286. package/dist/stats.d.ts.map +1 -1
  287. package/dist/stats.js +2 -1
  288. package/dist/stats.js.map +1 -1
  289. package/dist/store-dir.d.ts +43 -4
  290. package/dist/store-dir.d.ts.map +1 -1
  291. package/dist/store-dir.js +48 -5
  292. package/dist/store-dir.js.map +1 -1
  293. package/dist/store.d.ts +6 -0
  294. package/dist/store.d.ts.map +1 -1
  295. package/dist/store.js +6 -0
  296. package/dist/store.js.map +1 -1
  297. package/dist/types.d.ts +35 -3
  298. package/dist/types.d.ts.map +1 -1
  299. package/dist/types.js.map +1 -1
  300. package/grammars/tree-sitter-bash.wasm +0 -0
  301. package/grammars/tree-sitter-c.wasm +0 -0
  302. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  303. package/grammars/tree-sitter-cpp.wasm +0 -0
  304. package/grammars/tree-sitter-go.wasm +0 -0
  305. package/grammars/tree-sitter-java.wasm +0 -0
  306. package/grammars/tree-sitter-javascript.wasm +0 -0
  307. package/grammars/tree-sitter-kotlin.wasm +0 -0
  308. package/grammars/tree-sitter-php.wasm +0 -0
  309. package/grammars/tree-sitter-python.wasm +0 -0
  310. package/grammars/tree-sitter-ruby.wasm +0 -0
  311. package/grammars/tree-sitter-rust.wasm +0 -0
  312. package/grammars/tree-sitter-swift.wasm +0 -0
  313. package/grammars/tree-sitter-tsx.wasm +0 -0
  314. package/grammars/tree-sitter-typescript.wasm +0 -0
  315. package/package.json +22 -14
  316. package/dist/cli/resolve.d.ts +0 -103
  317. package/dist/cli/resolve.d.ts.map +0 -1
  318. package/dist/cli/resolve.js +0 -106
  319. package/dist/cli/resolve.js.map +0 -1
@@ -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"]}
@@ -1,6 +1,7 @@
1
1
  import { Parser } from 'web-tree-sitter';
2
2
  import { GrammarUnavailableError, MissingMarkerPricingError } from '../errors.js';
3
3
  import { profileFor, structuralLanguages } from '../lang/registry.js';
4
+ import { predictOutputBytes, savingBytes } from './budget.js';
4
5
  import { loadGrammar } from './grammar.js';
5
6
  export const STRUCTURAL_PLANNER_ID = 'structural/v1';
6
7
  /**
@@ -51,11 +52,14 @@ export class StructuralPlanner {
51
52
  * reused directly. Deterministic: same text, same language, same focus, same options —
52
53
  * byte-identical plan.
53
54
  *
54
- * `budgetBytes` is a target, not a guarantee, for the same reason it is in the lexical
55
- * planner: once every non-matching sibling run is collapsed there is nothing left to
56
- * cut except the declarations the caller asked to keep, and an optimizer that silently
57
- * drops the thing you searched for is the exact failure this design refuses. Callers
58
- * who need a hard ceiling check `outputBytes` and decide.
55
+ * `budgetBytes` is a target, not a guarantee. The plan is built in two passes: every
56
+ * maximal run of non-matching siblings is collapsed where that pays for its marker,
57
+ * and then only if the plan is still over budget each run the first pass refused
58
+ * is re-asked as its own best profitable sub-run (the budget rung, in
59
+ * {@link planFromTree}). Under the last of those there is nothing left to cut except
60
+ * the declarations the caller asked to keep, and an optimizer that silently drops the
61
+ * thing you searched for is the exact failure this design refuses. Callers who need a
62
+ * hard ceiling check `outputBytes` and decide.
59
63
  *
60
64
  * @throws {GrammarUnavailableError} when the language is not one this planner parses,
61
65
  * when the grammar cannot be loaded, or when the parser produces no tree. Never a
@@ -92,15 +96,32 @@ export async function planStructural(input, options = {}) {
92
96
  }
93
97
  }
94
98
  function assertStructuralLanguage(language) {
95
- for (const supported of STRUCTURAL_LANGUAGES) {
96
- if (language === supported)
97
- return supported;
98
- }
99
+ if (isStructuralLanguage(language))
100
+ return language;
99
101
  const named = `${STRUCTURAL_LANGUAGES.slice(0, -1).join(', ')} and ${STRUCTURAL_LANGUAGES[STRUCTURAL_LANGUAGES.length - 1]}`;
100
102
  throw new GrammarUnavailableError(`smelt: structural planning covers ${named} in this ` +
101
103
  `slice; got "${language}". It does not fall back to the lexical planner — output ` +
102
104
  `labelled structural/v1 that is really line windows would be undetectable from ` +
103
- `the outside. Use the lexical planner explicitly if that is what you want.`);
105
+ `the outside. Use \`strategy: 'auto'\` for a mixed stream that is sometimes not ` +
106
+ `code — it picks the planner from the language and labels the one it ran — or ` +
107
+ `the lexical planner explicitly if that is what you want.`);
108
+ }
109
+ /**
110
+ * Whether this planner has a bundled grammar for the language — the one membership
111
+ * test, shared with the `auto` strategy (`plan/auto.ts`).
112
+ *
113
+ * The refusal above and auto's selection are the same question asked for opposite
114
+ * reasons, and two spellings of it would be two answers: a language auto routed to
115
+ * `structural` that `assertStructuralLanguage` then refused would be a
116
+ * `GrammarUnavailableError` raised by the strategy whose entire job is not raising
117
+ * one.
118
+ */
119
+ export function isStructuralLanguage(language) {
120
+ for (const supported of STRUCTURAL_LANGUAGES) {
121
+ if (language === supported)
122
+ return true;
123
+ }
124
+ return false;
104
125
  }
105
126
  function planFromTree(tree, input, options, language) {
106
127
  const profile = profileFor(language);
@@ -118,51 +139,177 @@ function planFromTree(tree, input, options, language) {
118
139
  const matched = matchUnits(units, input, options);
119
140
  const minSiblings = options.minSiblings ?? 1;
120
141
  const toByte = utf8OffsetIndex(input.text, units.flatMap((unit) => [unit.start, unit.end]));
121
- const elisions = [];
122
- let run = [];
123
- const flush = () => {
124
- if (run.length === 0)
125
- return;
126
- const group = run;
127
- run = [];
128
- if (group.length < minSiblings)
129
- return;
130
- // A line-comment marker swallows the rest of its line. When the run's last unit
142
+ /**
143
+ * One contiguous group of siblings as an elision — or nothing, when collapsing it
144
+ * would be illegal or would not pay for its marker. Every cut in this planner,
145
+ * first pass and budget rung alike, is minted here: one legality rule, one price,
146
+ * one explanation, so the rung cannot cut something the first pass would refuse.
147
+ */
148
+ const collapse = (group) => {
149
+ if (group.length === 0 || group.length < minSiblings)
150
+ return undefined;
151
+ // A line-comment marker swallows the rest of its line. When the group's last unit
131
152
  // ends mid-line — python's `stmt_a(); stmt_b()` puts two top-level statements on
132
153
  // one line — the marker's `# ` leader would comment out the *kept* code after it.
133
154
  // Refusing the collapse is the honest move; extending the range would elide code
134
155
  // no unit accounted for.
135
156
  if (markerIsLineComment && !restOfLineIsBlank(input.text, group[group.length - 1].end)) {
136
- return;
157
+ return undefined;
137
158
  }
138
159
  const start = toByte.get(group[0].start);
139
160
  const end = toByte.get(group[group.length - 1].end);
140
- const cutBytes = end - start;
141
- const explanation = explain(group);
161
+ const candidate = {
162
+ range: { start, end },
163
+ reason: { rule: SIBLING_COLLAPSE_RULE, explanation: explain(group) },
164
+ };
142
165
  // Profitability, priced rather than estimated: ask the MarkerPricing seam for the
143
166
  // exact cost of the marker this cut would earn — the explanation's length varies
144
167
  // with kind diversity, and the pricing carries the language's comment leader (or a
145
168
  // caller's custom builder), so a fixed estimate can pass a cut whose marker is
146
169
  // bigger than what it removes, and a marker that costs more than it removes grows
147
170
  // the output.
148
- const markerBytes = pricing.costBytes({ rule: SIBLING_COLLAPSE_RULE, explanation }, cutBytes);
149
- if (cutBytes <= markerBytes)
150
- return;
151
- elisions.push({
152
- range: { start, end },
153
- reason: { rule: SIBLING_COLLAPSE_RULE, explanation },
154
- });
171
+ return savingBytes(candidate, pricing) > 0 ? candidate : undefined;
155
172
  };
173
+ /**
174
+ * The UTF-8 bytes a sub-run `[from, to)` spans — read off the same offset index
175
+ * `collapse` mints ranges from, so it is the exact upper bound on what collapsing
176
+ * that sub-run could save, in O(1) and without slicing. The budget rung's span
177
+ * bound depends on it being both exact and cheap.
178
+ */
179
+ const spanBytes = (group, from, to) => toByte.get(group[to - 1].end) - toByte.get(group[from].start);
180
+ // The first pass: every maximal run of siblings no focus term matched and no rule
181
+ // pinned, collapsed whole. A run that earns no cut is remembered, not forgotten —
182
+ // the budget rung below is the only reader of that list.
183
+ const elisions = [];
184
+ const refused = [];
185
+ for (const group of maximalRuns(units, matched)) {
186
+ const cut = collapse(group);
187
+ if (cut === undefined)
188
+ refused.push(group);
189
+ else
190
+ elisions.push(cut);
191
+ }
192
+ // THE BUDGET RUNG — the structural answer to the lexical planner's context ladder.
193
+ //
194
+ // Until this existed, `planStructural` never read `input.budgetBytes` at all: it
195
+ // took every profitable maximal run and stopped, which is right until the plan
196
+ // comes back over budget with a profitable cut still on the table. The review's
197
+ // case: 158 bytes, a budget of 120, and a run of three sibling classes worth 92
198
+ // bytes left whole — because the *maximal* run also swallowed a `VERSION = 1`
199
+ // statement above them, and a mixed-kind explanation ("1 statement, 3 classes")
200
+ // priced a 106-byte marker against a 105-byte cut. The planner returned over budget
201
+ // having elided nothing, while the three classes alone priced an 82-byte marker
202
+ // against 92 bytes — a real saving of 10 bytes nobody was offered.
203
+ //
204
+ // So, and only when the plan is still over budget, each refused run is re-asked as
205
+ // its own best profitable sub-run, earliest run first, stopping the moment the plan
206
+ // fits. Four properties hold by construction rather than by care:
207
+ //
208
+ // - **A focus match is never cut.** A matched unit is not in any run, so no
209
+ // sub-run of any run can contain one. Same for a pinned unit.
210
+ // - **The output never grows.** Every candidate goes through `collapse`, which
211
+ // mints nothing whose marker is not strictly cheaper than the bytes it removes.
212
+ // - **It stays deterministic.** Sub-runs are enumerated start-ascending and
213
+ // length-descending, and a tie in saving keeps the one already held — the
214
+ // earliest start, then the longest. Both of the rung's bounds read that same
215
+ // order, so a bounded sweep and an exhaustive one answer alike.
216
+ // - **It stays bounded.** A refused run can be the whole file — the mid-line
217
+ // refusal is about one character, not about size — so the sweep stops at the
218
+ // first cut that fits the budget and skips every candidate whose span already
219
+ // proves it cannot win. See {@link bestSubRun}.
220
+ //
221
+ // Budget pressure is the trigger, not profitability, because a maximal run is the
222
+ // better *explanation*: one marker naming everything it hid beats two naming halves
223
+ // of it. Trading that away is worth it to fit a budget and not worth it otherwise.
224
+ const inputBytes = Buffer.byteLength(input.text, 'utf8');
225
+ for (const group of refused) {
226
+ const currentBytes = predictOutputBytes(inputBytes, elisions, pricing);
227
+ if (currentBytes <= input.budgetBytes)
228
+ break;
229
+ // What this run would have to save to land the plan under budget — the sweep's
230
+ // stopping condition, so a run that can fit the budget is priced a handful of
231
+ // times instead of exhaustively. See {@link bestSubRun}.
232
+ const enough = currentBytes - input.budgetBytes;
233
+ const cut = bestSubRun(group, collapse, pricing, spanBytes, enough);
234
+ if (cut !== undefined)
235
+ elisions.push(cut);
236
+ }
237
+ return elisions.toSorted((a, b) => a.range.start - b.range.start);
238
+ }
239
+ /**
240
+ * The runs the first pass collapses: every maximal contiguous group of units that no
241
+ * focus term matched and no rule pinned. A matched or pinned unit ends the run before
242
+ * it and starts none — which is what makes "never cut a focus-matched declaration" a
243
+ * property of the *shape* of the data rather than a check anyone has to remember.
244
+ */
245
+ function maximalRuns(units, matched) {
246
+ const runs = [];
247
+ let run = [];
156
248
  for (let i = 0; i < units.length; i += 1) {
157
249
  if (matched[i] === true || units[i].pinned === true) {
158
- flush();
250
+ if (run.length > 0)
251
+ runs.push(run);
252
+ run = [];
159
253
  }
160
254
  else {
161
255
  run.push(units[i]);
162
256
  }
163
257
  }
164
- flush();
165
- return elisions;
258
+ if (run.length > 0)
259
+ runs.push(run);
260
+ return runs;
261
+ }
262
+ /**
263
+ * The best cut available inside one refused run: the contiguous sub-run whose real
264
+ * marker is cheapest against the bytes it removes.
265
+ *
266
+ * The enumeration is bounded, and it has to be. A run reaches here refused for any of
267
+ * three reasons, and only one of them is "it lost to a ~100-byte marker": a run is also
268
+ * refused when it holds fewer than `minSiblings` units, and — the one that bites —
269
+ * when its last unit ends mid-line, because a line-comment marker would swallow the
270
+ * kept code after it. That last rule is a property of one character in the file, not of
271
+ * the run's size, so a *whole-file* run can land here: one bash script whose last line
272
+ * is `cleanup_tmp; deploy_release --now` refuses its only maximal run, and an
273
+ * unbounded `(from, to)` sweep over its thousands of units — each candidate slicing,
274
+ * explaining and pricing — is cubic. Measured, before this bound: 19.5 KB / 373 ms,
275
+ * 39 KB / 2.3 s, 79 KB / 17.2 s, 159 KB / 131 s. That is a CPU loop on tool-controlled
276
+ * input, on the path the caller takes precisely when it is over budget — the normal
277
+ * case. Two bounds, neither of which changes which cut a bounded sweep would have
278
+ * found by exhaustion:
279
+ *
280
+ * - **`enough`** — the saving that lands the plan under budget. The rung exists to
281
+ * fit a budget; the first candidate that does is the answer, and enumeration order
282
+ * (start-ascending, length-descending) makes it the earliest, longest such cut.
283
+ * This is what turns the whole-file case into two priced candidates.
284
+ * - **The span bound** — a cut can never save more than the bytes it spans, so once
285
+ * a candidate's span has shrunk to `bestSaving` or below, neither it nor any
286
+ * shorter candidate from the same start can win, and once the *longest* candidate
287
+ * from a start is that small, no later start can win either. Spans shrink
288
+ * monotonically along both loops, so both breaks are exact, not heuristic: they
289
+ * skip only candidates already proven to lose.
290
+ */
291
+ function bestSubRun(group, collapse, pricing, spanBytes, enough) {
292
+ let best;
293
+ let bestSaving = 0;
294
+ for (let from = 0; from < group.length; from += 1) {
295
+ if (spanBytes(group, from, group.length) <= bestSaving)
296
+ break;
297
+ for (let to = group.length; to > from; to -= 1) {
298
+ if (spanBytes(group, from, to) <= bestSaving)
299
+ break;
300
+ const candidate = collapse(group.slice(from, to));
301
+ if (candidate === undefined)
302
+ continue;
303
+ const saving = savingBytes(candidate, pricing);
304
+ if (saving > bestSaving) {
305
+ best = candidate;
306
+ bestSaving = saving;
307
+ if (saving >= enough)
308
+ return best;
309
+ }
310
+ }
311
+ }
312
+ return best;
166
313
  }
167
314
  /** True when nothing but whitespace follows `index` on its own line. */
168
315
  function restOfLineIsBlank(text, index) {
@@ -1 +1 @@
1
- {"version":3,"file":"structural.js","sourceRoot":"","sources":["../../src/plan/structural.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"structural.js","sourceRoot":"","sources":["../../src/plan/structural.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAElF,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAUtE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,CAAC,MAAM,qBAAqB,GAAG,eAAe,CAAC;AAErD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAA0B,mBAAmB,EAAE,CAAC;AASjF;;;GAGG;AACH,MAAM,qBAAqB,GAAG,kBAAkB,CAAC;AAyCjD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,iBAAiB;IACnB,EAAE,GAAG,qBAAqB,CAAC;IAC3B,OAAO,CAA2B;IAE3C,YAAY,OAAO,GAA6B,EAAE;QAChD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,IAAI,CAAC,KAAgB;QACnB,OAAO,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7C,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,KAAgB,EAChB,OAAO,GAA6B,EAAE;IAEtC,MAAM,QAAQ,GAAG,wBAAwB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC1D,kFAAkF;IAClF,kFAAkF;IAClF,2EAA2E;IAC3E,MAAM,OAAO,GAA8B,KAAK,CAAC,OAAO,CAAC;IACzD,IAAI,OAAO,KAAK,SAAS;QAAE,MAAM,IAAI,yBAAyB,CAAC,qBAAqB,CAAC,CAAC;IACtF,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC;IAE5C,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAC5B,IAAI,IAAI,GAAgB,IAAI,CAAC;IAC7B,IAAI,CAAC;QACH,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC5B,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,MAAM,IAAI,uBAAuB,CAC/B,cAAc,QAAQ,uDAAuD;gBAC3E,yEAAyE;gBACzE,kCAAkC,CACrC,CAAC;QACJ,CAAC;QACD,OAAO;YACL,OAAO,EAAE,qBAAqB;YAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,QAAQ,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC;SACvD,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,IAAI,EAAE,MAAM,EAAE,CAAC;QACf,MAAM,CAAC,MAAM,EAAE,CAAC;IAClB,CAAC;AACH,CAAC;AAED,SAAS,wBAAwB,CAAC,QAA+B;IAC/D,IAAI,oBAAoB,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IACpD,MAAM,KAAK,GAAG,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAC3D,oBAAoB,CAAC,oBAAoB,CAAC,MAAM,GAAG,CAAC,CACtD,EAAE,CAAC;IACH,MAAM,IAAI,uBAAuB,CAC/B,qCAAqC,KAAK,WAAW;QACnD,eAAe,QAAQ,2DAA2D;QAClF,gFAAgF;QAChF,iFAAiF;QACjF,+EAA+E;QAC/E,0DAA0D,CAC7D,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAA+B;IAE/B,KAAK,MAAM,SAAS,IAAI,oBAAoB,EAAE,CAAC;QAC7C,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,YAAY,CACnB,IAAU,EACV,KAAgB,EAChB,OAAiC,EACjC,QAA4B;IAE5B,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IACrC,yEAAyE;IACzE,iFAAiF;IACjF,mDAAmD;IACnD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAU,CAAC;IACrC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,iFAAiF;IACjF,8EAA8E;IAC9E,gFAAgF;IAChF,iCAAiC;IACjC,MAAM,mBAAmB,GAAG,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC;IAC/D,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IAClD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,eAAe,CAC5B,KAAK,CAAC,IAAI,EACV,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAChD,CAAC;IAEF;;;;;OAKG;IACH,MAAM,QAAQ,GAAG,CAAC,KAAsB,EAA8B,EAAE;QACtE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,WAAW;YAAE,OAAO,SAAS,CAAC;QACvE,kFAAkF;QAClF,iFAAiF;QACjF,kFAAkF;QAClF,iFAAiF;QACjF,yBAAyB;QACzB,IAAI,mBAAmB,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,GAAG,CAAC,EAAE,CAAC;YACxF,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,KAAK,CAAE,CAAC;QAC3C,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,GAAG,CAAE,CAAC;QACtD,MAAM,SAAS,GAAmB;YAChC,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;YACrB,MAAM,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE;SACrE,CAAC;QACF,kFAAkF;QAClF,iFAAiF;QACjF,mFAAmF;QACnF,+EAA+E;QAC/E,kFAAkF;QAClF,cAAc;QACd,OAAO,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IACrE,CAAC,CAAC;IAEF;;;;;OAKG;IACH,MAAM,SAAS,GAAG,CAAC,KAAsB,EAAE,IAAY,EAAE,EAAU,EAAU,EAAE,CAC7E,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAE,CAAC,GAAG,CAAE,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAE,CAAC,KAAK,CAAE,CAAC;IAEpE,kFAAkF;IAClF,kFAAkF;IAClF,yDAAyD;IACzD,MAAM,QAAQ,GAAqB,EAAE,CAAC;IACtC,MAAM,OAAO,GAAwB,EAAE,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;QAChD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC5B,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;;YACtC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,mFAAmF;IACnF,EAAE;IACF,iFAAiF;IACjF,+EAA+E;IAC/E,gFAAgF;IAChF,gFAAgF;IAChF,8EAA8E;IAC9E,gFAAgF;IAChF,oFAAoF;IACpF,gFAAgF;IAChF,mEAAmE;IACnE,EAAE;IACF,mFAAmF;IACnF,oFAAoF;IACpF,kEAAkE;IAClE,EAAE;IACF,8EAA8E;IAC9E,kEAAkE;IAClE,iFAAiF;IACjF,oFAAoF;IACpF,8EAA8E;IAC9E,8EAA8E;IAC9E,iFAAiF;IACjF,oEAAoE;IACpE,+EAA+E;IAC/E,iFAAiF;IACjF,kFAAkF;IAClF,oDAAoD;IACpD,EAAE;IACF,kFAAkF;IAClF,oFAAoF;IACpF,mFAAmF;IACnF,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACzD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,YAAY,GAAG,kBAAkB,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QACvE,IAAI,YAAY,IAAI,KAAK,CAAC,WAAW;YAAE,MAAM;QAC7C,+EAA+E;QAC/E,8EAA8E;QAC9E,yDAAyD;QACzD,MAAM,MAAM,GAAG,YAAY,GAAG,KAAK,CAAC,WAAW,CAAC;QAChD,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QACpE,IAAI,GAAG,KAAK,SAAS;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5C,CAAC;IAED,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACpE,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAClB,KAAsB,EACtB,OAA2B;IAE3B,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,GAAG,GAAW,EAAE,CAAC;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAE,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YACrD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnC,GAAG,GAAG,EAAE,CAAC;QACX,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnC,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,SAAS,UAAU,CACjB,KAAsB,EACtB,QAAgE,EAChE,OAAsB,EACtB,SAAuE,EACvE,MAAc;IAEd,IAAI,IAAgC,CAAC;IACrC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;QAClD,IAAI,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,UAAU;YAAE,MAAM;QAC9D,KAAK,IAAI,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;YAC/C,IAAI,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,UAAU;gBAAE,MAAM;YACpD,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;YAClD,IAAI,SAAS,KAAK,SAAS;gBAAE,SAAS;YACtC,MAAM,MAAM,GAAG,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YAC/C,IAAI,MAAM,GAAG,UAAU,EAAE,CAAC;gBACxB,IAAI,GAAG,SAAS,CAAC;gBACjB,UAAU,GAAG,MAAM,CAAC;gBACpB,IAAI,MAAM,IAAI,MAAM;oBAAE,OAAO,IAAI,CAAC;YACpC,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,wEAAwE;AACxE,SAAS,iBAAiB,CAAC,IAAY,EAAE,KAAa;IACpD,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;QACpB,IAAI,EAAE,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAAE,OAAO,KAAK,CAAC;IACnC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,OAAO,CAAC,IAAU,EAAE,IAAY,EAAE,SAA4B;IACrE,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,yEAAyE;IACzE,IAAI,OAAO,GAAW,EAAE,CAAC;IACzB,8EAA8E;IAC9E,IAAI,mBAAmB,GAAG,KAAK,CAAC;IAEhC,MAAM,YAAY,GAAG,GAAS,EAAE;QAC9B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACjC,KAAK,CAAC,IAAI,CAAC;YACT,KAAK,EAAE,OAAO,CAAC,CAAC,CAAE,CAAC,UAAU;YAC7B,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,QAAQ;YAC1C,IAAI,EAAE,mBAAmB,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;YACnD,MAAM,EAAE,CAAC,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;SAC7F,CAAC,CAAC;QACH,OAAO,GAAG,EAAE,CAAC;QACb,mBAAmB,GAAG,KAAK,CAAC;IAC9B,CAAC,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACvC,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QAC7B,IAAI,SAAS,CAAC,kBAAkB,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/E,6EAA6E;YAC7E,6EAA6E;YAC7E,4EAA4E;YAC5E,2EAA2E;YAC3E,2EAA2E;YAC3E,0EAA0E;YAC1E,yCAAyC;YACzC,YAAY,EAAE,CAAC;YACf,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;YACtC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3D,SAAS;QACX,CAAC;QACD,IACE,SAAS,CAAC,WAAW,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI;YAC/C,eAAe,CAAC,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,EACvC,CAAC;YACD,6EAA6E;YAC7E,yEAAyE;YACzE,cAAc;YACd,YAAY,EAAE,CAAC;YACf,KAAK,CAAC,IAAI,CAAC;gBACT,KAAK,EAAE,KAAK,CAAC,UAAU;gBACvB,GAAG,EAAE,KAAK,CAAC,QAAQ;gBACnB,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC;gBAC9B,MAAM,EAAE,IAAI;aACb,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,MAAM,SAAS,GAAG,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzD,MAAM,WAAW,GAAG,SAAS,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7D,IAAI,SAAS,IAAI,WAAW,EAAE,CAAC;YAC7B,IACE,OAAO,CAAC,MAAM,GAAG,CAAC;gBAClB,CAAC,mBAAmB;gBACpB,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,EACxE,CAAC;gBACD,YAAY,EAAE,CAAC;YACjB,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACpB,mBAAmB,KAAK,WAAW,CAAC;YACpC,SAAS;QACX,CAAC;QAED,0EAA0E;QAC1E,gFAAgF;QAChF,iFAAiF;QACjF,2EAA2E;QAC3E,iFAAiF;QACjF,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAC/B,SAAS,CAAC,yBAAyB,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI;YAC3D,CAAC,CAAC,qBAAqB,CAAC,KAAK,EAAE,SAAS,CAAC;YACzC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QAE5C,MAAM,QAAQ,GACZ,OAAO,CAAC,MAAM,GAAG,CAAC;YAClB,CAAC,mBAAmB;gBAClB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;QAC7E,IAAI,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;YAC7E,2EAA2E;YAC3E,0EAA0E;YAC1E,YAAY,EAAE,CAAC;YACf,KAAK,CAAC,IAAI,CAAC;gBACT,KAAK,EAAE,KAAK,CAAC,UAAU;gBACvB,GAAG,EAAE,QAAQ;gBACb,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC;aAC/B,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,QAAQ,EAAE,CAAC;YACpB,KAAK,CAAC,IAAI,CAAC;gBACT,KAAK,EAAE,OAAO,CAAC,CAAC,CAAE,CAAC,UAAU;gBAC7B,GAAG,EAAE,QAAQ;gBACb,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC;aAC/B,CAAC,CAAC;YACH,OAAO,GAAG,EAAE,CAAC;YACb,mBAAmB,GAAG,KAAK,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,YAAY,EAAE,CAAC;YACf,KAAK,CAAC,IAAI,CAAC;gBACT,KAAK,EAAE,KAAK,CAAC,UAAU;gBACvB,GAAG,EAAE,QAAQ;gBACb,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC;aAC/B,CAAC,CAAC;QACL,CAAC;QACD,KAAK,MAAM,OAAO,IAAI,QAAQ;YAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxD,CAAC;IACD,YAAY,EAAE,CAAC;IACf,OAAO,KAAK,CAAC;AACf,CAAC;AAED,wFAAwF;AACxF,SAAS,aAAa,CAAC,IAAU,EAAE,IAAY,EAAE,SAA4B;IAC3E,IAAI,SAAS,CAAC,oBAAoB,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC/D,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACzD,OAAO,SAAS,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;AACzF,CAAC;AAED;;;;;GAKG;AACH,SAAS,eAAe,CAAC,IAAU,EAAE,IAAY,EAAE,SAA4B;IAC7E,MAAM,OAAO,GAAG,SAAS,CAAC,oBAAoB,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5D,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;GAIG;AACH,SAAS,qBAAqB,CAC5B,IAAU,EACV,SAA4B;IAE5B,MAAM,QAAQ,GAAW,EAAE,CAAC;IAC5B,IAAI,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC;IAC1B,MAAM,IAAI,GAAG,CAAC,OAAa,EAAQ,EAAE;QACnC,IAAI,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7C,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACvB,OAAO;QACT,CAAC;QACD,IAAI,WAAW,GAAG,KAAK,CAAC;QACxB,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACrC,IAAI,KAAK,KAAK,IAAI;gBAAE,SAAS;YAC7B,WAAW,GAAG,IAAI,CAAC;YACnB,IAAI,CAAC,KAAK,CAAC,CAAC;QACd,CAAC;QACD,IAAI,CAAC,WAAW,IAAI,OAAO,CAAC,QAAQ,GAAG,GAAG;YAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC;IACrE,CAAC,CAAC;IACF,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClC,IAAI,KAAK,KAAK,IAAI;YAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC,EAAE,CAAC;AACpF,CAAC;AAED,+EAA+E;AAC/E,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAW,EAAE,KAAa;IACxD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACvC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IACzC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;AAClD,CAAC;AAED,SAAS,UAAU,CACjB,KAAsB,EACtB,KAAgB,EAChB,OAAiC;IAEjC,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,KAAK,CAAC;IACrD,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAEhF,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACvC,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QACzD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,OAAO,CAAC,KAAsB;IACrC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAElF,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC;IAC3B,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC;QAC5B,OAAO,aAAa,MAAM,CAAC,KAAK,CAAC,YAAY,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;IACxE,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CACrC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAChE,CAAC;IACF,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClF,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,OAAO,CAAC;IACZ,OAAO,aAAa,MAAM,CAAC,KAAK,CAAC,YAAY,OAAO,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC/E,CAAC;AAED,sFAAsF;AACtF,MAAM,qBAAqB,GAAwB,IAAI,GAAG,CAAC;IACzD,WAAW;IACX,SAAS;IACT,iBAAiB;IACjB,gBAAgB;IAChB,gBAAgB;IAChB,qBAAqB;IACrB,WAAW;IACX,SAAS;IACT,qBAAqB;IACrB,mBAAmB;IACnB,SAAS;IACT,SAAS;IACT,cAAc;IACd,cAAc;IACd,wBAAwB;IACxB,0BAA0B;CAC3B,CAAC,CAAC;AAEH,SAAS,SAAS,CAAC,IAAY,EAAE,KAAa;IAC5C,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7B,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IAC9D,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC;AAC9D,CAAC;AAED;;;;GAIG;AACH,SAAS,MAAM,CAAC,IAAU,EAAE,SAA4B;IACtD,MAAM,eAAe,GAAG,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;QAClC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvC,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YACvE,MAAM,KAAK,GAAG,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC/C,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,KAAK,CAAC;QACxC,CAAC;QACD,OAAO,eAAe,CAAC;IACzB,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,iBAAiB,CAAC;IACpD,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,iBAAiB;QAAE,OAAO,WAAW,CAAC;IAC5F,OAAO,aAAa,CAAC;AACvB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,IAAY,EAAE,OAA0B;IAC/D,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,YAAY,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;QAC5E,aAAa,GAAG,KAAK,CAAC;QACtB,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","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"]}