@smeltjs/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +55 -0
  3. package/THIRD-PARTY.md +140 -0
  4. package/dist/apply.d.ts +145 -0
  5. package/dist/apply.d.ts.map +1 -0
  6. package/dist/apply.js +244 -0
  7. package/dist/apply.js.map +1 -0
  8. package/dist/cache/prefix.d.ts +164 -0
  9. package/dist/cache/prefix.d.ts.map +1 -0
  10. package/dist/cache/prefix.js +361 -0
  11. package/dist/cache/prefix.js.map +1 -0
  12. package/dist/cli/args.d.ts +71 -0
  13. package/dist/cli/args.d.ts.map +1 -0
  14. package/dist/cli/args.js +308 -0
  15. package/dist/cli/args.js.map +1 -0
  16. package/dist/cli/bin.d.ts +3 -0
  17. package/dist/cli/bin.d.ts.map +1 -0
  18. package/dist/cli/bin.js +161 -0
  19. package/dist/cli/bin.js.map +1 -0
  20. package/dist/cli/config.d.ts +73 -0
  21. package/dist/cli/config.d.ts.map +1 -0
  22. package/dist/cli/config.js +144 -0
  23. package/dist/cli/config.js.map +1 -0
  24. package/dist/cli/init.d.ts +56 -0
  25. package/dist/cli/init.d.ts.map +1 -0
  26. package/dist/cli/init.js +449 -0
  27. package/dist/cli/init.js.map +1 -0
  28. package/dist/cli/report.d.ts +44 -0
  29. package/dist/cli/report.d.ts.map +1 -0
  30. package/dist/cli/report.js +113 -0
  31. package/dist/cli/report.js.map +1 -0
  32. package/dist/cli/resolve.d.ts +103 -0
  33. package/dist/cli/resolve.d.ts.map +1 -0
  34. package/dist/cli/resolve.js +106 -0
  35. package/dist/cli/resolve.js.map +1 -0
  36. package/dist/cli/run.d.ts +99 -0
  37. package/dist/cli/run.d.ts.map +1 -0
  38. package/dist/cli/run.js +351 -0
  39. package/dist/cli/run.js.map +1 -0
  40. package/dist/detect.d.ts +16 -0
  41. package/dist/detect.d.ts.map +1 -0
  42. package/dist/detect.js +18 -0
  43. package/dist/detect.js.map +1 -0
  44. package/dist/errors.d.ts +88 -0
  45. package/dist/errors.d.ts.map +1 -0
  46. package/dist/errors.js +107 -0
  47. package/dist/errors.js.map +1 -0
  48. package/dist/hash.d.ts +13 -0
  49. package/dist/hash.d.ts.map +1 -0
  50. package/dist/hash.js +16 -0
  51. package/dist/hash.js.map +1 -0
  52. package/dist/index.d.ts +121 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +93 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/lang/bash.d.ts +3 -0
  57. package/dist/lang/bash.d.ts.map +1 -0
  58. package/dist/lang/bash.js +28 -0
  59. package/dist/lang/bash.js.map +1 -0
  60. package/dist/lang/c.d.ts +3 -0
  61. package/dist/lang/c.d.ts.map +1 -0
  62. package/dist/lang/c.js +39 -0
  63. package/dist/lang/c.js.map +1 -0
  64. package/dist/lang/c_sharp.d.ts +3 -0
  65. package/dist/lang/c_sharp.d.ts.map +1 -0
  66. package/dist/lang/c_sharp.js +37 -0
  67. package/dist/lang/c_sharp.js.map +1 -0
  68. package/dist/lang/cpp.d.ts +3 -0
  69. package/dist/lang/cpp.d.ts.map +1 -0
  70. package/dist/lang/cpp.js +46 -0
  71. package/dist/lang/cpp.js.map +1 -0
  72. package/dist/lang/go.d.ts +3 -0
  73. package/dist/lang/go.d.ts.map +1 -0
  74. package/dist/lang/go.js +31 -0
  75. package/dist/lang/go.js.map +1 -0
  76. package/dist/lang/java.d.ts +3 -0
  77. package/dist/lang/java.d.ts.map +1 -0
  78. package/dist/lang/java.js +34 -0
  79. package/dist/lang/java.js.map +1 -0
  80. package/dist/lang/javascript.d.ts +3 -0
  81. package/dist/lang/javascript.d.ts.map +1 -0
  82. package/dist/lang/javascript.js +28 -0
  83. package/dist/lang/javascript.js.map +1 -0
  84. package/dist/lang/kotlin.d.ts +3 -0
  85. package/dist/lang/kotlin.d.ts.map +1 -0
  86. package/dist/lang/kotlin.js +37 -0
  87. package/dist/lang/kotlin.js.map +1 -0
  88. package/dist/lang/php.d.ts +3 -0
  89. package/dist/lang/php.d.ts.map +1 -0
  90. package/dist/lang/php.js +37 -0
  91. package/dist/lang/php.js.map +1 -0
  92. package/dist/lang/profile.d.ts +130 -0
  93. package/dist/lang/profile.d.ts.map +1 -0
  94. package/dist/lang/profile.js +2 -0
  95. package/dist/lang/profile.js.map +1 -0
  96. package/dist/lang/python.d.ts +3 -0
  97. package/dist/lang/python.d.ts.map +1 -0
  98. package/dist/lang/python.js +33 -0
  99. package/dist/lang/python.js.map +1 -0
  100. package/dist/lang/registry.d.ts +31 -0
  101. package/dist/lang/registry.d.ts.map +1 -0
  102. package/dist/lang/registry.js +89 -0
  103. package/dist/lang/registry.js.map +1 -0
  104. package/dist/lang/ruby.d.ts +3 -0
  105. package/dist/lang/ruby.d.ts.map +1 -0
  106. package/dist/lang/ruby.js +47 -0
  107. package/dist/lang/ruby.js.map +1 -0
  108. package/dist/lang/rust.d.ts +3 -0
  109. package/dist/lang/rust.d.ts.map +1 -0
  110. package/dist/lang/rust.js +45 -0
  111. package/dist/lang/rust.js.map +1 -0
  112. package/dist/lang/swift.d.ts +3 -0
  113. package/dist/lang/swift.d.ts.map +1 -0
  114. package/dist/lang/swift.js +34 -0
  115. package/dist/lang/swift.js.map +1 -0
  116. package/dist/lang/tsx.d.ts +4 -0
  117. package/dist/lang/tsx.d.ts.map +1 -0
  118. package/dist/lang/tsx.js +11 -0
  119. package/dist/lang/tsx.js.map +1 -0
  120. package/dist/lang/typescript.d.ts +18 -0
  121. package/dist/lang/typescript.d.ts.map +1 -0
  122. package/dist/lang/typescript.js +61 -0
  123. package/dist/lang/typescript.js.map +1 -0
  124. package/dist/net/policy.d.ts +49 -0
  125. package/dist/net/policy.d.ts.map +1 -0
  126. package/dist/net/policy.js +120 -0
  127. package/dist/net/policy.js.map +1 -0
  128. package/dist/plan/grammar.d.ts +38 -0
  129. package/dist/plan/grammar.d.ts.map +1 -0
  130. package/dist/plan/grammar.js +85 -0
  131. package/dist/plan/grammar.js.map +1 -0
  132. package/dist/plan/lexical.d.ts +51 -0
  133. package/dist/plan/lexical.d.ts.map +1 -0
  134. package/dist/plan/lexical.js +167 -0
  135. package/dist/plan/lexical.js.map +1 -0
  136. package/dist/plan/planners.d.ts +35 -0
  137. package/dist/plan/planners.d.ts.map +1 -0
  138. package/dist/plan/planners.js +26 -0
  139. package/dist/plan/planners.js.map +1 -0
  140. package/dist/plan/structural.d.ts +68 -0
  141. package/dist/plan/structural.d.ts.map +1 -0
  142. package/dist/plan/structural.js +458 -0
  143. package/dist/plan/structural.js.map +1 -0
  144. package/dist/repomap/cache.d.ts +53 -0
  145. package/dist/repomap/cache.d.ts.map +1 -0
  146. package/dist/repomap/cache.js +139 -0
  147. package/dist/repomap/cache.js.map +1 -0
  148. package/dist/repomap/map.d.ts +161 -0
  149. package/dist/repomap/map.d.ts.map +1 -0
  150. package/dist/repomap/map.js +302 -0
  151. package/dist/repomap/map.js.map +1 -0
  152. package/dist/repomap/rank.d.ts +52 -0
  153. package/dist/repomap/rank.d.ts.map +1 -0
  154. package/dist/repomap/rank.js +155 -0
  155. package/dist/repomap/rank.js.map +1 -0
  156. package/dist/repomap/tags.d.ts +47 -0
  157. package/dist/repomap/tags.d.ts.map +1 -0
  158. package/dist/repomap/tags.js +101 -0
  159. package/dist/repomap/tags.js.map +1 -0
  160. package/dist/retrieve.d.ts +14 -0
  161. package/dist/retrieve.d.ts.map +1 -0
  162. package/dist/retrieve.js +44 -0
  163. package/dist/retrieve.js.map +1 -0
  164. package/dist/stages.d.ts +22 -0
  165. package/dist/stages.d.ts.map +1 -0
  166. package/dist/stages.js +33 -0
  167. package/dist/stages.js.map +1 -0
  168. package/dist/stats.d.ts +40 -0
  169. package/dist/stats.d.ts.map +1 -0
  170. package/dist/stats.js +37 -0
  171. package/dist/stats.js.map +1 -0
  172. package/dist/store-dir.d.ts +98 -0
  173. package/dist/store-dir.d.ts.map +1 -0
  174. package/dist/store-dir.js +379 -0
  175. package/dist/store-dir.js.map +1 -0
  176. package/dist/store.d.ts +36 -0
  177. package/dist/store.d.ts.map +1 -0
  178. package/dist/store.js +56 -0
  179. package/dist/store.js.map +1 -0
  180. package/dist/types.d.ts +298 -0
  181. package/dist/types.d.ts.map +1 -0
  182. package/dist/types.js +2 -0
  183. package/dist/types.js.map +1 -0
  184. package/grammars/tree-sitter-bash.wasm +0 -0
  185. package/grammars/tree-sitter-c.wasm +0 -0
  186. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  187. package/grammars/tree-sitter-cpp.wasm +0 -0
  188. package/grammars/tree-sitter-go.wasm +0 -0
  189. package/grammars/tree-sitter-java.wasm +0 -0
  190. package/grammars/tree-sitter-javascript.wasm +0 -0
  191. package/grammars/tree-sitter-kotlin.wasm +0 -0
  192. package/grammars/tree-sitter-php.wasm +0 -0
  193. package/grammars/tree-sitter-python.wasm +0 -0
  194. package/grammars/tree-sitter-ruby.wasm +0 -0
  195. package/grammars/tree-sitter-rust.wasm +0 -0
  196. package/grammars/tree-sitter-swift.wasm +0 -0
  197. package/grammars/tree-sitter-tsx.wasm +0 -0
  198. package/grammars/tree-sitter-typescript.wasm +0 -0
  199. package/package.json +68 -0
@@ -0,0 +1,144 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { CliUsageError } from '../errors.js';
4
+ import { isStrategy, STRATEGIES } from '../plan/planners.js';
5
+ import { CLI_NAME } from './args.js';
6
+ /**
7
+ * `smelt.config.json` — CLI defaults, and nothing more.
8
+ *
9
+ * This file exists so `smelt` can be run without retyping the same flags, and it is
10
+ * deliberately a *CLI* concern: the programmatic API never reads it. A library that
11
+ * reads config files off the caller's disk is a library whose behaviour depends on
12
+ * where it was invoked from, which is exactly the class of invisible input smelt
13
+ * refuses. `createSmelter()` takes explicit arguments; the CLI translates this file
14
+ * into those arguments, and explicit flags always win over it.
15
+ *
16
+ * The schema is versioned (`"smeltConfig": 1`) for the same reason the marker and the
17
+ * `--json` envelope are: files outlive the binaries that wrote them, and a mismatch
18
+ * must be identifiable rather than half-understood. Parsing is strict — an unknown
19
+ * key, a wrong type, or an unknown version is a {@link CliUsageError}, never a shrug.
20
+ * A config the CLI silently ignored would be a budget the user *thought* they set.
21
+ */
22
+ /** The file name looked for, from the working directory upward. */
23
+ export const CONFIG_FILE_NAME = 'smelt.config.json';
24
+ /** The schema version this build reads and writes. */
25
+ export const CONFIG_VERSION = 1;
26
+ /**
27
+ * The nearest `smelt.config.json`, walking up from `cwd` to the filesystem root —
28
+ * the same discovery shape as `package.json`, so a config at a repo root covers the
29
+ * whole tree. `undefined` when there is none, which is not an error: flags alone are
30
+ * a complete interface.
31
+ */
32
+ export function findConfigFile(cwd) {
33
+ let dir = resolve(cwd);
34
+ for (;;) {
35
+ const candidate = join(dir, CONFIG_FILE_NAME);
36
+ if (existsSync(candidate))
37
+ return candidate;
38
+ const parent = dirname(dir);
39
+ if (parent === dir)
40
+ return undefined;
41
+ dir = parent;
42
+ }
43
+ }
44
+ /**
45
+ * Find and parse the nearest config. `undefined` when no file exists; a file that
46
+ * exists but is malformed **throws** — see {@link parseConfig}. The distinction is the
47
+ * point: "no config" is a fine state, "a config you cannot have meant" is not.
48
+ *
49
+ * @throws {CliUsageError} when a config file exists and is malformed.
50
+ */
51
+ export function loadNearestConfig(cwd) {
52
+ const path = findConfigFile(cwd);
53
+ if (path === undefined)
54
+ return undefined;
55
+ return { path, config: parseConfig(readFileSync(path, 'utf8'), path) };
56
+ }
57
+ /**
58
+ * Parse and validate one config file, strictly.
59
+ *
60
+ * Strict means: unknown keys are refused, not skipped. A typo'd `defaultBudgetByte`
61
+ * that parsed cleanly would leave the user believing a default was set while every
62
+ * run ignored it — a config error whose failure mode is *silence*, which is the one
63
+ * failure shape this project refuses everywhere else too.
64
+ *
65
+ * @throws {CliUsageError} naming the file and the exact problem.
66
+ */
67
+ export function parseConfig(text, path) {
68
+ const bad = (why) => new CliUsageError(`${CLI_NAME}: malformed ${CONFIG_FILE_NAME} at ${path}: ${why}\n` +
69
+ `Fix it or delete it — a config smelt cannot read is never silently ignored. ` +
70
+ `\`${CLI_NAME} init\` can rewrite it.`);
71
+ let value;
72
+ try {
73
+ value = JSON.parse(text);
74
+ }
75
+ catch (cause) {
76
+ throw bad(`not JSON (${cause instanceof Error ? cause.message : String(cause)}).`);
77
+ }
78
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
79
+ throw bad('expected a JSON object at the top level.');
80
+ }
81
+ const fields = value;
82
+ if (fields['smeltConfig'] !== CONFIG_VERSION) {
83
+ throw bad(`"smeltConfig" is ${JSON.stringify(fields['smeltConfig'])}; this build reads version ` +
84
+ `${String(CONFIG_VERSION)}. The schema is versioned so a mismatch is visible ` +
85
+ `instead of half-understood.`);
86
+ }
87
+ const known = ['smeltConfig', 'defaultBudgetBytes', 'strategy', 'store'];
88
+ const unknown = Object.keys(fields).filter((key) => !known.includes(key));
89
+ if (unknown.length > 0) {
90
+ throw bad(`unknown key${unknown.length === 1 ? '' : 's'} ${unknown.map((k) => `"${k}"`).join(', ')}. ` +
91
+ `Known keys: ${known.join(', ')}. Unknown keys are refused because a typo that ` +
92
+ `parsed cleanly would be a setting you believed was set.`);
93
+ }
94
+ const budget = fields['defaultBudgetBytes'];
95
+ if (budget !== undefined && (typeof budget !== 'number' || !Number.isInteger(budget))) {
96
+ throw bad(`"defaultBudgetBytes" must be a whole number of UTF-8 bytes.`);
97
+ }
98
+ if (typeof budget === 'number' && budget <= 0) {
99
+ throw bad(`"defaultBudgetBytes" must be greater than zero, got ${String(budget)}.`);
100
+ }
101
+ const strategy = fields['strategy'];
102
+ if (strategy !== undefined && (typeof strategy !== 'string' || !isStrategy(strategy))) {
103
+ throw bad(`"strategy" must be ${STRATEGIES.map((s) => `"${s}"`).join(' or ')}, ` +
104
+ `got ${JSON.stringify(strategy)}.`);
105
+ }
106
+ const store = parseStore(fields['store'], bad);
107
+ return {
108
+ smeltConfig: CONFIG_VERSION,
109
+ ...(budget === undefined ? {} : { defaultBudgetBytes: budget }),
110
+ ...(strategy === undefined ? {} : { strategy }),
111
+ ...(store === undefined ? {} : { store }),
112
+ };
113
+ }
114
+ function parseStore(value, bad) {
115
+ if (value === undefined)
116
+ return undefined;
117
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
118
+ throw bad(`"store" must be an object like {"kind":"memory"} or {"kind":"directory","path":…}.`);
119
+ }
120
+ const fields = value;
121
+ const kind = fields['kind'];
122
+ if (kind === 'memory') {
123
+ const extra = Object.keys(fields).filter((key) => key !== 'kind');
124
+ if (extra.length > 0)
125
+ throw bad(`"store" of kind "memory" takes no other keys.`);
126
+ return { kind: 'memory' };
127
+ }
128
+ if (kind === 'directory') {
129
+ const path = fields['path'];
130
+ if (typeof path !== 'string' || path === '') {
131
+ throw bad(`"store" of kind "directory" needs a non-empty "path".`);
132
+ }
133
+ const extra = Object.keys(fields).filter((key) => key !== 'kind' && key !== 'path');
134
+ if (extra.length > 0)
135
+ throw bad(`"store" of kind "directory" takes only "kind" and "path".`);
136
+ return { kind: 'directory', path };
137
+ }
138
+ throw bad(`"store".kind must be "memory" or "directory", got ${JSON.stringify(kind)}.`);
139
+ }
140
+ /** `store.path` is relative to the config file, so the config works from any cwd. */
141
+ export function resolveStorePath(loaded, path) {
142
+ return resolve(dirname(loaded.path), path);
143
+ }
144
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import type { SmeltConfigStore } from './config.ts';
2
+ /**
3
+ * `smelt init` — the setup wizard.
4
+ *
5
+ * The same testability pattern as `run.ts`: the wizard is a pure function over an
6
+ * input/output pair, so every flow — fresh run, back-navigation, re-run editing,
7
+ * declined overwrites — runs in-process in tests, and `bin.ts` wires the real stdio.
8
+ *
9
+ * Three rules shape everything here:
10
+ *
11
+ * 1. **Nothing is written until the final confirm**, which lists exactly what will be
12
+ * written or changed. A wizard that writes as it goes cannot be backed out of.
13
+ * 2. **An existing file is never overwritten without an explicit per-file yes.** Not
14
+ * a global "overwrite all", not a default — one question per existing file, and
15
+ * anything but a literal `yes` skips it. Guarded by
16
+ * `test/guards/init-wizard.test.ts`, with a mutation proving the guard goes red.
17
+ * 3. **Every step accepts `back`.** A wizard you cannot reverse inside is a form.
18
+ *
19
+ * Law 4 note: the wizard's copy states what each choice *does*, never what it saves —
20
+ * no percentages, no rates, no numbers smelt has not measured.
21
+ */
22
+ /** The generated measure-hook stub's file name. */
23
+ export declare const MEASURE_STUB_FILE = "smelt.measure.ts";
24
+ /** The generated reranker stub's file name. */
25
+ export declare const RERANK_STUB_FILE = "smelt.rerank.ts";
26
+ /** Where the wizard's bytes come from and go. Injected so `runInit` is testable in-process. */
27
+ export interface InitIo {
28
+ /** Interactive input — the real stdin in `bin.ts`, a scripted stream in tests. */
29
+ readonly input: NodeJS.ReadableStream;
30
+ readonly output: (text: string) => void;
31
+ /**
32
+ * Where config discovery starts, and where a fresh run's files land. An edit run
33
+ * writes next to the discovered config instead — which may be an ancestor of `cwd`,
34
+ * and the "About to write" listing names that directory before anything is written.
35
+ */
36
+ readonly cwd: string;
37
+ }
38
+ /**
39
+ * The wizard, start to finish. Returns an exit code (0 in every completed flow,
40
+ * including "declined to write anything").
41
+ *
42
+ * @throws {CliUsageError} on a malformed existing config, or when input ends before
43
+ * the wizard finishes — both are usage-shaped, and nothing further is written.
44
+ */
45
+ export declare function runInit(io: InitIo): Promise<number>;
46
+ /** The config file, serialized with a stable key order so re-runs diff cleanly. */
47
+ export declare function renderConfig(choices: {
48
+ readonly budgetBytes: number | undefined;
49
+ readonly store: SmeltConfigStore;
50
+ readonly strategy: 'lexical' | 'structural';
51
+ }): string;
52
+ /** The `smelt.measure.ts` the wizard writes. Exported so tests can compile it. */
53
+ export declare function measureStubSource(): string;
54
+ /** The `smelt.rerank.ts` the wizard writes. Exported so tests can compile it. */
55
+ export declare function rerankStubSource(): string;
56
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,mDAAmD;AACnD,eAAO,MAAM,iBAAiB,qBAAqB,CAAC;AAEpD,+CAA+C;AAC/C,eAAO,MAAM,gBAAgB,oBAAoB,CAAC;AAElD,+FAA+F;AAC/F,MAAM,WAAW,MAAM;IACrB,kFAAkF;IAClF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAwBD;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBzD;AAmUD,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,OAAO,EAAE;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,SAAS,GAAG,YAAY,CAAC;CAC7C,GAAG,MAAM,CAQT;AAcD,kFAAkF;AAClF,wBAAgB,iBAAiB,IAAI,MAAM,CA+B1C;AAED,iFAAiF;AACjF,wBAAgB,gBAAgB,IAAI,MAAM,CA6DzC"}
@@ -0,0 +1,449 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { createInterface } from 'node:readline/promises';
4
+ import { CliUsageError } from '../errors.js';
5
+ import { STRUCTURAL_LANGUAGES } from '../plan/structural.js';
6
+ import { CLI_NAME } from './args.js';
7
+ import { CONFIG_FILE_NAME, CONFIG_VERSION, findConfigFile, parseConfig } from './config.js';
8
+ /**
9
+ * `smelt init` — the setup wizard.
10
+ *
11
+ * The same testability pattern as `run.ts`: the wizard is a pure function over an
12
+ * input/output pair, so every flow — fresh run, back-navigation, re-run editing,
13
+ * declined overwrites — runs in-process in tests, and `bin.ts` wires the real stdio.
14
+ *
15
+ * Three rules shape everything here:
16
+ *
17
+ * 1. **Nothing is written until the final confirm**, which lists exactly what will be
18
+ * written or changed. A wizard that writes as it goes cannot be backed out of.
19
+ * 2. **An existing file is never overwritten without an explicit per-file yes.** Not
20
+ * a global "overwrite all", not a default — one question per existing file, and
21
+ * anything but a literal `yes` skips it. Guarded by
22
+ * `test/guards/init-wizard.test.ts`, with a mutation proving the guard goes red.
23
+ * 3. **Every step accepts `back`.** A wizard you cannot reverse inside is a form.
24
+ *
25
+ * Law 4 note: the wizard's copy states what each choice *does*, never what it saves —
26
+ * no percentages, no rates, no numbers smelt has not measured.
27
+ */
28
+ /** The generated measure-hook stub's file name. */
29
+ export const MEASURE_STUB_FILE = 'smelt.measure.ts';
30
+ /** The generated reranker stub's file name. */
31
+ export const RERANK_STUB_FILE = 'smelt.rerank.ts';
32
+ /**
33
+ * The wizard, start to finish. Returns an exit code (0 in every completed flow,
34
+ * including "declined to write anything").
35
+ *
36
+ * @throws {CliUsageError} on a malformed existing config, or when input ends before
37
+ * the wizard finishes — both are usage-shaped, and nothing further is written.
38
+ */
39
+ export async function runInit(io) {
40
+ const rl = createInterface({ input: io.input });
41
+ const lines = rl[Symbol.asyncIterator]();
42
+ const ask = async (prompt) => {
43
+ io.output(prompt);
44
+ const next = await lines.next();
45
+ if (next.done === true) {
46
+ throw new CliUsageError(`${CLI_NAME} init: input ended before the wizard finished. ` +
47
+ `Files already confirmed and written stay; nothing further was written.`);
48
+ }
49
+ return next.value.trim();
50
+ };
51
+ try {
52
+ const existing = loadExisting(io.cwd);
53
+ return existing === undefined
54
+ ? await freshRun(io, ask)
55
+ : await editRun(io, ask, existing.path, existing.config);
56
+ }
57
+ finally {
58
+ rl.close();
59
+ }
60
+ }
61
+ function loadExisting(cwd) {
62
+ const path = findConfigFile(cwd);
63
+ if (path === undefined)
64
+ return undefined;
65
+ // Malformed is a loud usage error, not a silent fresh start: overwriting a config
66
+ // the user wrote, because it had a typo, would be the wizard deciding for them.
67
+ return { path, config: parseConfig(readFileSync(path, 'utf8'), path) };
68
+ }
69
+ const STEPS = [
70
+ { id: 'budget', run: stepBudget },
71
+ { id: 'store', run: stepStore },
72
+ { id: 'strategy', run: stepStrategy },
73
+ { id: 'measure', run: stepMeasure },
74
+ { id: 'rerank', run: stepRerank },
75
+ ];
76
+ async function freshRun(io, ask) {
77
+ io.output(`${CLI_NAME} init — sets up ${CONFIG_FILE_NAME} in ${io.cwd}.\n` +
78
+ `Answer \`back\` at any step to return to the previous one. ` +
79
+ `Nothing is written until you confirm at the end.\n\n`);
80
+ const choices = {
81
+ budgetBytes: undefined,
82
+ store: { kind: 'memory' },
83
+ strategy: 'lexical',
84
+ measureStub: false,
85
+ rerankStub: false,
86
+ };
87
+ let index = 0;
88
+ for (;;) {
89
+ while (index < STEPS.length) {
90
+ const outcome = await STEPS[index].run(io, ask, choices, io.cwd);
91
+ if (outcome === 'back') {
92
+ if (index === 0)
93
+ io.output(`This is the first step — there is nothing before it.\n`);
94
+ else
95
+ index -= 1;
96
+ }
97
+ else {
98
+ index += 1;
99
+ }
100
+ }
101
+ const verdict = await confirmAndWrite(io, ask, choices, io.cwd);
102
+ if (verdict !== 'back')
103
+ return 0;
104
+ index = STEPS.length - 1;
105
+ }
106
+ }
107
+ async function editRun(io, ask, configPath, config) {
108
+ const dir = dirname(configPath);
109
+ const choices = {
110
+ budgetBytes: config.defaultBudgetBytes,
111
+ store: config.store ?? { kind: 'memory' },
112
+ strategy: config.strategy ?? 'lexical',
113
+ measureStub: false,
114
+ rerankStub: false,
115
+ };
116
+ io.output(`${CLI_NAME} init — ${CONFIG_FILE_NAME} already exists at ${configPath}.\n` +
117
+ `Change one setting at a time; nothing is written until you confirm.\n`);
118
+ for (;;) {
119
+ io.output(`\nCurrent values:\n${summary(choices, dir)}\n`);
120
+ const answer = await ask(`Change which setting? (budget / store / strategy / measure / rerank, ` +
121
+ `\`done\` to review and confirm, \`back\` to leave without writing)\n> `);
122
+ if (answer === 'back') {
123
+ io.output(`Left as it was. Nothing has been written.\n`);
124
+ return 0;
125
+ }
126
+ if (answer === 'done') {
127
+ const verdict = await confirmAndWrite(io, ask, choices, dir);
128
+ if (verdict !== 'back')
129
+ return 0;
130
+ continue; // back from confirm returns to this menu
131
+ }
132
+ const step = STEPS.find((candidate) => candidate.id === answer);
133
+ if (step === undefined) {
134
+ io.output(`Not a setting: "${answer}". One of: budget, store, strategy, measure, rerank.\n`);
135
+ continue;
136
+ }
137
+ await step.run(io, ask, choices, dir); // its own `back` returns here
138
+ }
139
+ }
140
+ function summary(choices, dir) {
141
+ const stubLine = (file, generate) => {
142
+ if (generate)
143
+ return `generate ${file}`;
144
+ return existsSync(join(dir, file)) ? `${file} exists (kept as is)` : 'none';
145
+ };
146
+ return [
147
+ ` budget: ${choices.budgetBytes === undefined ? '(not set)' : `${String(choices.budgetBytes)} bytes`}`,
148
+ ` store: ${choices.store.kind === 'memory' ? 'memory' : `directory (${choices.store.path})`}`,
149
+ ` strategy: ${choices.strategy}`,
150
+ ` measure: ${stubLine(MEASURE_STUB_FILE, choices.measureStub)}`,
151
+ ` rerank: ${stubLine(RERANK_STUB_FILE, choices.rerankStub)}`,
152
+ ].join('\n');
153
+ }
154
+ // ---------------------------------------------------------------------------
155
+ // The steps. Each one accepts `back`.
156
+ // ---------------------------------------------------------------------------
157
+ async function stepBudget(io, ask, choices) {
158
+ io.output(`\nDefault byte budget — used when a \`${CLI_NAME}\` run omits --budget.\n` +
159
+ `Budgets are UTF-8 bytes, permanently; an explicit --budget always wins.\n` +
160
+ `There is no suggested number: the right budget depends on your traffic, and ` +
161
+ `smelt does not invent numbers it has not measured.\n`);
162
+ for (;;) {
163
+ const current = choices.budgetBytes === undefined ? '' : ` [${String(choices.budgetBytes)}]`;
164
+ const answer = await ask(`budget in bytes${current} (or back)> `);
165
+ if (answer === 'back')
166
+ return 'back';
167
+ if (answer === '' && choices.budgetBytes !== undefined)
168
+ return 'ok';
169
+ if (/^\d+$/.test(answer) && Number(answer) > 0) {
170
+ choices.budgetBytes = Number(answer);
171
+ return 'ok';
172
+ }
173
+ io.output(`A whole number of bytes greater than zero, e.g. 4000.\n`);
174
+ }
175
+ }
176
+ async function stepStore(io, ask, choices) {
177
+ io.output(`\nWhere elided bytes live. Every elision is reversible only while a store holds ` +
178
+ `its bytes (Law 3):\n` +
179
+ ` 1. memory — per-process; retrievals do not survive the process\n` +
180
+ ` 2. directory — persistent, content-addressed, on disk; retrieval counters ` +
181
+ `survive restarts\n`);
182
+ for (;;) {
183
+ const current = choices.store.kind === 'memory' ? '1' : '2';
184
+ const answer = await ask(`store (1/2) [${current}] (or back)> `);
185
+ if (answer === 'back')
186
+ return 'back';
187
+ const pick = answer === '' ? current : answer;
188
+ if (pick === '1') {
189
+ choices.store = { kind: 'memory' };
190
+ return 'ok';
191
+ }
192
+ if (pick === '2') {
193
+ const previous = choices.store.kind === 'directory' ? choices.store.path : '.smelt/store';
194
+ const path = await ask(`store directory, relative to ${CONFIG_FILE_NAME} [${previous}]> `);
195
+ if (path === 'back')
196
+ continue; // back to the store choice, not out of the step
197
+ choices.store = { kind: 'directory', path: path === '' ? previous : path };
198
+ return 'ok';
199
+ }
200
+ io.output(`1 for memory, 2 for directory, or back.\n`);
201
+ }
202
+ }
203
+ async function stepStrategy(io, ask, choices) {
204
+ io.output(`\nDefault planner strategy — used when a run omits --strategy:\n` +
205
+ ` 1. lexical — line windows around your focus terms; works on any text\n` +
206
+ ` 2. structural — parses ${String(STRUCTURAL_LANGUAGES.length)} languages with ` +
207
+ `bundled grammars (${STRUCTURAL_LANGUAGES.join(', ')}) and ` +
208
+ `collapses siblings by name; refuses other languages rather than approximating\n`);
209
+ for (;;) {
210
+ const current = choices.strategy === 'lexical' ? '1' : '2';
211
+ const answer = await ask(`strategy (1/2) [${current}] (or back)> `);
212
+ if (answer === 'back')
213
+ return 'back';
214
+ const pick = answer === '' ? current : answer;
215
+ if (pick === '1' || pick === '2') {
216
+ choices.strategy = pick === '1' ? 'lexical' : 'structural';
217
+ return 'ok';
218
+ }
219
+ io.output(`1 for lexical, 2 for structural, or back.\n`);
220
+ }
221
+ }
222
+ async function stepMeasure(io, ask, choices, dir) {
223
+ const exists = existsSync(join(dir, MEASURE_STUB_FILE));
224
+ io.output(`\nMeasure hook — your own counter (a tokenizer, usually), so results carry a ` +
225
+ `second, labelled number next to the byte counts. smelt ships none: a token ` +
226
+ `count without its tokenizer named is not a measurement.\n` +
227
+ ` 1. none\n` +
228
+ ` 2. generate ${MEASURE_STUB_FILE}, a typed stub you fill in\n` +
229
+ (exists ? `(${MEASURE_STUB_FILE} already exists; it is never deleted from here.)\n` : ``));
230
+ for (;;) {
231
+ const answer = await ask(`measure (1/2) [${choices.measureStub ? '2' : '1'}] (or back)> `);
232
+ if (answer === 'back')
233
+ return 'back';
234
+ const pick = answer === '' ? (choices.measureStub ? '2' : '1') : answer;
235
+ if (pick === '1' || pick === '2') {
236
+ choices.measureStub = pick === '2';
237
+ return 'ok';
238
+ }
239
+ io.output(`1 for none, 2 to generate the stub, or back.\n`);
240
+ }
241
+ }
242
+ async function stepRerank(io, ask, choices, dir) {
243
+ const exists = existsSync(join(dir, RERANK_STUB_FILE));
244
+ io.output(`\nReranker — a RerankStage of your own. smelt never bundles one and never will: ` +
245
+ `an outbound call must live in your code, under your key, visible in your own ` +
246
+ `review (Law 1).\n` +
247
+ ` 1. none\n` +
248
+ ` 2. generate ${RERANK_STUB_FILE}, a typed stub with the HTTP call sketched as a TODO\n` +
249
+ (exists ? `(${RERANK_STUB_FILE} already exists; it is never deleted from here.)\n` : ``));
250
+ for (;;) {
251
+ const answer = await ask(`rerank (1/2) [${choices.rerankStub ? '2' : '1'}] (or back)> `);
252
+ if (answer === 'back')
253
+ return 'back';
254
+ const pick = answer === '' ? (choices.rerankStub ? '2' : '1') : answer;
255
+ if (pick === '1' || pick === '2') {
256
+ choices.rerankStub = pick === '2';
257
+ return 'ok';
258
+ }
259
+ io.output(`1 for none, 2 to generate the stub, or back.\n`);
260
+ }
261
+ }
262
+ // ---------------------------------------------------------------------------
263
+ // The confirm step: the only place anything is written
264
+ // ---------------------------------------------------------------------------
265
+ const writeLabel = (write) => {
266
+ if (write.unchanged)
267
+ return 'unchanged — nothing to write';
268
+ return write.exists ? 'exists — will ask before overwriting' : 'new';
269
+ };
270
+ async function confirmAndWrite(io, ask, choices, dir) {
271
+ if (choices.budgetBytes === undefined) {
272
+ // Unreachable in the fresh flow (the budget step requires a number) but reachable
273
+ // from an edit of a config that never had one — `defaultBudgetBytes` is optional,
274
+ // so such a config parses fine. The wizard still insists: a confirm without a
275
+ // budget would write a file this wizard just called incomplete. `back` here is a
276
+ // real `back` — it returns to the caller, never falls forward into the confirm.
277
+ io.output(`No budget is set yet — set one before confirming.\n`);
278
+ if ((await stepBudget(io, ask, choices)) === 'back')
279
+ return 'back';
280
+ }
281
+ const writes = plannedWrites(choices, dir);
282
+ io.output(`\nAbout to write, into ${dir}:\n` +
283
+ writes.map((write) => ` ${write.name.padEnd(20)} (${writeLabel(write)})\n`).join('') +
284
+ `Nothing has been written yet.\n`);
285
+ for (;;) {
286
+ const answer = await ask(`confirm (yes / no / back)> `);
287
+ if (answer === 'back')
288
+ return 'back';
289
+ if (answer === 'no') {
290
+ io.output(`Nothing was written.\n`);
291
+ return 'done';
292
+ }
293
+ if (answer === 'yes')
294
+ break;
295
+ io.output(`yes to write, no to leave everything untouched, back to change a setting.\n`);
296
+ }
297
+ for (const write of writes) {
298
+ if (write.unchanged) {
299
+ io.output(` ${write.name} — unchanged, not rewritten\n`);
300
+ continue;
301
+ }
302
+ if (write.exists) {
303
+ // The per-file consent rule: one explicit question per existing file, and only
304
+ // a literal `yes` overwrites. This is the line the mutation
305
+ // `init-overwrite-without-consent` breaks to prove the guard can go red.
306
+ const answer = await ask(` ${write.name} exists — overwrite it? (yes/no)> `);
307
+ if (answer !== 'yes') {
308
+ io.output(` skipped ${write.name} — the existing file was not touched\n`);
309
+ continue;
310
+ }
311
+ }
312
+ writeFileSync(write.path, write.content);
313
+ io.output(` wrote ${write.name}\n`);
314
+ }
315
+ io.output(`Done.\n`);
316
+ return 'done';
317
+ }
318
+ function plannedWrites(choices, dir) {
319
+ const plan = (name, content) => {
320
+ const path = join(dir, name);
321
+ const exists = existsSync(path);
322
+ const unchanged = exists && readFileSync(path, 'utf8') === content;
323
+ return { name, path, content, exists, unchanged };
324
+ };
325
+ const writes = [plan(CONFIG_FILE_NAME, renderConfig(choices))];
326
+ if (choices.measureStub)
327
+ writes.push(plan(MEASURE_STUB_FILE, measureStubSource()));
328
+ if (choices.rerankStub)
329
+ writes.push(plan(RERANK_STUB_FILE, rerankStubSource()));
330
+ return writes;
331
+ }
332
+ /** The config file, serialized with a stable key order so re-runs diff cleanly. */
333
+ export function renderConfig(choices) {
334
+ const config = {
335
+ smeltConfig: CONFIG_VERSION,
336
+ ...(choices.budgetBytes === undefined ? {} : { defaultBudgetBytes: choices.budgetBytes }),
337
+ strategy: choices.strategy,
338
+ store: choices.store,
339
+ };
340
+ return `${JSON.stringify(config, null, 2)}\n`;
341
+ }
342
+ // ---------------------------------------------------------------------------
343
+ // The generated stubs. String literals on purpose: smelt's own import graph must not
344
+ // gain an HTTP client, and the zero-network guard's string-stripper ignores string
345
+ // bodies — the sketched fetch below lives in the CONSUMER'S file, never in smelt's.
346
+ //
347
+ // The comment token between `from` and the package name in each stub's import line is
348
+ // deliberate: the guard's import scanner reads raw source, so spelling that import
349
+ // plainly inside this template would register as an edge in smelt's own graph — which
350
+ // it is not. The comment keeps the generated file valid TypeScript while keeping this
351
+ // data out of the guard's walk.
352
+ // ---------------------------------------------------------------------------
353
+ /** The `smelt.measure.ts` the wizard writes. Exported so tests can compile it. */
354
+ export function measureStubSource() {
355
+ return `/**
356
+ * Measure hook — generated by \`smelt init\`.
357
+ *
358
+ * smelt's budgets are UTF-8 bytes, permanently; this hook adds a second, labelled
359
+ * number to every result in YOUR unit, counted by YOUR tokenizer. Both \`id\` and
360
+ * \`unit\` are required: a count without the counter named is not a measurement.
361
+ *
362
+ * Wire it in, in your own code:
363
+ *
364
+ * const smelter = createSmelter({ defaultBudgetBytes: 8_000, measure });
365
+ *
366
+ * with createSmelter imported from your @smeltjs/core install and measure from here.
367
+ */
368
+ import type { Measure } from /* your install */ '@smeltjs/core';
369
+
370
+ export const measure: Measure = {
371
+ // TODO: name the counter that produces these numbers, e.g. 'tiktoken/o200k_base'.
372
+ id: 'TODO/your-tokenizer',
373
+ unit: 'tokens',
374
+ count(text: string): number {
375
+ // TODO: replace with your real tokenizer. It must be local and synchronous —
376
+ // a count() that calls an API would make your process call an API on every smelt.
377
+ // e.g. return encode(text).length;
378
+ throw new Error(
379
+ 'smelt.measure.ts: count() is not implemented yet — ' +
380
+ 'fill it in with your tokenizer (length of input: ' + String(text.length) + ')',
381
+ );
382
+ },
383
+ };
384
+ `;
385
+ }
386
+ /** The `smelt.rerank.ts` the wizard writes. Exported so tests can compile it. */
387
+ export function rerankStubSource() {
388
+ return `/**
389
+ * Reranker stub — generated by \`smelt init\`.
390
+ *
391
+ * smelt itself makes zero network calls and never bundles a reranker (Law 1): the
392
+ * moment one ships as a default, every consumer's source code leaves the machine and
393
+ * they find out from a changelog, or never. So the outbound call lives HERE, in your
394
+ * file, reading your env var, visible in your own review.
395
+ *
396
+ * Wire it into your own pipeline; smelt never calls this for you.
397
+ */
398
+ import type { RerankCandidate, RerankedCandidate, RerankStage } from /* your install */ '@smeltjs/core';
399
+
400
+ /** The env var YOUR code reads. Rename it to match your vendor. */
401
+ const API_KEY_ENV = 'RERANKER_API_KEY';
402
+
403
+ export const rerank: RerankStage = {
404
+ id: 'my-reranker/v1',
405
+ async rerank(
406
+ candidates: readonly RerankCandidate[],
407
+ query: string,
408
+ ): Promise<readonly RerankedCandidate[]> {
409
+ const apiKey = process.env[API_KEY_ENV];
410
+ if (apiKey === undefined || apiKey === '') {
411
+ throw new Error(
412
+ 'smelt.rerank.ts: ' + API_KEY_ENV + ' is not set. This stage makes an outbound ' +
413
+ 'HTTP call from YOUR code with YOUR key; without a key it refuses to pretend.',
414
+ );
415
+ }
416
+
417
+ // TODO: the outbound call. Voyage AI is one example vendor; any reranker with an
418
+ // HTTP API fits this shape:
419
+ //
420
+ // const response = await fetch('https://api.voyageai.com/v1/rerank', {
421
+ // method: 'POST',
422
+ // headers: {
423
+ // 'content-type': 'application/json',
424
+ // authorization: \`Bearer \${apiKey}\`,
425
+ // },
426
+ // body: JSON.stringify({
427
+ // query,
428
+ // documents: candidates.map((candidate) => candidate.text),
429
+ // }),
430
+ // });
431
+ // if (!response.ok) throw new Error('rerank failed: ' + String(response.status));
432
+ // const body = (await response.json()) as {
433
+ // data: { index: number; relevance_score: number }[];
434
+ // };
435
+ // return body.data.map(({ index, relevance_score }) => ({
436
+ // ...candidates[index]!,
437
+ // score: relevance_score,
438
+ // }));
439
+ //
440
+ void candidates;
441
+ void query;
442
+ throw new Error(
443
+ 'smelt.rerank.ts: implement the outbound call sketched above, then delete this throw.',
444
+ );
445
+ },
446
+ };
447
+ `;
448
+ }
449
+ //# sourceMappingURL=init.js.map