@smeltjs/core 0.2.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 +3 -3
  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 -70
  7. package/dist/cli/args.d.ts.map +1 -1
  8. package/dist/cli/args.js +18 -338
  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 +40 -0
  13. package/dist/cli/config.d.ts.map +1 -1
  14. package/dist/cli/config.js +55 -1
  15. package/dist/cli/config.js.map +1 -1
  16. package/dist/cli/hooks.d.ts +33 -38
  17. package/dist/cli/hooks.d.ts.map +1 -1
  18. package/dist/cli/hooks.js +140 -485
  19. package/dist/cli/hooks.js.map +1 -1
  20. package/dist/cli/init.d.ts +30 -15
  21. package/dist/cli/init.d.ts.map +1 -1
  22. package/dist/cli/init.js +167 -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 -341
  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 +14 -14
  140. package/dist/hooks/guard-core.d.ts.map +1 -1
  141. package/dist/hooks/guard-core.js +15 -37
  142. package/dist/hooks/guard-core.js.map +1 -1
  143. package/dist/hooks/shim.d.ts +95 -4
  144. package/dist/hooks/shim.d.ts.map +1 -1
  145. package/dist/hooks/shim.js +87 -2
  146. package/dist/hooks/shim.js.map +1 -1
  147. package/dist/hooks/shims/claude-code.d.ts +5 -18
  148. package/dist/hooks/shims/claude-code.d.ts.map +1 -1
  149. package/dist/hooks/shims/claude-code.js +9 -56
  150. package/dist/hooks/shims/claude-code.js.map +1 -1
  151. package/dist/hooks/shims/cline.d.ts +5 -12
  152. package/dist/hooks/shims/cline.d.ts.map +1 -1
  153. package/dist/hooks/shims/cline.js +9 -34
  154. package/dist/hooks/shims/cline.js.map +1 -1
  155. package/dist/hooks/shims/codex.d.ts +5 -18
  156. package/dist/hooks/shims/codex.d.ts.map +1 -1
  157. package/dist/hooks/shims/codex.js +9 -51
  158. package/dist/hooks/shims/codex.js.map +1 -1
  159. package/dist/hooks/shims/cursor.d.ts +5 -14
  160. package/dist/hooks/shims/cursor.d.ts.map +1 -1
  161. package/dist/hooks/shims/cursor.js +9 -42
  162. package/dist/hooks/shims/cursor.js.map +1 -1
  163. package/dist/hooks/shims/gemini.d.ts +5 -18
  164. package/dist/hooks/shims/gemini.d.ts.map +1 -1
  165. package/dist/hooks/shims/gemini.js +9 -48
  166. package/dist/hooks/shims/gemini.js.map +1 -1
  167. package/dist/hooks/shims/grok.d.ts +5 -13
  168. package/dist/hooks/shims/grok.d.ts.map +1 -1
  169. package/dist/hooks/shims/grok.js +9 -32
  170. package/dist/hooks/shims/grok.js.map +1 -1
  171. package/dist/hooks/shims/hermes.d.ts +5 -17
  172. package/dist/hooks/shims/hermes.d.ts.map +1 -1
  173. package/dist/hooks/shims/hermes.js +9 -45
  174. package/dist/hooks/shims/hermes.js.map +1 -1
  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 +6 -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 +11 -11
  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
@@ -0,0 +1,138 @@
1
+ import { CliUsageError } from '../../errors.js';
2
+ import { CLI_NAME } from '../shell.js';
3
+ import { flagList, VERB_FLAGS } from './flags.js';
4
+ import { hooksCommand } from './hooks.js';
5
+ import { initCommand } from './init.js';
6
+ import { mapCommand } from './map.js';
7
+ import { retrieveCommand } from './retrieve.js';
8
+ import { smeltCommand } from './smelt.js';
9
+ import { statsCommand } from './stats.js';
10
+ import { DEFAULT_VERB } from './subcommand.js';
11
+ /**
12
+ * The registry — every verb `smelt` answers to, one {@link Subcommand} each.
13
+ *
14
+ * `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and
15
+ * `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a
16
+ * compile error, so the verb list and the facts cannot drift. Every derived view — the
17
+ * USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all
18
+ * the flag-ownership refusal below — is computed from this object, never written twice.
19
+ *
20
+ * Key order is meaningful: it is the order every rendered list uses (USAGE, the help
21
+ * sections, the owners named in a refusal), so keep it stable and append new verbs at
22
+ * the end.
23
+ */
24
+ export const SUBCOMMANDS = {
25
+ smelt: smeltCommand,
26
+ init: initCommand,
27
+ map: mapCommand,
28
+ retrieve: retrieveCommand,
29
+ stats: statsCommand,
30
+ hooks: hooksCommand,
31
+ };
32
+ /** Every command, in registry order. The list every rendered block walks. */
33
+ export const SUBCOMMAND_LIST = Object.values(SUBCOMMANDS);
34
+ /**
35
+ * The verbs with a word on the command line — everything but the default one.
36
+ *
37
+ * The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,
38
+ * not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.
39
+ */
40
+ export const NAMED_VERBS = SUBCOMMAND_LIST.map((command) => command.name).filter((name) => name !== DEFAULT_VERB);
41
+ /**
42
+ * Which command a command line selects: `positionals[0]` when it names a verb, the
43
+ * default verb otherwise. The whole of subcommand dispatch, in one lookup.
44
+ */
45
+ export function subcommandFor(positionals) {
46
+ const first = positionals[0];
47
+ const named = NAMED_VERBS.find((verb) => verb === first);
48
+ return SUBCOMMANDS[named ?? DEFAULT_VERB];
49
+ }
50
+ /**
51
+ * Which verb minted an invocation. Total over the modes `parseSmeltArgs` can return
52
+ * for a verb, so a new mode without a home is a compile error — `'reconstruct'` maps
53
+ * to the default verb because `--reconstruct` is that verb's second job, not a
54
+ * seventh command.
55
+ */
56
+ const VERB_BY_MODE = {
57
+ smelt: 'smelt',
58
+ reconstruct: 'smelt',
59
+ init: 'init',
60
+ map: 'map',
61
+ retrieve: 'retrieve',
62
+ stats: 'stats',
63
+ hooks: 'hooks',
64
+ };
65
+ /** The command that produced an invocation, for dispatch. */
66
+ export function commandFor(mode) {
67
+ return SUBCOMMANDS[VERB_BY_MODE[mode]];
68
+ }
69
+ /** How a command is named in prose. The default verb has no word to name it by. */
70
+ function label(command) {
71
+ return command.name === DEFAULT_VERB ? 'a single-blob run' : command.name;
72
+ }
73
+ /** How a command is named as the *owner* of a flag: the form you would type. */
74
+ function ownerLabel(command) {
75
+ return command.name === DEFAULT_VERB ? 'a single-blob run' : `\`${CLI_NAME} ${command.name}\``;
76
+ }
77
+ /**
78
+ * The commands that own a flag, in registry order — empty for the two global flags,
79
+ * which no verb owns. A flag with exactly one owner has an unambiguous home worth
80
+ * naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several
81
+ * verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them
82
+ * would be a guess.
83
+ */
84
+ export function ownersOf(flag) {
85
+ return SUBCOMMAND_LIST.filter((command) => command.flags.includes(flag));
86
+ }
87
+ /**
88
+ * Refuse every flag the chosen verb does not own — the one message that replaced five
89
+ * hand-written ones.
90
+ *
91
+ * Before this existed, each verb refused each other verb's flags in prose, so the
92
+ * refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag
93
+ * edited five of them. Now ownership is declared once per verb and the message is
94
+ * generated from three derived facts, in the order a reader needs them:
95
+ *
96
+ * 1. **what this verb takes**, and what it got instead — the offending flag, named;
97
+ * 2. **where the flag does belong**, when exactly one verb owns it, listed as that
98
+ * owner's *exclusively* owned flags (so `--ignore` here still reads "--ignore and
99
+ * --cache belong to `smelt map`", exactly as the hand-written message did);
100
+ * 3. **why not here** — the verb's own `refusal` sentence, the half of the old
101
+ * messages worth keeping, and the only half a verb still writes.
102
+ *
103
+ * Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.
104
+ *
105
+ * @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.
106
+ */
107
+ export function refuseForeignFlags(command, values) {
108
+ const foreign = VERB_FLAGS.filter((flag) => values[flag] !== undefined && !command.flags.includes(flag));
109
+ if (foreign.length === 0)
110
+ return;
111
+ const takes = command.flags.length === 0
112
+ ? `${label(command)} takes no flags (got ${flagList(foreign)}).`
113
+ : `${label(command)} takes only ${flagList(command.flags)} (got ${flagList(foreign)}).`;
114
+ throw new CliUsageError(`${CLI_NAME}: ${takes}${redirects(foreign)} ${command.refusal}`);
115
+ }
116
+ /**
117
+ * ` --ignore and --cache belong to \`smelt map\`.` — one clause per single-owner verb.
118
+ *
119
+ * A clause names only the flags its owner owns **alone**, never the ones it shares.
120
+ * `ownersOf(flag).length === 1` is the same test that decided the owner deserved a
121
+ * clause at all, applied to the whole clause: a shared flag (`--budget`, `--focus`,
122
+ * `--json`) has no single home, so naming it here would assert an ownership that the
123
+ * OPTIONS block — which prefixes `map only.` by the same rule — correctly denies.
124
+ */
125
+ function redirects(foreign) {
126
+ const owners = SUBCOMMAND_LIST.filter((owner) => foreign.some((flag) => {
127
+ const claimants = ownersOf(flag);
128
+ return claimants.length === 1 && claimants[0] === owner;
129
+ }));
130
+ return owners
131
+ .map((owner) => {
132
+ const elsewhere = owner.flags.filter((flag) => ownersOf(flag).length === 1);
133
+ const verb = elsewhere.length === 1 ? 'belongs' : 'belong';
134
+ return ` ${flagList(elsewhere)} ${verb} to ${ownerLabel(owner)}.`;
135
+ })
136
+ .join('');
137
+ }
138
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/cli/subcommands/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,WAAW,GAA0C;IAChE,KAAK,EAAE,YAAY;IACnB,IAAI,EAAE,WAAW;IACjB,GAAG,EAAE,UAAU;IACf,QAAQ,EAAE,eAAe;IACzB,KAAK,EAAE,YAAY;IACnB,KAAK,EAAE,YAAY;CACpB,CAAC;AAsBF,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAA6B,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAEpF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAoB,eAAe,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAC/F,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,YAAY,CAChC,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,WAA8B;IAC1D,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC;IACzD,OAAO,WAAW,CAAC,KAAK,IAAI,YAAY,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;GAKG;AACH,MAAM,YAAY,GAAG;IACnB,KAAK,EAAE,OAAO;IACd,WAAW,EAAE,OAAO;IACpB,IAAI,EAAE,MAAM;IACZ,GAAG,EAAE,KAAK;IACV,QAAQ,EAAE,UAAU;IACpB,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;CACqE,CAAC;AAEtF,6DAA6D;AAC7D,MAAM,UAAU,UAAU,CACxB,IAAwD;IAExD,OAAO,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,mFAAmF;AACnF,SAAS,KAAK,CAAC,OAAsB;IACnC,OAAO,OAAO,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;AAC5E,CAAC;AAED,gFAAgF;AAChF,SAAS,UAAU,CAAC,OAAsB;IACxC,OAAO,OAAO,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,IAAI,CAAC;AACjG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAc;IACrC,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAE,OAAO,CAAC,KAA2B,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;AAClG,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAsB,EAAE,MAAkB;IAC3E,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAC/B,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CACtE,CAAC;IACF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAEjC,MAAM,KAAK,GACT,OAAO,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;QACxB,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,wBAAwB,QAAQ,CAAC,OAAO,CAAC,IAAI;QAChE,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,eAAe,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC;IAE5F,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,KAAK,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;AAC3F,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,SAAS,CAAC,OAA4B;IAC7C,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACpB,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjC,OAAO,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC;IAC1D,CAAC,CAAC,CACH,CAAC;IACF,OAAO,MAAM;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACb,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC;QAC5E,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC3D,OAAO,IAAI,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,OAAO,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC;IACpE,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { CLI_NAME } from '../shell.ts';\n\nimport { flagList, VERB_FLAGS } from './flags.ts';\nimport type { FlagName, FlagValues, VerbFlag } from './flags.ts';\nimport { hooksCommand } from './hooks.ts';\nimport type { HooksInvocation } from './hooks.ts';\nimport { initCommand } from './init.ts';\nimport type { InitInvocation } from './init.ts';\nimport { mapCommand } from './map.ts';\nimport type { MapInvocation } from './map.ts';\nimport { retrieveCommand } from './retrieve.ts';\nimport type { RetrieveInvocation } from './retrieve.ts';\nimport { smeltCommand } from './smelt.ts';\nimport type { SmeltInvocation } from './smelt.ts';\nimport { statsCommand } from './stats.ts';\nimport type { StatsInvocation } from './stats.ts';\nimport { DEFAULT_VERB } from './subcommand.ts';\nimport type { Subcommand, Verb } from './subcommand.ts';\n\n/**\n * The registry — every verb `smelt` answers to, one {@link Subcommand} each.\n *\n * `Record<Verb, Subcommand>` on purpose, exactly like `LANGUAGE_PROFILES` and\n * `HARNESS_PROFILES`: adding a `Verb` in `subcommand.ts` without writing its file is a\n * compile error, so the verb list and the facts cannot drift. Every derived view — the\n * USAGE block, the help's sections, the `map only.` prefixes in OPTIONS, and above all\n * the flag-ownership refusal below — is computed from this object, never written twice.\n *\n * Key order is meaningful: it is the order every rendered list uses (USAGE, the help\n * sections, the owners named in a refusal), so keep it stable and append new verbs at\n * the end.\n */\nexport const SUBCOMMANDS: Readonly<Record<Verb, AnySubcommand>> = {\n smelt: smeltCommand,\n init: initCommand,\n map: mapCommand,\n retrieve: retrieveCommand,\n stats: statsCommand,\n hooks: hooksCommand,\n};\n\n/** Everything `parseSmeltArgs` can return. Narrow on `mode`. */\nexport type CliInvocation =\n | SmeltInvocation\n | InitInvocation\n | MapInvocation\n | RetrieveInvocation\n | StatsInvocation\n | HooksInvocation;\n\n/**\n * One registry entry, with its verb's own invocation and resolved types erased.\n *\n * The erasure is what lets six differently-typed commands live in one `Record`, and it\n * is safe because the two ends are never crossed: `runCli` hands a command exactly the\n * invocation that command's own `parse` produced (`verbFor` maps each `mode` back to\n * the verb that minted it), and the resolved value never leaves the pair of calls that\n * makes and consumes it.\n */\nexport type AnySubcommand = Subcommand<CliInvocation, unknown>;\n\n/** Every command, in registry order. The list every rendered block walks. */\nexport const SUBCOMMAND_LIST: readonly AnySubcommand[] = Object.values(SUBCOMMANDS);\n\n/**\n * The verbs with a word on the command line — everything but the default one.\n *\n * The default verb is excluded deliberately: `smelt smelt` is a file named `smelt`,\n * not a recursive invocation, the same way `smelt map` as a *file* needs `./map`.\n */\nexport const NAMED_VERBS: readonly Verb[] = SUBCOMMAND_LIST.map((command) => command.name).filter(\n (name) => name !== DEFAULT_VERB,\n);\n\n/**\n * Which command a command line selects: `positionals[0]` when it names a verb, the\n * default verb otherwise. The whole of subcommand dispatch, in one lookup.\n */\nexport function subcommandFor(positionals: readonly string[]): AnySubcommand {\n const first = positionals[0];\n const named = NAMED_VERBS.find((verb) => verb === first);\n return SUBCOMMANDS[named ?? DEFAULT_VERB];\n}\n\n/**\n * Which verb minted an invocation. Total over the modes `parseSmeltArgs` can return\n * for a verb, so a new mode without a home is a compile error — `'reconstruct'` maps\n * to the default verb because `--reconstruct` is that verb's second job, not a\n * seventh command.\n */\nconst VERB_BY_MODE = {\n smelt: 'smelt',\n reconstruct: 'smelt',\n init: 'init',\n map: 'map',\n retrieve: 'retrieve',\n stats: 'stats',\n hooks: 'hooks',\n} as const satisfies Record<Exclude<CliInvocation['mode'], 'help' | 'version'>, Verb>;\n\n/** The command that produced an invocation, for dispatch. */\nexport function commandFor(\n mode: Exclude<CliInvocation['mode'], 'help' | 'version'>,\n): AnySubcommand {\n return SUBCOMMANDS[VERB_BY_MODE[mode]];\n}\n\n/** How a command is named in prose. The default verb has no word to name it by. */\nfunction label(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : command.name;\n}\n\n/** How a command is named as the *owner* of a flag: the form you would type. */\nfunction ownerLabel(command: AnySubcommand): string {\n return command.name === DEFAULT_VERB ? 'a single-blob run' : `\\`${CLI_NAME} ${command.name}\\``;\n}\n\n/**\n * The commands that own a flag, in registry order — empty for the two global flags,\n * which no verb owns. A flag with exactly one owner has an unambiguous home worth\n * naming in a refusal (and worth prefixing its OPTIONS entry with); a flag several\n * verbs share (`--json`, `--budget`, `--focus`) has none, and pointing at one of them\n * would be a guess.\n */\nexport function ownersOf(flag: FlagName): readonly AnySubcommand[] {\n return SUBCOMMAND_LIST.filter((command) => (command.flags as readonly string[]).includes(flag));\n}\n\n/**\n * Refuse every flag the chosen verb does not own — the one message that replaced five\n * hand-written ones.\n *\n * Before this existed, each verb refused each other verb's flags in prose, so the\n * refusals were O(verbs × flags) sentences kept in sync by hand and an eleventh flag\n * edited five of them. Now ownership is declared once per verb and the message is\n * generated from three derived facts, in the order a reader needs them:\n *\n * 1. **what this verb takes**, and what it got instead — the offending flag, named;\n * 2. **where the flag does belong**, when exactly one verb owns it, listed as that\n * owner's *exclusively* owned flags (so `--ignore` here still reads \"--ignore and\n * --cache belong to `smelt map`\", exactly as the hand-written message did);\n * 3. **why not here** — the verb's own `refusal` sentence, the half of the old\n * messages worth keeping, and the only half a verb still writes.\n *\n * Every refusal stays a {@link CliUsageError}, so every one of them still exits 2.\n *\n * @throws {CliUsageError} naming the offending flag, this verb, and the flag's owner.\n */\nexport function refuseForeignFlags(command: AnySubcommand, values: FlagValues): void {\n const foreign = VERB_FLAGS.filter(\n (flag) => values[flag] !== undefined && !command.flags.includes(flag),\n );\n if (foreign.length === 0) return;\n\n const takes =\n command.flags.length === 0\n ? `${label(command)} takes no flags (got ${flagList(foreign)}).`\n : `${label(command)} takes only ${flagList(command.flags)} (got ${flagList(foreign)}).`;\n\n throw new CliUsageError(`${CLI_NAME}: ${takes}${redirects(foreign)} ${command.refusal}`);\n}\n\n/**\n * ` --ignore and --cache belong to \\`smelt map\\`.` — one clause per single-owner verb.\n *\n * A clause names only the flags its owner owns **alone**, never the ones it shares.\n * `ownersOf(flag).length === 1` is the same test that decided the owner deserved a\n * clause at all, applied to the whole clause: a shared flag (`--budget`, `--focus`,\n * `--json`) has no single home, so naming it here would assert an ownership that the\n * OPTIONS block — which prefixes `map only.` by the same rule — correctly denies.\n */\nfunction redirects(foreign: readonly VerbFlag[]): string {\n const owners = SUBCOMMAND_LIST.filter((owner) =>\n foreign.some((flag) => {\n const claimants = ownersOf(flag);\n return claimants.length === 1 && claimants[0] === owner;\n }),\n );\n return owners\n .map((owner) => {\n const elsewhere = owner.flags.filter((flag) => ownersOf(flag).length === 1);\n const verb = elsewhere.length === 1 ? 'belongs' : 'belong';\n return ` ${flagList(elsewhere)} ${verb} to ${ownerLabel(owner)}.`;\n })\n .join('');\n}\n"]}
@@ -0,0 +1,74 @@
1
+ import type { LoadedConfig } from '../config.ts';
2
+ import type { Subcommand } from './subcommand.ts';
3
+ /**
4
+ * `smelt retrieve <hash>` — the marker's `retrieve("hash")`, as a real command.
5
+ *
6
+ * The exact original bytes go to stdout and **nothing else does**: no report, no
7
+ * trailing newline the store did not hold, no re-encoding. Trailing-newline fidelity
8
+ * is not pedantry — the retrieved bytes get spliced back into reasoning about the
9
+ * original, and an almost-right blob is the silent wrong answer this project refuses.
10
+ *
11
+ * The retrieval IS counted — that is the point. `store.retrieve()` journals the hit,
12
+ * so an agent working from pure shell moves the same `expansionRate` a tool-calling
13
+ * consumer moves, and over-pruning stays visible whichever loop is in use. Errors are
14
+ * the store's own, verbatim: `UnknownHashError` for a hash never elided,
15
+ * `StoreCorruptionError` for bytes that no longer hash to their name — distinct
16
+ * texts, both exiting {@link EXIT.refused}.
17
+ */
18
+ /**
19
+ * `smelt retrieve <hash>` — parsed. A sibling shape, not a field on somebody else's:
20
+ * nothing but the hash, because the command's whole contract is "hash in, exact bytes
21
+ * out" — the same contract as the `smelt_retrieve` tool, reachable from a shell.
22
+ */
23
+ export interface RetrieveInvocation {
24
+ readonly mode: 'retrieve';
25
+ /** The hash exactly as the marker printed it. Validated by the store, not here. */
26
+ readonly hash: string;
27
+ }
28
+ /**
29
+ * Everything `smelt retrieve` and `smelt stats` need from the config, fully merged:
30
+ * the persistent store's directory, already resolved against the config file. The
31
+ * third `Resolved*Run`, and the only one two verbs share — because they share the
32
+ * whole of it: no budget, no strategy, no file leg at all, only the store leg, which
33
+ * is config-only. `stats` resolves through {@link resolveStoreRun} rather than
34
+ * restating it.
35
+ */
36
+ export interface ResolvedStoreRun {
37
+ /** Absolute path of the directory store, resolved against the config file. */
38
+ readonly storePath: string;
39
+ }
40
+ /**
41
+ * What `retrieve` runs on: the shared store leg, plus the hash this invocation named.
42
+ * A composition rather than a wider {@link ResolvedStoreRun}, because `stats` shares
43
+ * the store leg and has no hash — a field that is meaningless for half its users is
44
+ * how a shared struct starts lying.
45
+ */
46
+ export interface ResolvedRetrieveRun {
47
+ readonly store: ResolvedStoreRun;
48
+ readonly hash: string;
49
+ }
50
+ export declare const retrieveCommand: Subcommand<RetrieveInvocation, ResolvedRetrieveRun>;
51
+ /**
52
+ * The store leg alone, for the two commands whose entire job is the store between
53
+ * runs. The refusal is the point: `retrieve` exists so the marker's
54
+ * `retrieve("hash")` works from a later shell — cross-run retrieval — and a memory
55
+ * store dies with the process that filled it, so with a memory store (or no config
56
+ * at all) there is nothing those commands could honestly read. Answering with
57
+ * `UnknownHashError` or all-zero stats instead would be the quiet wrong answer this
58
+ * project refuses everywhere: the hash *was* elided, the counters *did* move — in a
59
+ * store that no longer exists.
60
+ *
61
+ * **This is a policy, not a law, which is why it stays here and is not exported.** The
62
+ * shared half — a config's store decision (`configuredStore`) and opening it
63
+ * (`openStore` in `ops/inputs.ts`) — is what every consumer needs and now what every
64
+ * consumer imports. This function is the CLI's ruling *on top of* that decision, and
65
+ * the MCP server deliberately rules the other way: it accepts a memory store, serves
66
+ * the whole session from it, and says how to get persistence at the moment an unknown
67
+ * hash makes the difference visible. Exporting this would offer that server the CLI's
68
+ * refusal wearing the name of a shared law — the fork this seam exists to end, running
69
+ * in the other direction.
70
+ *
71
+ * @throws {CliUsageError} when no config exists, or the configured store is memory.
72
+ */
73
+ export declare function resolveStoreRun(command: 'retrieve' | 'stats', config: LoadedConfig | undefined): ResolvedStoreRun;
74
+ //# sourceMappingURL=retrieve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retrieve.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/retrieve.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKjD,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;;;;;;;GAcG;AAEH;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,EAAE,UAAU,CAAC,kBAAkB,EAAE,mBAAmB,CAgD/E,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,UAAU,GAAG,OAAO,EAC7B,MAAM,EAAE,YAAY,GAAG,SAAS,GAC/B,gBAAgB,CAgBlB","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { openStore } from '../../ops/inputs.ts';\nimport { retrieveBytes } from '../../ops/verbs.ts';\nimport { CONFIG_FILE_NAME, configuredStore } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt retrieve <hash>` — the marker's `retrieve(\"hash\")`, as a real command.\n *\n * The exact original bytes go to stdout and **nothing else does**: no report, no\n * trailing newline the store did not hold, no re-encoding. Trailing-newline fidelity\n * is not pedantry — the retrieved bytes get spliced back into reasoning about the\n * original, and an almost-right blob is the silent wrong answer this project refuses.\n *\n * The retrieval IS counted — that is the point. `store.retrieve()` journals the hit,\n * so an agent working from pure shell moves the same `expansionRate` a tool-calling\n * consumer moves, and over-pruning stays visible whichever loop is in use. Errors are\n * the store's own, verbatim: `UnknownHashError` for a hash never elided,\n * `StoreCorruptionError` for bytes that no longer hash to their name — distinct\n * texts, both exiting {@link EXIT.refused}.\n */\n\n/**\n * `smelt retrieve <hash>` — parsed. A sibling shape, not a field on somebody else's:\n * nothing but the hash, because the command's whole contract is \"hash in, exact bytes\n * out\" — the same contract as the `smelt_retrieve` tool, reachable from a shell.\n */\nexport interface RetrieveInvocation {\n readonly mode: 'retrieve';\n /** The hash exactly as the marker printed it. Validated by the store, not here. */\n readonly hash: string;\n}\n\n/**\n * Everything `smelt retrieve` and `smelt stats` need from the config, fully merged:\n * the persistent store's directory, already resolved against the config file. The\n * third `Resolved*Run`, and the only one two verbs share — because they share the\n * whole of it: no budget, no strategy, no file leg at all, only the store leg, which\n * is config-only. `stats` resolves through {@link resolveStoreRun} rather than\n * restating it.\n */\nexport interface ResolvedStoreRun {\n /** Absolute path of the directory store, resolved against the config file. */\n readonly storePath: string;\n}\n\n/**\n * What `retrieve` runs on: the shared store leg, plus the hash this invocation named.\n * A composition rather than a wider {@link ResolvedStoreRun}, because `stats` shares\n * the store leg and has no hash — a field that is meaningless for half its users is\n * how a shared struct starts lying.\n */\nexport interface ResolvedRetrieveRun {\n readonly store: ResolvedStoreRun;\n readonly hash: string;\n}\n\nexport const retrieveCommand: Subcommand<RetrieveInvocation, ResolvedRetrieveRun> = {\n name: 'retrieve',\n flags: [],\n refusal:\n `retrieve prints the exact original bytes for one hash, nothing else — even ` +\n `--json would wrap what must come back verbatim.`,\n usage: {\n synopsis: ['retrieve <hash>'],\n section: {\n heading: 'RETRIEVE & STATS',\n body:\n ` Every marker carries the hash of the bytes it replaced — <<smelt/v1: … —\\n` +\n ` retrieve(\"hash\")>> — and the marker's retrieve(\"hash\") is this command:\\n` +\n ` ${CLI_NAME} retrieve <hash> prints the exact original bytes on stdout, byte for\\n` +\n ` byte, nothing else. That closes the loop from pure shell: an agent that got a\\n` +\n ` marker asks for the bytes back with a command instead of a tool call, and the\\n` +\n ` retrieval is counted — asking for material back is exactly what the expansion\\n` +\n ` rate measures. An unknown hash and damaged bytes are distinct refusals (exit 3):\\n` +\n ` \"never elided\" and \"the store was corrupted\" call for different responses.`,\n },\n },\n\n /**\n * The hash and nothing else. Every flag is refused by the registry's one generated\n * message rather than ignored — the command prints the exact original bytes on\n * stdout and nothing more, so a flag that changed the output would break the one\n * contract it has, and a flag silently dropped would be a setting the user believed\n * was in force.\n */\n parse(_values: FlagValues, positionals: readonly string[]): RetrieveInvocation {\n if (positionals.length !== 2) {\n throw new CliUsageError(\n `${CLI_NAME}: retrieve needs exactly one hash — the one a marker printed.\\n` +\n ` ${CLI_NAME} retrieve 84998967370f38bc`,\n );\n }\n return { mode: 'retrieve', hash: positionals[1]! };\n },\n\n resolve(invocation: RetrieveInvocation, config: ConfigSource): ResolvedRetrieveRun {\n return { store: resolveStoreRun('retrieve', config()), hash: invocation.hash };\n },\n\n run(resolved: ResolvedRetrieveRun, io: CliIo): number {\n const store = openStore({ kind: 'directory', path: resolved.store.storePath });\n io.stdout(retrieveBytes({ store, hash: resolved.hash }));\n return EXIT.ok;\n },\n};\n\n/**\n * The store leg alone, for the two commands whose entire job is the store between\n * runs. The refusal is the point: `retrieve` exists so the marker's\n * `retrieve(\"hash\")` works from a later shell — cross-run retrieval — and a memory\n * store dies with the process that filled it, so with a memory store (or no config\n * at all) there is nothing those commands could honestly read. Answering with\n * `UnknownHashError` or all-zero stats instead would be the quiet wrong answer this\n * project refuses everywhere: the hash *was* elided, the counters *did* move — in a\n * store that no longer exists.\n *\n * **This is a policy, not a law, which is why it stays here and is not exported.** The\n * shared half — a config's store decision (`configuredStore`) and opening it\n * (`openStore` in `ops/inputs.ts`) — is what every consumer needs and now what every\n * consumer imports. This function is the CLI's ruling *on top of* that decision, and\n * the MCP server deliberately rules the other way: it accepts a memory store, serves\n * the whole session from it, and says how to get persistence at the moment an unknown\n * hash makes the difference visible. Exporting this would offer that server the CLI's\n * refusal wearing the name of a shared law — the fork this seam exists to end, running\n * in the other direction.\n *\n * @throws {CliUsageError} when no config exists, or the configured store is memory.\n */\nexport function resolveStoreRun(\n command: 'retrieve' | 'stats',\n config: LoadedConfig | undefined,\n): ResolvedStoreRun {\n const store = configuredStore(config);\n if (store.kind !== 'directory') {\n const state =\n config === undefined\n ? `there is no ${CONFIG_FILE_NAME} here`\n : `the ${CONFIG_FILE_NAME} at ${config.path} uses a memory store`;\n throw new CliUsageError(\n `${CLI_NAME}: ${command} needs a persistent store, and ${state}. Cross-run ` +\n `retrieval is the point of the store: a memory store dies with the process ` +\n `that made it, so a marker's hash from an earlier run names bytes this run ` +\n `never held. Configure {\"store\": {\"kind\": \"directory\", \"path\": …}} in ` +\n `${CONFIG_FILE_NAME} — \\`${CLI_NAME} init\\` writes one.`,\n );\n }\n return { storePath: store.path };\n}\n"]}
@@ -0,0 +1,84 @@
1
+ import { CliUsageError } from '../../errors.js';
2
+ import { openStore } from '../../ops/inputs.js';
3
+ import { retrieveBytes } from '../../ops/verbs.js';
4
+ import { CONFIG_FILE_NAME, configuredStore } from '../config.js';
5
+ import { CLI_NAME, EXIT } from '../shell.js';
6
+ export const retrieveCommand = {
7
+ name: 'retrieve',
8
+ flags: [],
9
+ refusal: `retrieve prints the exact original bytes for one hash, nothing else — even ` +
10
+ `--json would wrap what must come back verbatim.`,
11
+ usage: {
12
+ synopsis: ['retrieve <hash>'],
13
+ section: {
14
+ heading: 'RETRIEVE & STATS',
15
+ body: ` Every marker carries the hash of the bytes it replaced — <<smelt/v1: … —\n` +
16
+ ` retrieve("hash")>> — and the marker's retrieve("hash") is this command:\n` +
17
+ ` ${CLI_NAME} retrieve <hash> prints the exact original bytes on stdout, byte for\n` +
18
+ ` byte, nothing else. That closes the loop from pure shell: an agent that got a\n` +
19
+ ` marker asks for the bytes back with a command instead of a tool call, and the\n` +
20
+ ` retrieval is counted — asking for material back is exactly what the expansion\n` +
21
+ ` rate measures. An unknown hash and damaged bytes are distinct refusals (exit 3):\n` +
22
+ ` "never elided" and "the store was corrupted" call for different responses.`,
23
+ },
24
+ },
25
+ /**
26
+ * The hash and nothing else. Every flag is refused by the registry's one generated
27
+ * message rather than ignored — the command prints the exact original bytes on
28
+ * stdout and nothing more, so a flag that changed the output would break the one
29
+ * contract it has, and a flag silently dropped would be a setting the user believed
30
+ * was in force.
31
+ */
32
+ parse(_values, positionals) {
33
+ if (positionals.length !== 2) {
34
+ throw new CliUsageError(`${CLI_NAME}: retrieve needs exactly one hash — the one a marker printed.\n` +
35
+ ` ${CLI_NAME} retrieve 84998967370f38bc`);
36
+ }
37
+ return { mode: 'retrieve', hash: positionals[1] };
38
+ },
39
+ resolve(invocation, config) {
40
+ return { store: resolveStoreRun('retrieve', config()), hash: invocation.hash };
41
+ },
42
+ run(resolved, io) {
43
+ const store = openStore({ kind: 'directory', path: resolved.store.storePath });
44
+ io.stdout(retrieveBytes({ store, hash: resolved.hash }));
45
+ return EXIT.ok;
46
+ },
47
+ };
48
+ /**
49
+ * The store leg alone, for the two commands whose entire job is the store between
50
+ * runs. The refusal is the point: `retrieve` exists so the marker's
51
+ * `retrieve("hash")` works from a later shell — cross-run retrieval — and a memory
52
+ * store dies with the process that filled it, so with a memory store (or no config
53
+ * at all) there is nothing those commands could honestly read. Answering with
54
+ * `UnknownHashError` or all-zero stats instead would be the quiet wrong answer this
55
+ * project refuses everywhere: the hash *was* elided, the counters *did* move — in a
56
+ * store that no longer exists.
57
+ *
58
+ * **This is a policy, not a law, which is why it stays here and is not exported.** The
59
+ * shared half — a config's store decision (`configuredStore`) and opening it
60
+ * (`openStore` in `ops/inputs.ts`) — is what every consumer needs and now what every
61
+ * consumer imports. This function is the CLI's ruling *on top of* that decision, and
62
+ * the MCP server deliberately rules the other way: it accepts a memory store, serves
63
+ * the whole session from it, and says how to get persistence at the moment an unknown
64
+ * hash makes the difference visible. Exporting this would offer that server the CLI's
65
+ * refusal wearing the name of a shared law — the fork this seam exists to end, running
66
+ * in the other direction.
67
+ *
68
+ * @throws {CliUsageError} when no config exists, or the configured store is memory.
69
+ */
70
+ export function resolveStoreRun(command, config) {
71
+ const store = configuredStore(config);
72
+ if (store.kind !== 'directory') {
73
+ const state = config === undefined
74
+ ? `there is no ${CONFIG_FILE_NAME} here`
75
+ : `the ${CONFIG_FILE_NAME} at ${config.path} uses a memory store`;
76
+ throw new CliUsageError(`${CLI_NAME}: ${command} needs a persistent store, and ${state}. Cross-run ` +
77
+ `retrieval is the point of the store: a memory store dies with the process ` +
78
+ `that made it, so a marker's hash from an earlier run names bytes this run ` +
79
+ `never held. Configure {"store": {"kind": "directory", "path": …}} in ` +
80
+ `${CONFIG_FILE_NAME} — \`${CLI_NAME} init\` writes one.`);
81
+ }
82
+ return { storePath: store.path };
83
+ }
84
+ //# sourceMappingURL=retrieve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retrieve.js","sourceRoot":"","sources":["../../../src/cli/subcommands/retrieve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAyD7C,MAAM,CAAC,MAAM,eAAe,GAAwD;IAClF,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,EAAE;IACT,OAAO,EACL,6EAA6E;QAC7E,iDAAiD;IACnD,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,iBAAiB,CAAC;QAC7B,OAAO,EAAE;YACP,OAAO,EAAE,kBAAkB;YAC3B,IAAI,EACF,8EAA8E;gBAC9E,6EAA6E;gBAC7E,KAAK,QAAQ,wEAAwE;gBACrF,mFAAmF;gBACnF,mFAAmF;gBACnF,mFAAmF;gBACnF,sFAAsF;gBACtF,8EAA8E;SACjF;KACF;IAED;;;;;;OAMG;IACH,KAAK,CAAC,OAAmB,EAAE,WAA8B;QACvD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,iEAAiE;gBAC1E,KAAK,QAAQ,4BAA4B,CAC5C,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAE,EAAE,CAAC;IACrD,CAAC;IAED,OAAO,CAAC,UAA8B,EAAE,MAAoB;QAC1D,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;IACjF,CAAC;IAED,GAAG,CAAC,QAA6B,EAAE,EAAS;QAC1C,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;QAC/E,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;CACF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,eAAe,CAC7B,OAA6B,EAC7B,MAAgC;IAEhC,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC/B,MAAM,KAAK,GACT,MAAM,KAAK,SAAS;YAClB,CAAC,CAAC,eAAe,gBAAgB,OAAO;YACxC,CAAC,CAAC,OAAO,gBAAgB,OAAO,MAAM,CAAC,IAAI,sBAAsB,CAAC;QACtE,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,KAAK,OAAO,kCAAkC,KAAK,cAAc;YAC1E,4EAA4E;YAC5E,4EAA4E;YAC5E,uEAAuE;YACvE,GAAG,gBAAgB,QAAQ,QAAQ,qBAAqB,CAC3D,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;AACnC,CAAC","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { openStore } from '../../ops/inputs.ts';\nimport { retrieveBytes } from '../../ops/verbs.ts';\nimport { CONFIG_FILE_NAME, configuredStore } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt retrieve <hash>` — the marker's `retrieve(\"hash\")`, as a real command.\n *\n * The exact original bytes go to stdout and **nothing else does**: no report, no\n * trailing newline the store did not hold, no re-encoding. Trailing-newline fidelity\n * is not pedantry — the retrieved bytes get spliced back into reasoning about the\n * original, and an almost-right blob is the silent wrong answer this project refuses.\n *\n * The retrieval IS counted — that is the point. `store.retrieve()` journals the hit,\n * so an agent working from pure shell moves the same `expansionRate` a tool-calling\n * consumer moves, and over-pruning stays visible whichever loop is in use. Errors are\n * the store's own, verbatim: `UnknownHashError` for a hash never elided,\n * `StoreCorruptionError` for bytes that no longer hash to their name — distinct\n * texts, both exiting {@link EXIT.refused}.\n */\n\n/**\n * `smelt retrieve <hash>` — parsed. A sibling shape, not a field on somebody else's:\n * nothing but the hash, because the command's whole contract is \"hash in, exact bytes\n * out\" — the same contract as the `smelt_retrieve` tool, reachable from a shell.\n */\nexport interface RetrieveInvocation {\n readonly mode: 'retrieve';\n /** The hash exactly as the marker printed it. Validated by the store, not here. */\n readonly hash: string;\n}\n\n/**\n * Everything `smelt retrieve` and `smelt stats` need from the config, fully merged:\n * the persistent store's directory, already resolved against the config file. The\n * third `Resolved*Run`, and the only one two verbs share — because they share the\n * whole of it: no budget, no strategy, no file leg at all, only the store leg, which\n * is config-only. `stats` resolves through {@link resolveStoreRun} rather than\n * restating it.\n */\nexport interface ResolvedStoreRun {\n /** Absolute path of the directory store, resolved against the config file. */\n readonly storePath: string;\n}\n\n/**\n * What `retrieve` runs on: the shared store leg, plus the hash this invocation named.\n * A composition rather than a wider {@link ResolvedStoreRun}, because `stats` shares\n * the store leg and has no hash — a field that is meaningless for half its users is\n * how a shared struct starts lying.\n */\nexport interface ResolvedRetrieveRun {\n readonly store: ResolvedStoreRun;\n readonly hash: string;\n}\n\nexport const retrieveCommand: Subcommand<RetrieveInvocation, ResolvedRetrieveRun> = {\n name: 'retrieve',\n flags: [],\n refusal:\n `retrieve prints the exact original bytes for one hash, nothing else — even ` +\n `--json would wrap what must come back verbatim.`,\n usage: {\n synopsis: ['retrieve <hash>'],\n section: {\n heading: 'RETRIEVE & STATS',\n body:\n ` Every marker carries the hash of the bytes it replaced — <<smelt/v1: … —\\n` +\n ` retrieve(\"hash\")>> — and the marker's retrieve(\"hash\") is this command:\\n` +\n ` ${CLI_NAME} retrieve <hash> prints the exact original bytes on stdout, byte for\\n` +\n ` byte, nothing else. That closes the loop from pure shell: an agent that got a\\n` +\n ` marker asks for the bytes back with a command instead of a tool call, and the\\n` +\n ` retrieval is counted — asking for material back is exactly what the expansion\\n` +\n ` rate measures. An unknown hash and damaged bytes are distinct refusals (exit 3):\\n` +\n ` \"never elided\" and \"the store was corrupted\" call for different responses.`,\n },\n },\n\n /**\n * The hash and nothing else. Every flag is refused by the registry's one generated\n * message rather than ignored — the command prints the exact original bytes on\n * stdout and nothing more, so a flag that changed the output would break the one\n * contract it has, and a flag silently dropped would be a setting the user believed\n * was in force.\n */\n parse(_values: FlagValues, positionals: readonly string[]): RetrieveInvocation {\n if (positionals.length !== 2) {\n throw new CliUsageError(\n `${CLI_NAME}: retrieve needs exactly one hash — the one a marker printed.\\n` +\n ` ${CLI_NAME} retrieve 84998967370f38bc`,\n );\n }\n return { mode: 'retrieve', hash: positionals[1]! };\n },\n\n resolve(invocation: RetrieveInvocation, config: ConfigSource): ResolvedRetrieveRun {\n return { store: resolveStoreRun('retrieve', config()), hash: invocation.hash };\n },\n\n run(resolved: ResolvedRetrieveRun, io: CliIo): number {\n const store = openStore({ kind: 'directory', path: resolved.store.storePath });\n io.stdout(retrieveBytes({ store, hash: resolved.hash }));\n return EXIT.ok;\n },\n};\n\n/**\n * The store leg alone, for the two commands whose entire job is the store between\n * runs. The refusal is the point: `retrieve` exists so the marker's\n * `retrieve(\"hash\")` works from a later shell — cross-run retrieval — and a memory\n * store dies with the process that filled it, so with a memory store (or no config\n * at all) there is nothing those commands could honestly read. Answering with\n * `UnknownHashError` or all-zero stats instead would be the quiet wrong answer this\n * project refuses everywhere: the hash *was* elided, the counters *did* move — in a\n * store that no longer exists.\n *\n * **This is a policy, not a law, which is why it stays here and is not exported.** The\n * shared half — a config's store decision (`configuredStore`) and opening it\n * (`openStore` in `ops/inputs.ts`) — is what every consumer needs and now what every\n * consumer imports. This function is the CLI's ruling *on top of* that decision, and\n * the MCP server deliberately rules the other way: it accepts a memory store, serves\n * the whole session from it, and says how to get persistence at the moment an unknown\n * hash makes the difference visible. Exporting this would offer that server the CLI's\n * refusal wearing the name of a shared law — the fork this seam exists to end, running\n * in the other direction.\n *\n * @throws {CliUsageError} when no config exists, or the configured store is memory.\n */\nexport function resolveStoreRun(\n command: 'retrieve' | 'stats',\n config: LoadedConfig | undefined,\n): ResolvedStoreRun {\n const store = configuredStore(config);\n if (store.kind !== 'directory') {\n const state =\n config === undefined\n ? `there is no ${CONFIG_FILE_NAME} here`\n : `the ${CONFIG_FILE_NAME} at ${config.path} uses a memory store`;\n throw new CliUsageError(\n `${CLI_NAME}: ${command} needs a persistent store, and ${state}. Cross-run ` +\n `retrieval is the point of the store: a memory store dies with the process ` +\n `that made it, so a marker's hash from an earlier run names bytes this run ` +\n `never held. Configure {\"store\": {\"kind\": \"directory\", \"path\": …}} in ` +\n `${CONFIG_FILE_NAME} — \\`${CLI_NAME} init\\` writes one.`,\n );\n }\n return { storePath: store.path };\n}\n"]}
@@ -0,0 +1,107 @@
1
+ import type { Strategy } from '../../plan/planners.ts';
2
+ import type { DetectedLanguage, SmeltResult } from '../../types.ts';
3
+ import type { ConfiguredStore, LoadedConfig } from '../config.ts';
4
+ import type { Subcommand } from './subcommand.ts';
5
+ /**
6
+ * The default verb: `smelt <file>` and `smelt < input`, plus the `--reconstruct` round
7
+ * trip that reads one of its own `--json` envelopes back.
8
+ *
9
+ * Two jobs, one verb, which is why USAGE lists it twice: `--reconstruct` reads a file
10
+ * this command wrote and puts every byte back, so it takes no budget, no focus and no
11
+ * strategy — but it is the same front door, and giving it a subcommand word would
12
+ * break every script that types `smelt --reconstruct`.
13
+ */
14
+ /** What the CLI was asked to do. Pure data, so the parse is testable on its own. */
15
+ export interface SmeltInvocation {
16
+ readonly mode: 'smelt' | 'reconstruct' | 'help' | 'version' | 'init';
17
+ /** Path to read. `undefined` means stdin. */
18
+ readonly file?: string;
19
+ /**
20
+ * UTF-8 bytes. `undefined` in `'smelt'` mode means the flag was not given; the
21
+ * runner then consults `smelt.config.json` and errors if that has no default either.
22
+ */
23
+ readonly budgetBytes?: number;
24
+ readonly focus: readonly string[];
25
+ readonly language?: DetectedLanguage;
26
+ /** `undefined` means the flag was not given — the config default may apply. */
27
+ readonly strategy?: Strategy;
28
+ readonly json: boolean;
29
+ }
30
+ /**
31
+ * Everything one smelt run needs, fully merged — with a receipt for where each
32
+ * merged value came from.
33
+ *
34
+ * This is the verb's single merge of flags + config + built-ins. Precedence lives
35
+ * here and nowhere else: `resolveRun` is the only code that may look at a flag and a
36
+ * config default side by side, so a precedence question is always answered by one
37
+ * function instead of by reading two files. `runSmelt` executes this object
38
+ * straight-line, without a `??` of its own.
39
+ */
40
+ export interface ResolvedRun {
41
+ readonly budgetBytes: number;
42
+ /** Where the budget came from. A missing budget never gets here — it throws. */
43
+ readonly budgetSource: 'flag' | 'config';
44
+ readonly strategy: Strategy;
45
+ readonly strategySource: 'flag' | 'config' | 'builtin';
46
+ /**
47
+ * The store decision, with `path` already resolved against the config file's
48
+ * directory. `'memory'` is the built-in default — a fresh in-memory store per run.
49
+ */
50
+ readonly store: ConfiguredStore;
51
+ /** Path to read. `undefined` means stdin. Flags only; the config has no say. */
52
+ readonly file?: string;
53
+ readonly focus: readonly string[];
54
+ readonly language?: DetectedLanguage;
55
+ readonly json: boolean;
56
+ }
57
+ /**
58
+ * The default verb's two resolved shapes.
59
+ *
60
+ * `--reconstruct` merges nothing: there is no budget to default, no strategy to pick
61
+ * and no store to choose, because the envelope carries its own bytes. Saying that in
62
+ * the type is cheaper than a {@link ResolvedRun} whose every field would be a lie for
63
+ * half of this verb's runs.
64
+ */
65
+ export type ResolvedSmeltCommand = {
66
+ readonly kind: 'smelt';
67
+ readonly run: ResolvedRun;
68
+ } | {
69
+ readonly kind: 'reconstruct';
70
+ readonly file?: string;
71
+ };
72
+ /**
73
+ * The `--json` envelope format, versioned like the marker for the same reason: this is
74
+ * a surface other programs parse, so a change to it has to be identifiable rather than
75
+ * silent.
76
+ */
77
+ export declare const CLI_JSON_FORMAT = "smelt-cli/v1";
78
+ /**
79
+ * What `--json` prints, and what `--reconstruct` reads back.
80
+ *
81
+ * `result` is the {@link SmeltResult} **verbatim** — nothing renamed, nothing dropped —
82
+ * so it can be diffed in a test. `elided` is the other half, and it is here because a
83
+ * result on its own is *not* reconstructible: Law 3 is a property of the result plus
84
+ * the store that holds its bytes, and a file claiming to prove the round trip while
85
+ * carrying only half of it would prove nothing.
86
+ */
87
+ export interface CliJsonEnvelope {
88
+ readonly format: string;
89
+ readonly result: SmeltResult;
90
+ /** hash → the exact elided bytes. Keys match `result.elisions[].hash`. */
91
+ readonly elided: Readonly<Record<string, string>>;
92
+ }
93
+ export declare const smeltCommand: Subcommand<SmeltInvocation, ResolvedSmeltCommand>;
94
+ /**
95
+ * Merge one `'smelt'`-mode invocation with the loaded config (or `undefined` when no
96
+ * `smelt.config.json` exists) and the built-in defaults.
97
+ *
98
+ * The precedence is strict and one-directional: an explicit flag always wins over the
99
+ * config, and the config only fills what the flags left unsaid. Built-ins fill last,
100
+ * and only where a built-in exists at all — the budget deliberately has none, so a run
101
+ * with no budget from either source is refused here, in the one function that owns that
102
+ * error.
103
+ *
104
+ * @throws {CliUsageError} when neither `--budget` nor the config names a budget.
105
+ */
106
+ export declare function resolveRun(invocation: SmeltInvocation, config: LoadedConfig | undefined): ResolvedRun;
107
+ //# sourceMappingURL=smelt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smelt.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/smelt.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,KAAK,EAAE,gBAAgB,EAAgB,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElF,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAOlE,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;GAQG;AAEH,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACrE,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACvD;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D;;;;GAIG;AACH,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,oBAAoB,CA8D1E,CAAC;AAqDF;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,YAAY,GAAG,SAAS,GAC/B,WAAW,CA6Bb","sourcesContent":["import { reconstruct } from '../../apply.ts';\nimport { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError, SmeltError } from '../../errors.ts';\nimport { budgetRequired, openStore, readBlob, resolveStrategy } from '../../ops/inputs.ts';\nimport { smeltBlob } from '../../ops/verbs.ts';\nimport { isStrategy, STRATEGIES } from '../../plan/planners.ts';\nimport type { Strategy } from '../../plan/planners.ts';\nimport { MemoryElisionStore } from '../../store.ts';\nimport type { DetectedLanguage, ElisionStore, SmeltResult } from '../../types.ts';\nimport { CONFIG_FILE_NAME, configuredStore } from '../config.ts';\nimport type { ConfiguredStore, LoadedConfig } from '../config.ts';\nimport { formatReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport { flagList, parseBudget, VERB_FLAGS } from './flags.ts';\nimport type { FlagValues, VerbFlag } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * The default verb: `smelt <file>` and `smelt < input`, plus the `--reconstruct` round\n * trip that reads one of its own `--json` envelopes back.\n *\n * Two jobs, one verb, which is why USAGE lists it twice: `--reconstruct` reads a file\n * this command wrote and puts every byte back, so it takes no budget, no focus and no\n * strategy — but it is the same front door, and giving it a subcommand word would\n * break every script that types `smelt --reconstruct`.\n */\n\n/** What the CLI was asked to do. Pure data, so the parse is testable on its own. */\nexport interface SmeltInvocation {\n readonly mode: 'smelt' | 'reconstruct' | 'help' | 'version' | 'init';\n /** Path to read. `undefined` means stdin. */\n readonly file?: string;\n /**\n * UTF-8 bytes. `undefined` in `'smelt'` mode means the flag was not given; the\n * runner then consults `smelt.config.json` and errors if that has no default either.\n */\n readonly budgetBytes?: number;\n readonly focus: readonly string[];\n readonly language?: DetectedLanguage;\n /** `undefined` means the flag was not given — the config default may apply. */\n readonly strategy?: Strategy;\n readonly json: boolean;\n}\n\n/**\n * Everything one smelt run needs, fully merged — with a receipt for where each\n * merged value came from.\n *\n * This is the verb's single merge of flags + config + built-ins. Precedence lives\n * here and nowhere else: `resolveRun` is the only code that may look at a flag and a\n * config default side by side, so a precedence question is always answered by one\n * function instead of by reading two files. `runSmelt` executes this object\n * straight-line, without a `??` of its own.\n */\nexport interface ResolvedRun {\n readonly budgetBytes: number;\n /** Where the budget came from. A missing budget never gets here — it throws. */\n readonly budgetSource: 'flag' | 'config';\n readonly strategy: Strategy;\n readonly strategySource: 'flag' | 'config' | 'builtin';\n /**\n * The store decision, with `path` already resolved against the config file's\n * directory. `'memory'` is the built-in default — a fresh in-memory store per run.\n */\n readonly store: ConfiguredStore;\n /** Path to read. `undefined` means stdin. Flags only; the config has no say. */\n readonly file?: string;\n readonly focus: readonly string[];\n readonly language?: DetectedLanguage;\n readonly json: boolean;\n}\n\n/**\n * The default verb's two resolved shapes.\n *\n * `--reconstruct` merges nothing: there is no budget to default, no strategy to pick\n * and no store to choose, because the envelope carries its own bytes. Saying that in\n * the type is cheaper than a {@link ResolvedRun} whose every field would be a lie for\n * half of this verb's runs.\n */\nexport type ResolvedSmeltCommand =\n | { readonly kind: 'smelt'; readonly run: ResolvedRun }\n | { readonly kind: 'reconstruct'; readonly file?: string };\n\n/**\n * The `--json` envelope format, versioned like the marker for the same reason: this is\n * a surface other programs parse, so a change to it has to be identifiable rather than\n * silent.\n */\nexport const CLI_JSON_FORMAT = 'smelt-cli/v1';\n\n/**\n * What `--json` prints, and what `--reconstruct` reads back.\n *\n * `result` is the {@link SmeltResult} **verbatim** — nothing renamed, nothing dropped —\n * so it can be diffed in a test. `elided` is the other half, and it is here because a\n * result on its own is *not* reconstructible: Law 3 is a property of the result plus\n * the store that holds its bytes, and a file claiming to prove the round trip while\n * carrying only half of it would prove nothing.\n */\nexport interface CliJsonEnvelope {\n readonly format: string;\n readonly result: SmeltResult;\n /** hash → the exact elided bytes. Keys match `result.elisions[].hash`. */\n readonly elided: Readonly<Record<string, string>>;\n}\n\nexport const smeltCommand: Subcommand<SmeltInvocation, ResolvedSmeltCommand> = {\n name: 'smelt',\n flags: ['budget', 'focus', 'language', 'strategy', 'json', 'reconstruct'],\n refusal:\n `A single-blob run reads one file or stdin; there is no tree to walk, ` +\n `nothing to cache, and no harness to install into.`,\n usage: {\n synopsis: [\n '<file> --budget <bytes> [--focus <term>]...',\n '--budget <bytes> [--focus <term>]... < input',\n ],\n occasional: ['--reconstruct <result.json>', '--reconstruct < result.json'],\n },\n\n parse(values: FlagValues, positionals: readonly string[]): SmeltInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: expected at most one file, got ${String(positionals.length)} ` +\n `(${positionals.join(', ')}). smelt reads one blob at a time.`,\n );\n }\n const file = positionals[0];\n\n if (values.reconstruct === true) {\n refuseReconstructFlags(values);\n return {\n mode: 'reconstruct',\n ...(file === undefined ? {} : { file }),\n focus: [],\n json: false,\n };\n }\n\n const budgetBytes = parseBudget(values.budget);\n const chosenStrategy = parseStrategy(values.strategy);\n return {\n mode: 'smelt',\n ...(file === undefined ? {} : { file }),\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n focus: values.focus ?? [],\n ...(values.language === undefined ? {} : { language: parseLanguage(values.language) }),\n ...(chosenStrategy === undefined ? {} : { strategy: chosenStrategy }),\n json: values.json === true,\n };\n },\n\n resolve(invocation: SmeltInvocation, config: ConfigSource): ResolvedSmeltCommand {\n if (invocation.mode === 'reconstruct') {\n return {\n kind: 'reconstruct',\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n };\n }\n return { kind: 'smelt', run: resolveRun(invocation, config()) };\n },\n\n run(resolved: ResolvedSmeltCommand, io: CliIo): number | Promise<number> {\n if (resolved.kind === 'reconstruct') {\n return runReconstruct(readInput(resolved.file, io), io);\n }\n return runSmelt(resolved.run, io);\n },\n};\n\n/**\n * Every flag this verb owns that `--reconstruct` cannot honour, with the reason each\n * one makes no sense — the second job's flag ownership, which the registry cannot\n * express because both jobs are the same verb.\n *\n * `refuseForeignFlags` refuses a flag no verb owns *here*; it cannot refuse `--focus`\n * on a single-blob run, because a single-blob run is exactly where `--focus` belongs.\n * So the ones the round trip ignores are named here instead, and named exhaustively:\n * every flag on `smeltCommand.flags` except `--reconstruct` itself has an entry, which\n * `test/guards/subcommand-registry.test.ts` crosses. A flag added to this verb and\n * forgotten here would be silently ignored by half of the verb's runs — the failure\n * this table exists to make impossible.\n *\n * Key order is the flag table's order, so a refusal lists flags the way every other\n * refusal does.\n */\nconst RECONSTRUCT_REFUSALS = {\n budget: `Reconstruction puts every byte back; there is nothing to fit.`,\n focus:\n `Focus decides what survives a cut, and the cut has already been made — ` +\n `the envelope names every elision it took.`,\n language:\n `Nothing is detected or parsed on the way back: the envelope carries the ` +\n `bytes and the ranges the cut recorded.`,\n strategy: `No planner runs on the way back — the elisions come from the envelope.`,\n json:\n `--reconstruct reads a --json envelope and prints the original text; ` +\n `there is no second envelope to write.`,\n} as const satisfies Partial<Record<VerbFlag, string>>;\n\n/**\n * Refuse every flag `--reconstruct` would otherwise ignore, in one message shaped like\n * the ownership refusals: the flags named, then why not here. A {@link CliUsageError},\n * so it exits 2 exactly as every other refusal does.\n *\n * @throws {CliUsageError} naming each offending flag and what the round trip does instead.\n */\nfunction refuseReconstructFlags(values: FlagValues): void {\n const reasons: Partial<Record<VerbFlag, string>> = RECONSTRUCT_REFUSALS;\n const offending = VERB_FLAGS.filter(\n (flag) => reasons[flag] !== undefined && values[flag] !== undefined,\n );\n if (offending.length === 0) return;\n\n const verb = offending.length === 1 ? 'makes' : 'make';\n const why = offending.map((flag) => reasons[flag] ?? '').join(' ');\n throw new CliUsageError(\n `${CLI_NAME}: ${flagList(offending)} ${verb} no sense with --reconstruct. ${why}`,\n );\n}\n\n/**\n * Merge one `'smelt'`-mode invocation with the loaded config (or `undefined` when no\n * `smelt.config.json` exists) and the built-in defaults.\n *\n * The precedence is strict and one-directional: an explicit flag always wins over the\n * config, and the config only fills what the flags left unsaid. Built-ins fill last,\n * and only where a built-in exists at all — the budget deliberately has none, so a run\n * with no budget from either source is refused here, in the one function that owns that\n * error.\n *\n * @throws {CliUsageError} when neither `--budget` nor the config names a budget.\n */\nexport function resolveRun(\n invocation: SmeltInvocation,\n config: LoadedConfig | undefined,\n): ResolvedRun {\n const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;\n if (budgetBytes === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: ` +\n budgetRequired({\n knob: '--budget',\n stake: 'your context to throw away',\n advice:\n `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +\n `(\\`${CLI_NAME} init\\` writes one).\\n` +\n ` ${CLI_NAME} src/server.ts --budget 4000 --focus handleRequest`,\n }),\n );\n }\n\n const strategy = resolveStrategy(invocation.strategy, config?.config.strategy);\n\n return {\n budgetBytes,\n budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',\n strategy: strategy.strategy,\n strategySource: strategy.source,\n store: configuredStore(config),\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n focus: invocation.focus,\n ...(invocation.language === undefined ? {} : { language: invocation.language }),\n json: invocation.json,\n };\n}\n\n/**\n * One smelt run: read the input, hand the resolved values to {@link smeltBlob}, render.\n *\n * All merging — flags versus `smelt.config.json` versus built-ins, including the\n * budget-required refusal — happens in {@link resolveRun}, which is the only place\n * precedence lives. This function reads the resolved object and never consults a flag\n * or a config field directly. The cut itself belongs to no front door: `smeltBlob` in\n * `ops/verbs.ts` builds the smelter and returns the values this function prints, and\n * the `smelt_file` tool calls the same op with its own arguments.\n */\nasync function runSmelt(run: ResolvedRun, io: CliIo): Promise<number> {\n const inputText = readInput(run.file, io);\n\n const outcome = await smeltBlob({\n text: inputText,\n source: run.file ?? '<stdin>',\n budgetBytes: run.budgetBytes,\n strategy: run.strategy,\n store: openStore(run.store),\n ...(run.file === undefined ? {} : { path: run.file }),\n ...(run.language === undefined ? {} : { language: run.language }),\n focus: run.focus,\n });\n\n if (run.json) {\n io.stdout(`${JSON.stringify(envelope(outcome.result, outcome.store), null, 2)}\\n`);\n } else {\n io.stdout(outcome.result.text);\n }\n io.stderr(formatReport(outcome));\n\n return outcome.result.outputBytes > run.budgetBytes ? EXIT.overBudget : EXIT.ok;\n}\n\n/**\n * Law 3, from a shell.\n *\n * This is deliberately not \"print the text and hope\": it rebuilds the store from the\n * envelope, checks every hash against the bytes it claims to key, and checks the\n * reconstructed length against the `inputBytes` the result recorded at the time of the\n * cut. A round trip that quietly returns almost-right text is the failure this whole\n * repository is arranged against.\n */\nfunction runReconstruct(text: string, io: CliIo): number {\n const { result, elided } = parseEnvelope(text);\n const store = new MemoryElisionStore();\n\n for (const [hash, content] of Object.entries(elided)) {\n const actual = store.put(content);\n if (actual !== hash) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is not self-consistent — it stores bytes under ` +\n `\"${hash}\" that actually hash to \"${actual}\". Refusing to reconstruct from it.`,\n );\n }\n }\n for (const elision of result.elisions) {\n if (!store.has(elision.hash)) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is missing the bytes for \"${elision.hash}\". A result ` +\n `without its elided bytes cannot be reconstructed; re-run with --json.`,\n );\n }\n }\n\n const original = reconstruct(result, store);\n const bytes = Buffer.byteLength(original, 'utf8');\n if (bytes !== result.inputBytes) {\n throw new SmeltError(\n `${CLI_NAME}: reconstruction produced ${String(bytes)} bytes but the result ` +\n `recorded ${String(result.inputBytes)}. The round trip did not close.`,\n );\n }\n\n io.stdout(original);\n io.stderr(\n `${CLI_NAME} reconstructed ${String(result.inputBytes)} B from ` +\n `${String(result.elisions.length)} elisions — byte for byte\\n`,\n );\n return EXIT.ok;\n}\n\n/** `peek`, not `retrieve`: writing a file is not the model asking for anything back. */\nfunction envelope(result: SmeltResult, store: ElisionStore): CliJsonEnvelope {\n const elided: Record<string, string> = {};\n for (const elision of result.elisions) {\n const content = store.peek(elision.hash);\n if (content === undefined) {\n throw new SmeltError(\n `${CLI_NAME}: the store does not hold \"${elision.hash}\", which its own result ` +\n `says it elided. Refusing to write an envelope that cannot round-trip.`,\n );\n }\n elided[elision.hash] = content;\n }\n return { format: CLI_JSON_FORMAT, result, elided };\n}\n\nfunction parseEnvelope(text: string): CliJsonEnvelope {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (cause) {\n throw new CliUsageError(\n `${CLI_NAME}: --reconstruct expected a --json envelope, and this is not JSON: ` +\n `${cause instanceof Error ? cause.message : String(cause)}`,\n );\n }\n\n if (typeof value !== 'object' || value === null) {\n throw new CliUsageError(`${CLI_NAME}: --reconstruct expected a JSON object.`);\n }\n const fields = value as Record<string, unknown>;\n\n if (fields['format'] !== CLI_JSON_FORMAT) {\n throw new CliUsageError(\n `${CLI_NAME}: this envelope says format \"${String(fields['format'])}\"; this build ` +\n `reads \"${CLI_JSON_FORMAT}\". Formats are versioned so a mismatch is visible ` +\n `instead of being half-understood.`,\n );\n }\n\n const result = fields['result'];\n if (typeof result !== 'object' || result === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`result\\` object.`);\n }\n const resultFields = result as Record<string, unknown>;\n if (\n typeof resultFields['text'] !== 'string' ||\n typeof resultFields['inputBytes'] !== 'number' ||\n !Array.isArray(resultFields['elisions'])\n ) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope's \\`result\\` is missing text, inputBytes or elisions.`,\n );\n }\n\n const elided = fields['elided'];\n if (typeof elided !== 'object' || elided === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`elided\\` map.`);\n }\n for (const [hash, content] of Object.entries(elided)) {\n if (typeof content !== 'string') {\n throw new CliUsageError(`${CLI_NAME}: envelope's \\`elided[\"${hash}\"]\\` is not a string.`);\n }\n }\n\n return {\n format: CLI_JSON_FORMAT,\n result: result as unknown as SmeltResult,\n elided: elided as Record<string, string>,\n };\n}\n\n/**\n * The blob to work on: stdin when no file was named, otherwise the file — read\n * through the ops law, so \"cannot read X\" says the same thing here and in the\n * `smelt_file` tool. Only the stdin leg is the CLI's own; a tool has no stdin.\n */\nfunction readInput(file: string | undefined, io: CliIo): string {\n if (file === undefined) return io.stdin();\n const read = readBlob(file, file);\n if (!read.ok) throw new CliUsageError(`${CLI_NAME}: ${read.refusal}`);\n return read.value;\n}\n\nfunction parseLanguage(raw: string): DetectedLanguage {\n const known: readonly string[] = [...SUPPORTED_LANGUAGES, 'unknown'];\n if (!known.includes(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --language \"${raw}\". Known: ${known.join(', ')}.`,\n );\n }\n return raw as DetectedLanguage;\n}\n\n/** Membership in the {@link PLANNERS} registry is the whole validation. */\nfunction parseStrategy(raw: string | undefined): Strategy | undefined {\n if (raw === undefined) return undefined;\n if (!isStrategy(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --strategy \"${raw}\". Known: ${STRATEGIES.join(', ')}.`,\n );\n }\n return raw;\n}\n"]}