@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.
- package/README.md +3 -3
- package/dist/apply.d.ts.map +1 -1
- package/dist/apply.js.map +1 -1
- package/dist/cache/prefix.d.ts.map +1 -1
- package/dist/cache/prefix.js.map +1 -1
- package/dist/cli/args.d.ts +16 -70
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +18 -338
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/config.d.ts +40 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +55 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/hooks.d.ts +33 -38
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +140 -485
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/init.d.ts +30 -15
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +167 -38
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +2 -2
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/run.d.ts +25 -88
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +32 -341
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/shell.d.ts +106 -0
- package/dist/cli/shell.d.ts.map +1 -0
- package/dist/cli/shell.js +100 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/subcommands/flags.d.ts +141 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -0
- package/dist/cli/subcommands/flags.js +208 -0
- package/dist/cli/subcommands/flags.js.map +1 -0
- package/dist/cli/subcommands/hooks.d.ts +18 -0
- package/dist/cli/subcommands/hooks.d.ts.map +1 -0
- package/dist/cli/subcommands/hooks.js +64 -0
- package/dist/cli/subcommands/hooks.js.map +1 -0
- package/dist/cli/subcommands/init.d.ts +19 -0
- package/dist/cli/subcommands/init.d.ts.map +1 -0
- package/dist/cli/subcommands/init.js +44 -0
- package/dist/cli/subcommands/init.js.map +1 -0
- package/dist/cli/subcommands/map.d.ts +75 -0
- package/dist/cli/subcommands/map.d.ts.map +1 -0
- package/dist/cli/subcommands/map.js +132 -0
- package/dist/cli/subcommands/map.js.map +1 -0
- package/dist/cli/subcommands/registry.d.ts +80 -0
- package/dist/cli/subcommands/registry.d.ts.map +1 -0
- package/dist/cli/subcommands/registry.js +138 -0
- package/dist/cli/subcommands/registry.js.map +1 -0
- package/dist/cli/subcommands/retrieve.d.ts +74 -0
- package/dist/cli/subcommands/retrieve.d.ts.map +1 -0
- package/dist/cli/subcommands/retrieve.js +84 -0
- package/dist/cli/subcommands/retrieve.js.map +1 -0
- package/dist/cli/subcommands/smelt.d.ts +107 -0
- package/dist/cli/subcommands/smelt.d.ts.map +1 -0
- package/dist/cli/subcommands/smelt.js +304 -0
- package/dist/cli/subcommands/smelt.js.map +1 -0
- package/dist/cli/subcommands/stats.d.ts +43 -0
- package/dist/cli/subcommands/stats.d.ts.map +1 -0
- package/dist/cli/subcommands/stats.js +62 -0
- package/dist/cli/subcommands/stats.js.map +1 -0
- package/dist/cli/subcommands/subcommand.d.ts +113 -0
- package/dist/cli/subcommands/subcommand.d.ts.map +1 -0
- package/dist/cli/subcommands/subcommand.js +3 -0
- package/dist/cli/subcommands/subcommand.js.map +1 -0
- package/dist/cli/usage.d.ts +3 -0
- package/dist/cli/usage.d.ts.map +1 -0
- package/dist/cli/usage.js +95 -0
- package/dist/cli/usage.js.map +1 -0
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js.map +1 -1
- package/dist/errors.d.ts +21 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +33 -1
- package/dist/errors.js.map +1 -1
- package/dist/harness/aider.d.ts +10 -0
- package/dist/harness/aider.d.ts.map +1 -0
- package/dist/harness/aider.js +21 -0
- package/dist/harness/aider.js.map +1 -0
- package/dist/harness/claude-code.d.ts +3 -0
- package/dist/harness/claude-code.d.ts.map +1 -0
- package/dist/harness/claude-code.js +68 -0
- package/dist/harness/claude-code.js.map +1 -0
- package/dist/harness/cline.d.ts +3 -0
- package/dist/harness/cline.d.ts.map +1 -0
- package/dist/harness/cline.js +54 -0
- package/dist/harness/cline.js.map +1 -0
- package/dist/harness/codex.d.ts +3 -0
- package/dist/harness/codex.d.ts.map +1 -0
- package/dist/harness/codex.js +91 -0
- package/dist/harness/codex.js.map +1 -0
- package/dist/harness/cursor.d.ts +3 -0
- package/dist/harness/cursor.d.ts.map +1 -0
- package/dist/harness/cursor.js +54 -0
- package/dist/harness/cursor.js.map +1 -0
- package/dist/harness/gemini.d.ts +3 -0
- package/dist/harness/gemini.d.ts.map +1 -0
- package/dist/harness/gemini.js +59 -0
- package/dist/harness/gemini.js.map +1 -0
- package/dist/harness/grok.d.ts +3 -0
- package/dist/harness/grok.d.ts.map +1 -0
- package/dist/harness/grok.js +46 -0
- package/dist/harness/grok.js.map +1 -0
- package/dist/harness/hermes.d.ts +3 -0
- package/dist/harness/hermes.d.ts.map +1 -0
- package/dist/harness/hermes.js +71 -0
- package/dist/harness/hermes.js.map +1 -0
- package/dist/harness/kilocode.d.ts +3 -0
- package/dist/harness/kilocode.d.ts.map +1 -0
- package/dist/harness/kilocode.js +35 -0
- package/dist/harness/kilocode.js.map +1 -0
- package/dist/harness/opencode.d.ts +3 -0
- package/dist/harness/opencode.d.ts.map +1 -0
- package/dist/harness/opencode.js +97 -0
- package/dist/harness/opencode.js.map +1 -0
- package/dist/harness/paths.d.ts +17 -0
- package/dist/harness/paths.d.ts.map +1 -0
- package/dist/harness/paths.js +45 -0
- package/dist/harness/paths.js.map +1 -0
- package/dist/harness/profile.d.ts +184 -0
- package/dist/harness/profile.d.ts.map +1 -0
- package/dist/harness/profile.js +26 -0
- package/dist/harness/profile.js.map +1 -0
- package/dist/harness/registry.d.ts +51 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +87 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/snippet.d.ts +19 -0
- package/dist/harness/snippet.d.ts.map +1 -0
- package/dist/harness/snippet.js +41 -0
- package/dist/harness/snippet.js.map +1 -0
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js.map +1 -1
- package/dist/hooks/guard-core.d.ts +14 -14
- package/dist/hooks/guard-core.d.ts.map +1 -1
- package/dist/hooks/guard-core.js +15 -37
- package/dist/hooks/guard-core.js.map +1 -1
- package/dist/hooks/shim.d.ts +95 -4
- package/dist/hooks/shim.d.ts.map +1 -1
- package/dist/hooks/shim.js +87 -2
- package/dist/hooks/shim.js.map +1 -1
- package/dist/hooks/shims/claude-code.d.ts +5 -18
- package/dist/hooks/shims/claude-code.d.ts.map +1 -1
- package/dist/hooks/shims/claude-code.js +9 -56
- package/dist/hooks/shims/claude-code.js.map +1 -1
- package/dist/hooks/shims/cline.d.ts +5 -12
- package/dist/hooks/shims/cline.d.ts.map +1 -1
- package/dist/hooks/shims/cline.js +9 -34
- package/dist/hooks/shims/cline.js.map +1 -1
- package/dist/hooks/shims/codex.d.ts +5 -18
- package/dist/hooks/shims/codex.d.ts.map +1 -1
- package/dist/hooks/shims/codex.js +9 -51
- package/dist/hooks/shims/codex.js.map +1 -1
- package/dist/hooks/shims/cursor.d.ts +5 -14
- package/dist/hooks/shims/cursor.d.ts.map +1 -1
- package/dist/hooks/shims/cursor.js +9 -42
- package/dist/hooks/shims/cursor.js.map +1 -1
- package/dist/hooks/shims/gemini.d.ts +5 -18
- package/dist/hooks/shims/gemini.d.ts.map +1 -1
- package/dist/hooks/shims/gemini.js +9 -48
- package/dist/hooks/shims/gemini.js.map +1 -1
- package/dist/hooks/shims/grok.d.ts +5 -13
- package/dist/hooks/shims/grok.d.ts.map +1 -1
- package/dist/hooks/shims/grok.js +9 -32
- package/dist/hooks/shims/grok.js.map +1 -1
- package/dist/hooks/shims/hermes.d.ts +5 -17
- package/dist/hooks/shims/hermes.d.ts.map +1 -1
- package/dist/hooks/shims/hermes.js +9 -45
- package/dist/hooks/shims/hermes.js.map +1 -1
- package/dist/index.d.ts +33 -76
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -67
- package/dist/index.js.map +1 -1
- package/dist/lang/bash.d.ts.map +1 -1
- package/dist/lang/bash.js.map +1 -1
- package/dist/lang/c.d.ts.map +1 -1
- package/dist/lang/c.js.map +1 -1
- package/dist/lang/c_sharp.d.ts.map +1 -1
- package/dist/lang/c_sharp.js.map +1 -1
- package/dist/lang/cpp.d.ts.map +1 -1
- package/dist/lang/cpp.js.map +1 -1
- package/dist/lang/go.d.ts.map +1 -1
- package/dist/lang/go.js.map +1 -1
- package/dist/lang/java.d.ts.map +1 -1
- package/dist/lang/java.js.map +1 -1
- package/dist/lang/javascript.d.ts.map +1 -1
- package/dist/lang/javascript.js.map +1 -1
- package/dist/lang/kotlin.d.ts.map +1 -1
- package/dist/lang/kotlin.js.map +1 -1
- package/dist/lang/php.d.ts.map +1 -1
- package/dist/lang/php.js.map +1 -1
- package/dist/lang/profile.d.ts.map +1 -1
- package/dist/lang/profile.js.map +1 -1
- package/dist/lang/python.d.ts.map +1 -1
- package/dist/lang/python.js.map +1 -1
- package/dist/lang/registry.d.ts.map +1 -1
- package/dist/lang/registry.js.map +1 -1
- package/dist/lang/ruby.d.ts.map +1 -1
- package/dist/lang/ruby.js.map +1 -1
- package/dist/lang/rust.d.ts.map +1 -1
- package/dist/lang/rust.js.map +1 -1
- package/dist/lang/swift.d.ts.map +1 -1
- package/dist/lang/swift.js.map +1 -1
- package/dist/lang/tsx.d.ts.map +1 -1
- package/dist/lang/tsx.js.map +1 -1
- package/dist/lang/typescript.d.ts.map +1 -1
- package/dist/lang/typescript.js.map +1 -1
- package/dist/net/policy.d.ts +18 -1
- package/dist/net/policy.d.ts.map +1 -1
- package/dist/net/policy.js +6 -4
- package/dist/net/policy.js.map +1 -1
- package/dist/ops/index.d.ts +39 -0
- package/dist/ops/index.d.ts.map +1 -0
- package/dist/ops/index.js +37 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/ops/inputs.d.ts +170 -0
- package/dist/ops/inputs.d.ts.map +1 -0
- package/dist/ops/inputs.js +142 -0
- package/dist/ops/inputs.js.map +1 -0
- package/dist/ops/verbs.d.ts +136 -0
- package/dist/ops/verbs.d.ts.map +1 -0
- package/dist/ops/verbs.js +76 -0
- package/dist/ops/verbs.js.map +1 -0
- package/dist/plan/auto.d.ts +69 -0
- package/dist/plan/auto.d.ts.map +1 -0
- package/dist/plan/auto.js +70 -0
- package/dist/plan/auto.js.map +1 -0
- package/dist/plan/budget.d.ts +21 -0
- package/dist/plan/budget.d.ts.map +1 -0
- package/dist/plan/budget.js +26 -0
- package/dist/plan/budget.js.map +1 -0
- package/dist/plan/grammar.d.ts +14 -0
- package/dist/plan/grammar.d.ts.map +1 -1
- package/dist/plan/grammar.js +47 -6
- package/dist/plan/grammar.js.map +1 -1
- package/dist/plan/lexical.d.ts.map +1 -1
- package/dist/plan/lexical.js +1 -7
- package/dist/plan/lexical.js.map +1 -1
- package/dist/plan/planners.d.ts +35 -2
- package/dist/plan/planners.d.ts.map +1 -1
- package/dist/plan/planners.js +35 -2
- package/dist/plan/planners.js.map +1 -1
- package/dist/plan/structural.d.ts +19 -5
- package/dist/plan/structural.d.ts.map +1 -1
- package/dist/plan/structural.js +180 -33
- package/dist/plan/structural.js.map +1 -1
- package/dist/repomap/cache.d.ts +48 -0
- package/dist/repomap/cache.d.ts.map +1 -1
- package/dist/repomap/cache.js +112 -13
- package/dist/repomap/cache.js.map +1 -1
- package/dist/repomap/io.d.ts +24 -0
- package/dist/repomap/io.d.ts.map +1 -0
- package/dist/repomap/io.js +34 -0
- package/dist/repomap/io.js.map +1 -0
- package/dist/repomap/map.d.ts +69 -6
- package/dist/repomap/map.d.ts.map +1 -1
- package/dist/repomap/map.js +104 -22
- package/dist/repomap/map.js.map +1 -1
- package/dist/repomap/rank.d.ts +26 -2
- package/dist/repomap/rank.d.ts.map +1 -1
- package/dist/repomap/rank.js +20 -0
- package/dist/repomap/rank.js.map +1 -1
- package/dist/repomap/reader.d.ts +73 -0
- package/dist/repomap/reader.d.ts.map +1 -0
- package/dist/repomap/reader.js +30 -0
- package/dist/repomap/reader.js.map +1 -0
- package/dist/repomap/tags.d.ts.map +1 -1
- package/dist/repomap/tags.js.map +1 -1
- package/dist/retrieve.d.ts +8 -0
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +12 -1
- package/dist/retrieve.js.map +1 -1
- package/dist/smelter.d.ts +79 -0
- package/dist/smelter.d.ts.map +1 -0
- package/dist/smelter.js +65 -0
- package/dist/smelter.js.map +1 -0
- package/dist/stages.d.ts.map +1 -1
- package/dist/stages.js +2 -2
- package/dist/stages.js.map +1 -1
- package/dist/stats.d.ts +2 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +2 -1
- package/dist/stats.js.map +1 -1
- package/dist/store-dir.d.ts +43 -4
- package/dist/store-dir.d.ts.map +1 -1
- package/dist/store-dir.js +48 -5
- package/dist/store-dir.js.map +1 -1
- package/dist/store.d.ts +6 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +6 -0
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +35 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +11 -11
- package/dist/cli/resolve.d.ts +0 -103
- package/dist/cli/resolve.d.ts.map +0 -1
- package/dist/cli/resolve.js +0 -106
- package/dist/cli/resolve.js.map +0 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { reconstruct } from '../../apply.js';
|
|
2
|
+
import { SUPPORTED_LANGUAGES } from '../../detect.js';
|
|
3
|
+
import { CliUsageError, SmeltError } from '../../errors.js';
|
|
4
|
+
import { budgetRequired, openStore, readBlob, resolveStrategy } from '../../ops/inputs.js';
|
|
5
|
+
import { smeltBlob } from '../../ops/verbs.js';
|
|
6
|
+
import { isStrategy, STRATEGIES } from '../../plan/planners.js';
|
|
7
|
+
import { MemoryElisionStore } from '../../store.js';
|
|
8
|
+
import { CONFIG_FILE_NAME, configuredStore } from '../config.js';
|
|
9
|
+
import { formatReport } from '../report.js';
|
|
10
|
+
import { CLI_NAME, EXIT } from '../shell.js';
|
|
11
|
+
import { flagList, parseBudget, VERB_FLAGS } from './flags.js';
|
|
12
|
+
/**
|
|
13
|
+
* The `--json` envelope format, versioned like the marker for the same reason: this is
|
|
14
|
+
* a surface other programs parse, so a change to it has to be identifiable rather than
|
|
15
|
+
* silent.
|
|
16
|
+
*/
|
|
17
|
+
export const CLI_JSON_FORMAT = 'smelt-cli/v1';
|
|
18
|
+
export const smeltCommand = {
|
|
19
|
+
name: 'smelt',
|
|
20
|
+
flags: ['budget', 'focus', 'language', 'strategy', 'json', 'reconstruct'],
|
|
21
|
+
refusal: `A single-blob run reads one file or stdin; there is no tree to walk, ` +
|
|
22
|
+
`nothing to cache, and no harness to install into.`,
|
|
23
|
+
usage: {
|
|
24
|
+
synopsis: [
|
|
25
|
+
'<file> --budget <bytes> [--focus <term>]...',
|
|
26
|
+
'--budget <bytes> [--focus <term>]... < input',
|
|
27
|
+
],
|
|
28
|
+
occasional: ['--reconstruct <result.json>', '--reconstruct < result.json'],
|
|
29
|
+
},
|
|
30
|
+
parse(values, positionals) {
|
|
31
|
+
if (positionals.length > 1) {
|
|
32
|
+
throw new CliUsageError(`${CLI_NAME}: expected at most one file, got ${String(positionals.length)} ` +
|
|
33
|
+
`(${positionals.join(', ')}). smelt reads one blob at a time.`);
|
|
34
|
+
}
|
|
35
|
+
const file = positionals[0];
|
|
36
|
+
if (values.reconstruct === true) {
|
|
37
|
+
refuseReconstructFlags(values);
|
|
38
|
+
return {
|
|
39
|
+
mode: 'reconstruct',
|
|
40
|
+
...(file === undefined ? {} : { file }),
|
|
41
|
+
focus: [],
|
|
42
|
+
json: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const budgetBytes = parseBudget(values.budget);
|
|
46
|
+
const chosenStrategy = parseStrategy(values.strategy);
|
|
47
|
+
return {
|
|
48
|
+
mode: 'smelt',
|
|
49
|
+
...(file === undefined ? {} : { file }),
|
|
50
|
+
...(budgetBytes === undefined ? {} : { budgetBytes }),
|
|
51
|
+
focus: values.focus ?? [],
|
|
52
|
+
...(values.language === undefined ? {} : { language: parseLanguage(values.language) }),
|
|
53
|
+
...(chosenStrategy === undefined ? {} : { strategy: chosenStrategy }),
|
|
54
|
+
json: values.json === true,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
resolve(invocation, config) {
|
|
58
|
+
if (invocation.mode === 'reconstruct') {
|
|
59
|
+
return {
|
|
60
|
+
kind: 'reconstruct',
|
|
61
|
+
...(invocation.file === undefined ? {} : { file: invocation.file }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { kind: 'smelt', run: resolveRun(invocation, config()) };
|
|
65
|
+
},
|
|
66
|
+
run(resolved, io) {
|
|
67
|
+
if (resolved.kind === 'reconstruct') {
|
|
68
|
+
return runReconstruct(readInput(resolved.file, io), io);
|
|
69
|
+
}
|
|
70
|
+
return runSmelt(resolved.run, io);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Every flag this verb owns that `--reconstruct` cannot honour, with the reason each
|
|
75
|
+
* one makes no sense — the second job's flag ownership, which the registry cannot
|
|
76
|
+
* express because both jobs are the same verb.
|
|
77
|
+
*
|
|
78
|
+
* `refuseForeignFlags` refuses a flag no verb owns *here*; it cannot refuse `--focus`
|
|
79
|
+
* on a single-blob run, because a single-blob run is exactly where `--focus` belongs.
|
|
80
|
+
* So the ones the round trip ignores are named here instead, and named exhaustively:
|
|
81
|
+
* every flag on `smeltCommand.flags` except `--reconstruct` itself has an entry, which
|
|
82
|
+
* `test/guards/subcommand-registry.test.ts` crosses. A flag added to this verb and
|
|
83
|
+
* forgotten here would be silently ignored by half of the verb's runs — the failure
|
|
84
|
+
* this table exists to make impossible.
|
|
85
|
+
*
|
|
86
|
+
* Key order is the flag table's order, so a refusal lists flags the way every other
|
|
87
|
+
* refusal does.
|
|
88
|
+
*/
|
|
89
|
+
const RECONSTRUCT_REFUSALS = {
|
|
90
|
+
budget: `Reconstruction puts every byte back; there is nothing to fit.`,
|
|
91
|
+
focus: `Focus decides what survives a cut, and the cut has already been made — ` +
|
|
92
|
+
`the envelope names every elision it took.`,
|
|
93
|
+
language: `Nothing is detected or parsed on the way back: the envelope carries the ` +
|
|
94
|
+
`bytes and the ranges the cut recorded.`,
|
|
95
|
+
strategy: `No planner runs on the way back — the elisions come from the envelope.`,
|
|
96
|
+
json: `--reconstruct reads a --json envelope and prints the original text; ` +
|
|
97
|
+
`there is no second envelope to write.`,
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Refuse every flag `--reconstruct` would otherwise ignore, in one message shaped like
|
|
101
|
+
* the ownership refusals: the flags named, then why not here. A {@link CliUsageError},
|
|
102
|
+
* so it exits 2 exactly as every other refusal does.
|
|
103
|
+
*
|
|
104
|
+
* @throws {CliUsageError} naming each offending flag and what the round trip does instead.
|
|
105
|
+
*/
|
|
106
|
+
function refuseReconstructFlags(values) {
|
|
107
|
+
const reasons = RECONSTRUCT_REFUSALS;
|
|
108
|
+
const offending = VERB_FLAGS.filter((flag) => reasons[flag] !== undefined && values[flag] !== undefined);
|
|
109
|
+
if (offending.length === 0)
|
|
110
|
+
return;
|
|
111
|
+
const verb = offending.length === 1 ? 'makes' : 'make';
|
|
112
|
+
const why = offending.map((flag) => reasons[flag] ?? '').join(' ');
|
|
113
|
+
throw new CliUsageError(`${CLI_NAME}: ${flagList(offending)} ${verb} no sense with --reconstruct. ${why}`);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Merge one `'smelt'`-mode invocation with the loaded config (or `undefined` when no
|
|
117
|
+
* `smelt.config.json` exists) and the built-in defaults.
|
|
118
|
+
*
|
|
119
|
+
* The precedence is strict and one-directional: an explicit flag always wins over the
|
|
120
|
+
* config, and the config only fills what the flags left unsaid. Built-ins fill last,
|
|
121
|
+
* and only where a built-in exists at all — the budget deliberately has none, so a run
|
|
122
|
+
* with no budget from either source is refused here, in the one function that owns that
|
|
123
|
+
* error.
|
|
124
|
+
*
|
|
125
|
+
* @throws {CliUsageError} when neither `--budget` nor the config names a budget.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveRun(invocation, config) {
|
|
128
|
+
const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;
|
|
129
|
+
if (budgetBytes === undefined) {
|
|
130
|
+
throw new CliUsageError(`${CLI_NAME}: ` +
|
|
131
|
+
budgetRequired({
|
|
132
|
+
knob: '--budget',
|
|
133
|
+
stake: 'your context to throw away',
|
|
134
|
+
advice: `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +
|
|
135
|
+
`(\`${CLI_NAME} init\` writes one).\n` +
|
|
136
|
+
` ${CLI_NAME} src/server.ts --budget 4000 --focus handleRequest`,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
const strategy = resolveStrategy(invocation.strategy, config?.config.strategy);
|
|
140
|
+
return {
|
|
141
|
+
budgetBytes,
|
|
142
|
+
budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',
|
|
143
|
+
strategy: strategy.strategy,
|
|
144
|
+
strategySource: strategy.source,
|
|
145
|
+
store: configuredStore(config),
|
|
146
|
+
...(invocation.file === undefined ? {} : { file: invocation.file }),
|
|
147
|
+
focus: invocation.focus,
|
|
148
|
+
...(invocation.language === undefined ? {} : { language: invocation.language }),
|
|
149
|
+
json: invocation.json,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* One smelt run: read the input, hand the resolved values to {@link smeltBlob}, render.
|
|
154
|
+
*
|
|
155
|
+
* All merging — flags versus `smelt.config.json` versus built-ins, including the
|
|
156
|
+
* budget-required refusal — happens in {@link resolveRun}, which is the only place
|
|
157
|
+
* precedence lives. This function reads the resolved object and never consults a flag
|
|
158
|
+
* or a config field directly. The cut itself belongs to no front door: `smeltBlob` in
|
|
159
|
+
* `ops/verbs.ts` builds the smelter and returns the values this function prints, and
|
|
160
|
+
* the `smelt_file` tool calls the same op with its own arguments.
|
|
161
|
+
*/
|
|
162
|
+
async function runSmelt(run, io) {
|
|
163
|
+
const inputText = readInput(run.file, io);
|
|
164
|
+
const outcome = await smeltBlob({
|
|
165
|
+
text: inputText,
|
|
166
|
+
source: run.file ?? '<stdin>',
|
|
167
|
+
budgetBytes: run.budgetBytes,
|
|
168
|
+
strategy: run.strategy,
|
|
169
|
+
store: openStore(run.store),
|
|
170
|
+
...(run.file === undefined ? {} : { path: run.file }),
|
|
171
|
+
...(run.language === undefined ? {} : { language: run.language }),
|
|
172
|
+
focus: run.focus,
|
|
173
|
+
});
|
|
174
|
+
if (run.json) {
|
|
175
|
+
io.stdout(`${JSON.stringify(envelope(outcome.result, outcome.store), null, 2)}\n`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
io.stdout(outcome.result.text);
|
|
179
|
+
}
|
|
180
|
+
io.stderr(formatReport(outcome));
|
|
181
|
+
return outcome.result.outputBytes > run.budgetBytes ? EXIT.overBudget : EXIT.ok;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Law 3, from a shell.
|
|
185
|
+
*
|
|
186
|
+
* This is deliberately not "print the text and hope": it rebuilds the store from the
|
|
187
|
+
* envelope, checks every hash against the bytes it claims to key, and checks the
|
|
188
|
+
* reconstructed length against the `inputBytes` the result recorded at the time of the
|
|
189
|
+
* cut. A round trip that quietly returns almost-right text is the failure this whole
|
|
190
|
+
* repository is arranged against.
|
|
191
|
+
*/
|
|
192
|
+
function runReconstruct(text, io) {
|
|
193
|
+
const { result, elided } = parseEnvelope(text);
|
|
194
|
+
const store = new MemoryElisionStore();
|
|
195
|
+
for (const [hash, content] of Object.entries(elided)) {
|
|
196
|
+
const actual = store.put(content);
|
|
197
|
+
if (actual !== hash) {
|
|
198
|
+
throw new CliUsageError(`${CLI_NAME}: envelope is not self-consistent — it stores bytes under ` +
|
|
199
|
+
`"${hash}" that actually hash to "${actual}". Refusing to reconstruct from it.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const elision of result.elisions) {
|
|
203
|
+
if (!store.has(elision.hash)) {
|
|
204
|
+
throw new CliUsageError(`${CLI_NAME}: envelope is missing the bytes for "${elision.hash}". A result ` +
|
|
205
|
+
`without its elided bytes cannot be reconstructed; re-run with --json.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const original = reconstruct(result, store);
|
|
209
|
+
const bytes = Buffer.byteLength(original, 'utf8');
|
|
210
|
+
if (bytes !== result.inputBytes) {
|
|
211
|
+
throw new SmeltError(`${CLI_NAME}: reconstruction produced ${String(bytes)} bytes but the result ` +
|
|
212
|
+
`recorded ${String(result.inputBytes)}. The round trip did not close.`);
|
|
213
|
+
}
|
|
214
|
+
io.stdout(original);
|
|
215
|
+
io.stderr(`${CLI_NAME} reconstructed ${String(result.inputBytes)} B from ` +
|
|
216
|
+
`${String(result.elisions.length)} elisions — byte for byte\n`);
|
|
217
|
+
return EXIT.ok;
|
|
218
|
+
}
|
|
219
|
+
/** `peek`, not `retrieve`: writing a file is not the model asking for anything back. */
|
|
220
|
+
function envelope(result, store) {
|
|
221
|
+
const elided = {};
|
|
222
|
+
for (const elision of result.elisions) {
|
|
223
|
+
const content = store.peek(elision.hash);
|
|
224
|
+
if (content === undefined) {
|
|
225
|
+
throw new SmeltError(`${CLI_NAME}: the store does not hold "${elision.hash}", which its own result ` +
|
|
226
|
+
`says it elided. Refusing to write an envelope that cannot round-trip.`);
|
|
227
|
+
}
|
|
228
|
+
elided[elision.hash] = content;
|
|
229
|
+
}
|
|
230
|
+
return { format: CLI_JSON_FORMAT, result, elided };
|
|
231
|
+
}
|
|
232
|
+
function parseEnvelope(text) {
|
|
233
|
+
let value;
|
|
234
|
+
try {
|
|
235
|
+
value = JSON.parse(text);
|
|
236
|
+
}
|
|
237
|
+
catch (cause) {
|
|
238
|
+
throw new CliUsageError(`${CLI_NAME}: --reconstruct expected a --json envelope, and this is not JSON: ` +
|
|
239
|
+
`${cause instanceof Error ? cause.message : String(cause)}`);
|
|
240
|
+
}
|
|
241
|
+
if (typeof value !== 'object' || value === null) {
|
|
242
|
+
throw new CliUsageError(`${CLI_NAME}: --reconstruct expected a JSON object.`);
|
|
243
|
+
}
|
|
244
|
+
const fields = value;
|
|
245
|
+
if (fields['format'] !== CLI_JSON_FORMAT) {
|
|
246
|
+
throw new CliUsageError(`${CLI_NAME}: this envelope says format "${String(fields['format'])}"; this build ` +
|
|
247
|
+
`reads "${CLI_JSON_FORMAT}". Formats are versioned so a mismatch is visible ` +
|
|
248
|
+
`instead of being half-understood.`);
|
|
249
|
+
}
|
|
250
|
+
const result = fields['result'];
|
|
251
|
+
if (typeof result !== 'object' || result === null) {
|
|
252
|
+
throw new CliUsageError(`${CLI_NAME}: envelope has no \`result\` object.`);
|
|
253
|
+
}
|
|
254
|
+
const resultFields = result;
|
|
255
|
+
if (typeof resultFields['text'] !== 'string' ||
|
|
256
|
+
typeof resultFields['inputBytes'] !== 'number' ||
|
|
257
|
+
!Array.isArray(resultFields['elisions'])) {
|
|
258
|
+
throw new CliUsageError(`${CLI_NAME}: envelope's \`result\` is missing text, inputBytes or elisions.`);
|
|
259
|
+
}
|
|
260
|
+
const elided = fields['elided'];
|
|
261
|
+
if (typeof elided !== 'object' || elided === null) {
|
|
262
|
+
throw new CliUsageError(`${CLI_NAME}: envelope has no \`elided\` map.`);
|
|
263
|
+
}
|
|
264
|
+
for (const [hash, content] of Object.entries(elided)) {
|
|
265
|
+
if (typeof content !== 'string') {
|
|
266
|
+
throw new CliUsageError(`${CLI_NAME}: envelope's \`elided["${hash}"]\` is not a string.`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
format: CLI_JSON_FORMAT,
|
|
271
|
+
result: result,
|
|
272
|
+
elided: elided,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The blob to work on: stdin when no file was named, otherwise the file — read
|
|
277
|
+
* through the ops law, so "cannot read X" says the same thing here and in the
|
|
278
|
+
* `smelt_file` tool. Only the stdin leg is the CLI's own; a tool has no stdin.
|
|
279
|
+
*/
|
|
280
|
+
function readInput(file, io) {
|
|
281
|
+
if (file === undefined)
|
|
282
|
+
return io.stdin();
|
|
283
|
+
const read = readBlob(file, file);
|
|
284
|
+
if (!read.ok)
|
|
285
|
+
throw new CliUsageError(`${CLI_NAME}: ${read.refusal}`);
|
|
286
|
+
return read.value;
|
|
287
|
+
}
|
|
288
|
+
function parseLanguage(raw) {
|
|
289
|
+
const known = [...SUPPORTED_LANGUAGES, 'unknown'];
|
|
290
|
+
if (!known.includes(raw)) {
|
|
291
|
+
throw new CliUsageError(`${CLI_NAME}: unknown --language "${raw}". Known: ${known.join(', ')}.`);
|
|
292
|
+
}
|
|
293
|
+
return raw;
|
|
294
|
+
}
|
|
295
|
+
/** Membership in the {@link PLANNERS} registry is the whole validation. */
|
|
296
|
+
function parseStrategy(raw) {
|
|
297
|
+
if (raw === undefined)
|
|
298
|
+
return undefined;
|
|
299
|
+
if (!isStrategy(raw)) {
|
|
300
|
+
throw new CliUsageError(`${CLI_NAME}: unknown --strategy "${raw}". Known: ${STRATEGIES.join(', ')}.`);
|
|
301
|
+
}
|
|
302
|
+
return raw;
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=smelt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smelt.js","sourceRoot":"","sources":["../../../src/cli/subcommands/smelt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3F,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEpD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEjE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAG7C,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAuE/D;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,cAAc,CAAC;AAkB9C,MAAM,CAAC,MAAM,YAAY,GAAsD;IAC7E,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC;IACzE,OAAO,EACL,uEAAuE;QACvE,mDAAmD;IACrD,KAAK,EAAE;QACL,QAAQ,EAAE;YACR,6CAA6C;YAC7C,8CAA8C;SAC/C;QACD,UAAU,EAAE,CAAC,6BAA6B,EAAE,6BAA6B,CAAC;KAC3E;IAED,KAAK,CAAC,MAAkB,EAAE,WAA8B;QACtD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,oCAAoC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG;gBAC1E,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,oCAAoC,CACjE,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;QAE5B,IAAI,MAAM,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;YAChC,sBAAsB,CAAC,MAAM,CAAC,CAAC;YAC/B,OAAO;gBACL,IAAI,EAAE,aAAa;gBACnB,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;gBACvC,KAAK,EAAE,EAAE;gBACT,IAAI,EAAE,KAAK;aACZ,CAAC;QACJ,CAAC;QAED,MAAM,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC/C,MAAM,cAAc,GAAG,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACtD,OAAO;YACL,IAAI,EAAE,OAAO;YACb,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;YACvC,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;YACrD,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE;YACzB,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtF,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;YACrE,IAAI,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI;SAC3B,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,UAA2B,EAAE,MAAoB;QACvD,IAAI,UAAU,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YACtC,OAAO;gBACL,IAAI,EAAE,aAAa;gBACnB,GAAG,CAAC,UAAU,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;aACpE,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IAClE,CAAC;IAED,GAAG,CAAC,QAA8B,EAAE,EAAS;QAC3C,IAAI,QAAQ,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YACpC,OAAO,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1D,CAAC;QACD,OAAO,QAAQ,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACpC,CAAC;CACF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,oBAAoB,GAAG;IAC3B,MAAM,EAAE,+DAA+D;IACvE,KAAK,EACH,yEAAyE;QACzE,2CAA2C;IAC7C,QAAQ,EACN,0EAA0E;QAC1E,wCAAwC;IAC1C,QAAQ,EAAE,wEAAwE;IAClF,IAAI,EACF,sEAAsE;QACtE,uCAAuC;CACW,CAAC;AAEvD;;;;;;GAMG;AACH,SAAS,sBAAsB,CAAC,MAAkB;IAChD,MAAM,OAAO,GAAsC,oBAAoB,CAAC;IACxE,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CACjC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,SAAS,CACpE,CAAC;IACF,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAEnC,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IACvD,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnE,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,KAAK,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,iCAAiC,GAAG,EAAE,CAClF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CACxB,UAA2B,EAC3B,MAAgC;IAEhC,MAAM,WAAW,GAAG,UAAU,CAAC,WAAW,IAAI,MAAM,EAAE,MAAM,CAAC,kBAAkB,CAAC;IAChF,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,IAAI;YACb,cAAc,CAAC;gBACb,IAAI,EAAE,UAAU;gBAChB,KAAK,EAAE,4BAA4B;gBACnC,MAAM,EACJ,+CAA+C,gBAAgB,GAAG;oBAClE,MAAM,QAAQ,wBAAwB;oBACtC,KAAK,QAAQ,oDAAoD;aACpE,CAAC,CACL,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,eAAe,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAE/E,OAAO;QACL,WAAW;QACX,YAAY,EAAE,UAAU,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;QACtE,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,cAAc,EAAE,QAAQ,CAAC,MAAM;QAC/B,KAAK,EAAE,eAAe,CAAC,MAAM,CAAC;QAC9B,GAAG,CAAC,UAAU,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;QACnE,KAAK,EAAE,UAAU,CAAC,KAAK;QACvB,GAAG,CAAC,UAAU,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC;QAC/E,IAAI,EAAE,UAAU,CAAC,IAAI;KACtB,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,QAAQ,CAAC,GAAgB,EAAE,EAAS;IACjD,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAE1C,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC;QAC9B,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,GAAG,CAAC,IAAI,IAAI,SAAS;QAC7B,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;QAC3B,GAAG,CAAC,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;QACrD,GAAG,CAAC,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;QACjE,KAAK,EAAE,GAAG,CAAC,KAAK;KACjB,CAAC,CAAC;IAEH,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QACb,EAAE,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACrF,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IACD,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IAEjC,OAAO,OAAO,CAAC,MAAM,CAAC,WAAW,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;AAClF,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,cAAc,CAAC,IAAY,EAAE,EAAS;IAC7C,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,IAAI,kBAAkB,EAAE,CAAC;IAEvC,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACrD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,4DAA4D;gBACrE,IAAI,IAAI,4BAA4B,MAAM,qCAAqC,CAClF,CAAC;QACJ,CAAC;IACH,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACtC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,wCAAwC,OAAO,CAAC,IAAI,cAAc;gBAC3E,uEAAuE,CAC1E,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAClD,IAAI,KAAK,KAAK,MAAM,CAAC,UAAU,EAAE,CAAC;QAChC,MAAM,IAAI,UAAU,CAClB,GAAG,QAAQ,6BAA6B,MAAM,CAAC,KAAK,CAAC,wBAAwB;YAC3E,YAAY,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,iCAAiC,CACzE,CAAC;IACJ,CAAC;IAED,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACpB,EAAE,CAAC,MAAM,CACP,GAAG,QAAQ,mBAAmB,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU;QAC/D,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,6BAA6B,CACjE,CAAC;IACF,OAAO,IAAI,CAAC,EAAE,CAAC;AACjB,CAAC;AAED,wFAAwF;AACxF,SAAS,QAAQ,CAAC,MAAmB,EAAE,KAAmB;IACxD,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,IAAI,UAAU,CAClB,GAAG,QAAQ,8BAA8B,OAAO,CAAC,IAAI,0BAA0B;gBAC7E,uEAAuE,CAC1E,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;IACjC,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACrD,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,oEAAoE;YAC7E,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAC9D,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,yCAAyC,CAAC,CAAC;IAChF,CAAC;IACD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAEhD,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,eAAe,EAAE,CAAC;QACzC,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,gCAAgC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,gBAAgB;YACjF,UAAU,eAAe,oDAAoD;YAC7E,mCAAmC,CACtC,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAClD,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,sCAAsC,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,YAAY,GAAG,MAAiC,CAAC;IACvD,IACE,OAAO,YAAY,CAAC,MAAM,CAAC,KAAK,QAAQ;QACxC,OAAO,YAAY,CAAC,YAAY,CAAC,KAAK,QAAQ;QAC9C,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,EACxC,CAAC;QACD,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,kEAAkE,CAC9E,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAClD,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,mCAAmC,CAAC,CAAC;IAC1E,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACrD,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,0BAA0B,IAAI,uBAAuB,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC;IAED,OAAO;QACL,MAAM,EAAE,eAAe;QACvB,MAAM,EAAE,MAAgC;QACxC,MAAM,EAAE,MAAgC;KACzC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,SAAS,CAAC,IAAwB,EAAE,EAAS;IACpD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC;IAC1C,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,IAAI,aAAa,CAAC,GAAG,QAAQ,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC,KAAK,CAAC;AACpB,CAAC;AAED,SAAS,aAAa,CAAC,GAAW;IAChC,MAAM,KAAK,GAAsB,CAAC,GAAG,mBAAmB,EAAE,SAAS,CAAC,CAAC;IACrE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,yBAAyB,GAAG,aAAa,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACxE,CAAC;IACJ,CAAC;IACD,OAAO,GAAuB,CAAC;AACjC,CAAC;AAED,2EAA2E;AAC3E,SAAS,aAAa,CAAC,GAAuB;IAC5C,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,yBAAyB,GAAG,aAAa,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC7E,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","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"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RetrieveStats } from '../../types.ts';
|
|
2
|
+
import type { ResolvedStoreRun } from './retrieve.ts';
|
|
3
|
+
import type { Subcommand } from './subcommand.ts';
|
|
4
|
+
/**
|
|
5
|
+
* `smelt stats` — the store's counters, without touching them.
|
|
6
|
+
*
|
|
7
|
+
* Reading stats does NOT count as a retrieval: `stats()` folds the journal and scans
|
|
8
|
+
* the blobs, journaling nothing, so watching the expansion rate can never move it —
|
|
9
|
+
* an observer that inflated its own metric would make the honest signal dishonest.
|
|
10
|
+
*
|
|
11
|
+
* The plain form is one `name value` per line, greppable and stable; `--json` emits
|
|
12
|
+
* the {@link RetrieveStats} verbatim in its own versioned envelope
|
|
13
|
+
* ({@link CLI_STATS_JSON_FORMAT}), like every other machine-read surface here.
|
|
14
|
+
*
|
|
15
|
+
* It resolves through `retrieve`'s {@link resolveStoreRun} because the two verbs
|
|
16
|
+
* share one merge — the store leg, and the same refusal when that store cannot
|
|
17
|
+
* outlive a run. Sharing the function is the point; sharing a struct with a hash
|
|
18
|
+
* field `stats` would never read is not.
|
|
19
|
+
*/
|
|
20
|
+
/** `smelt stats` — the store's counters, read without touching them. */
|
|
21
|
+
export interface StatsInvocation {
|
|
22
|
+
readonly mode: 'stats';
|
|
23
|
+
readonly json: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** What `stats` runs on: the shared store leg, plus how to print it. */
|
|
26
|
+
export interface ResolvedStatsRun {
|
|
27
|
+
readonly store: ResolvedStoreRun;
|
|
28
|
+
readonly json: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The `smelt stats --json` envelope format. Its own version line for the same reason
|
|
32
|
+
* `smelt map` has one: the two envelopes carry different structures and must move
|
|
33
|
+
* independently.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CLI_STATS_JSON_FORMAT = "smelt-stats-cli/v1";
|
|
36
|
+
/** What `smelt stats --json` prints: the {@link RetrieveStats} verbatim, versioned. */
|
|
37
|
+
export interface CliStatsJsonEnvelope {
|
|
38
|
+
readonly format: string;
|
|
39
|
+
/** The {@link RetrieveStats} exactly as the store's `stats()` returned them. */
|
|
40
|
+
readonly stats: RetrieveStats;
|
|
41
|
+
}
|
|
42
|
+
export declare const statsCommand: Subcommand<StatsInvocation, ResolvedStatsRun>;
|
|
43
|
+
//# sourceMappingURL=stats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/stats.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAMpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;;;;;;;;GAeG;AAEH,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAE1D,uFAAuF;AACvF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;CAC/B;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,gBAAgB,CA2DtE,CAAC","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { openStore } from '../../ops/inputs.ts';\nimport { readCounters } from '../../ops/verbs.ts';\nimport type { RetrieveStats } from '../../types.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport { resolveStoreRun } from './retrieve.ts';\nimport type { ResolvedStoreRun } from './retrieve.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt stats` — the store's counters, without touching them.\n *\n * Reading stats does NOT count as a retrieval: `stats()` folds the journal and scans\n * the blobs, journaling nothing, so watching the expansion rate can never move it —\n * an observer that inflated its own metric would make the honest signal dishonest.\n *\n * The plain form is one `name value` per line, greppable and stable; `--json` emits\n * the {@link RetrieveStats} verbatim in its own versioned envelope\n * ({@link CLI_STATS_JSON_FORMAT}), like every other machine-read surface here.\n *\n * It resolves through `retrieve`'s {@link resolveStoreRun} because the two verbs\n * share one merge — the store leg, and the same refusal when that store cannot\n * outlive a run. Sharing the function is the point; sharing a struct with a hash\n * field `stats` would never read is not.\n */\n\n/** `smelt stats` — the store's counters, read without touching them. */\nexport interface StatsInvocation {\n readonly mode: 'stats';\n readonly json: boolean;\n}\n\n/** What `stats` runs on: the shared store leg, plus how to print it. */\nexport interface ResolvedStatsRun {\n readonly store: ResolvedStoreRun;\n readonly json: boolean;\n}\n\n/**\n * The `smelt stats --json` envelope format. Its own version line for the same reason\n * `smelt map` has one: the two envelopes carry different structures and must move\n * independently.\n */\nexport const CLI_STATS_JSON_FORMAT = 'smelt-stats-cli/v1';\n\n/** What `smelt stats --json` prints: the {@link RetrieveStats} verbatim, versioned. */\nexport interface CliStatsJsonEnvelope {\n readonly format: string;\n /** The {@link RetrieveStats} exactly as the store's `stats()` returned them. */\n readonly stats: RetrieveStats;\n}\n\nexport const statsCommand: Subcommand<StatsInvocation, ResolvedStatsRun> = {\n name: 'stats',\n flags: ['json'],\n refusal: `stats reads counters; there is nothing to budget, focus or plan.`,\n usage: {\n synopsis: ['stats [--json]'],\n section: {\n heading: 'RETRIEVE & STATS',\n body:\n ` ${CLI_NAME} stats prints the same store's counters, one \\`name value\\` per line —\\n` +\n ` elisionsStored, bytesStored, retrieveCalls, uniqueRetrieved, expansionRate,\\n` +\n ` allElisionsRetrieved — and reading them is NOT counted as a retrieval. --json\\n` +\n ` emits the RetrieveStats verbatim in its own versioned envelope.\\n` +\n `\\n` +\n ` Both need somewhere for elisions to outlive the run that made them: a\\n` +\n ` smelt.config.json with a directory store (\\`${CLI_NAME} init\\` writes one). With a\\n` +\n ` memory store — or no config — every run's store dies with its process, so there\\n` +\n ` is nothing to retrieve across runs, and that is a usage error rather than a\\n` +\n ` quiet empty answer.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): StatsInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: stats takes no further arguments, got ` +\n `${positionals.slice(1).join(', ')}. It reports on the one configured store.`,\n );\n }\n return { mode: 'stats', json: values.json === true };\n },\n\n resolve(invocation: StatsInvocation, config: ConfigSource): ResolvedStatsRun {\n return { store: resolveStoreRun('stats', config()), json: invocation.json };\n },\n\n run(resolved: ResolvedStatsRun, io: CliIo): number {\n const store = openStore({ kind: 'directory', path: resolved.store.storePath });\n const stats = readCounters({ store });\n\n if (resolved.json) {\n const statsEnvelope: CliStatsJsonEnvelope = { format: CLI_STATS_JSON_FORMAT, stats };\n io.stdout(`${JSON.stringify(statsEnvelope, null, 2)}\\n`);\n return EXIT.ok;\n }\n\n io.stdout(\n [\n `elisionsStored ${String(stats.elisionsStored)}`,\n `bytesStored ${String(stats.bytesStored)}`,\n `retrieveCalls ${String(stats.retrieveCalls)}`,\n `uniqueRetrieved ${String(stats.uniqueRetrieved)}`,\n `expansionRate ${String(stats.expansionRate)}`,\n `allElisionsRetrieved ${String(stats.allElisionsRetrieved)}`,\n '',\n ].join('\\n'),\n );\n return EXIT.ok;\n },\n};\n"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { CliUsageError } from '../../errors.js';
|
|
2
|
+
import { openStore } from '../../ops/inputs.js';
|
|
3
|
+
import { readCounters } from '../../ops/verbs.js';
|
|
4
|
+
import { CLI_NAME, EXIT } from '../shell.js';
|
|
5
|
+
import { resolveStoreRun } from './retrieve.js';
|
|
6
|
+
/**
|
|
7
|
+
* The `smelt stats --json` envelope format. Its own version line for the same reason
|
|
8
|
+
* `smelt map` has one: the two envelopes carry different structures and must move
|
|
9
|
+
* independently.
|
|
10
|
+
*/
|
|
11
|
+
export const CLI_STATS_JSON_FORMAT = 'smelt-stats-cli/v1';
|
|
12
|
+
export const statsCommand = {
|
|
13
|
+
name: 'stats',
|
|
14
|
+
flags: ['json'],
|
|
15
|
+
refusal: `stats reads counters; there is nothing to budget, focus or plan.`,
|
|
16
|
+
usage: {
|
|
17
|
+
synopsis: ['stats [--json]'],
|
|
18
|
+
section: {
|
|
19
|
+
heading: 'RETRIEVE & STATS',
|
|
20
|
+
body: ` ${CLI_NAME} stats prints the same store's counters, one \`name value\` per line —\n` +
|
|
21
|
+
` elisionsStored, bytesStored, retrieveCalls, uniqueRetrieved, expansionRate,\n` +
|
|
22
|
+
` allElisionsRetrieved — and reading them is NOT counted as a retrieval. --json\n` +
|
|
23
|
+
` emits the RetrieveStats verbatim in its own versioned envelope.\n` +
|
|
24
|
+
`\n` +
|
|
25
|
+
` Both need somewhere for elisions to outlive the run that made them: a\n` +
|
|
26
|
+
` smelt.config.json with a directory store (\`${CLI_NAME} init\` writes one). With a\n` +
|
|
27
|
+
` memory store — or no config — every run's store dies with its process, so there\n` +
|
|
28
|
+
` is nothing to retrieve across runs, and that is a usage error rather than a\n` +
|
|
29
|
+
` quiet empty answer.`,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
parse(values, positionals) {
|
|
33
|
+
if (positionals.length > 1) {
|
|
34
|
+
throw new CliUsageError(`${CLI_NAME}: stats takes no further arguments, got ` +
|
|
35
|
+
`${positionals.slice(1).join(', ')}. It reports on the one configured store.`);
|
|
36
|
+
}
|
|
37
|
+
return { mode: 'stats', json: values.json === true };
|
|
38
|
+
},
|
|
39
|
+
resolve(invocation, config) {
|
|
40
|
+
return { store: resolveStoreRun('stats', config()), json: invocation.json };
|
|
41
|
+
},
|
|
42
|
+
run(resolved, io) {
|
|
43
|
+
const store = openStore({ kind: 'directory', path: resolved.store.storePath });
|
|
44
|
+
const stats = readCounters({ store });
|
|
45
|
+
if (resolved.json) {
|
|
46
|
+
const statsEnvelope = { format: CLI_STATS_JSON_FORMAT, stats };
|
|
47
|
+
io.stdout(`${JSON.stringify(statsEnvelope, null, 2)}\n`);
|
|
48
|
+
return EXIT.ok;
|
|
49
|
+
}
|
|
50
|
+
io.stdout([
|
|
51
|
+
`elisionsStored ${String(stats.elisionsStored)}`,
|
|
52
|
+
`bytesStored ${String(stats.bytesStored)}`,
|
|
53
|
+
`retrieveCalls ${String(stats.retrieveCalls)}`,
|
|
54
|
+
`uniqueRetrieved ${String(stats.uniqueRetrieved)}`,
|
|
55
|
+
`expansionRate ${String(stats.expansionRate)}`,
|
|
56
|
+
`allElisionsRetrieved ${String(stats.allElisionsRetrieved)}`,
|
|
57
|
+
'',
|
|
58
|
+
].join('\n'));
|
|
59
|
+
return EXIT.ok;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.js","sourceRoot":"","sources":["../../../src/cli/subcommands/stats.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAI7C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAiChD;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,oBAAoB,CAAC;AAS1D,MAAM,CAAC,MAAM,YAAY,GAAkD;IACzE,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,OAAO,EAAE,kEAAkE;IAC3E,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,gBAAgB,CAAC;QAC5B,OAAO,EAAE;YACP,OAAO,EAAE,kBAAkB;YAC3B,IAAI,EACF,KAAK,QAAQ,0EAA0E;gBACvF,iFAAiF;gBACjF,mFAAmF;gBACnF,qEAAqE;gBACrE,IAAI;gBACJ,2EAA2E;gBAC3E,iDAAiD,QAAQ,+BAA+B;gBACxF,qFAAqF;gBACrF,iFAAiF;gBACjF,uBAAuB;SAC1B;KACF;IAED,KAAK,CAAC,MAAkB,EAAE,WAA8B;QACtD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,0CAA0C;gBACnD,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,2CAA2C,CAChF,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;IACvD,CAAC;IAED,OAAO,CAAC,UAA2B,EAAE,MAAoB;QACvD,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;IAC9E,CAAC;IAED,GAAG,CAAC,QAA0B,EAAE,EAAS;QACvC,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;QAC/E,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAEtC,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;YAClB,MAAM,aAAa,GAAyB,EAAE,MAAM,EAAE,qBAAqB,EAAE,KAAK,EAAE,CAAC;YACrF,EAAE,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YACzD,OAAO,IAAI,CAAC,EAAE,CAAC;QACjB,CAAC;QAED,EAAE,CAAC,MAAM,CACP;YACE,kBAAkB,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE;YAChD,eAAe,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE;YAC1C,iBAAiB,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE;YAC9C,mBAAmB,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE;YAClD,iBAAiB,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE;YAC9C,wBAAwB,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE;YAC5D,EAAE;SACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;QACF,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;CACF,CAAC","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { openStore } from '../../ops/inputs.ts';\nimport { readCounters } from '../../ops/verbs.ts';\nimport type { RetrieveStats } from '../../types.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport { resolveStoreRun } from './retrieve.ts';\nimport type { ResolvedStoreRun } from './retrieve.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt stats` — the store's counters, without touching them.\n *\n * Reading stats does NOT count as a retrieval: `stats()` folds the journal and scans\n * the blobs, journaling nothing, so watching the expansion rate can never move it —\n * an observer that inflated its own metric would make the honest signal dishonest.\n *\n * The plain form is one `name value` per line, greppable and stable; `--json` emits\n * the {@link RetrieveStats} verbatim in its own versioned envelope\n * ({@link CLI_STATS_JSON_FORMAT}), like every other machine-read surface here.\n *\n * It resolves through `retrieve`'s {@link resolveStoreRun} because the two verbs\n * share one merge — the store leg, and the same refusal when that store cannot\n * outlive a run. Sharing the function is the point; sharing a struct with a hash\n * field `stats` would never read is not.\n */\n\n/** `smelt stats` — the store's counters, read without touching them. */\nexport interface StatsInvocation {\n readonly mode: 'stats';\n readonly json: boolean;\n}\n\n/** What `stats` runs on: the shared store leg, plus how to print it. */\nexport interface ResolvedStatsRun {\n readonly store: ResolvedStoreRun;\n readonly json: boolean;\n}\n\n/**\n * The `smelt stats --json` envelope format. Its own version line for the same reason\n * `smelt map` has one: the two envelopes carry different structures and must move\n * independently.\n */\nexport const CLI_STATS_JSON_FORMAT = 'smelt-stats-cli/v1';\n\n/** What `smelt stats --json` prints: the {@link RetrieveStats} verbatim, versioned. */\nexport interface CliStatsJsonEnvelope {\n readonly format: string;\n /** The {@link RetrieveStats} exactly as the store's `stats()` returned them. */\n readonly stats: RetrieveStats;\n}\n\nexport const statsCommand: Subcommand<StatsInvocation, ResolvedStatsRun> = {\n name: 'stats',\n flags: ['json'],\n refusal: `stats reads counters; there is nothing to budget, focus or plan.`,\n usage: {\n synopsis: ['stats [--json]'],\n section: {\n heading: 'RETRIEVE & STATS',\n body:\n ` ${CLI_NAME} stats prints the same store's counters, one \\`name value\\` per line —\\n` +\n ` elisionsStored, bytesStored, retrieveCalls, uniqueRetrieved, expansionRate,\\n` +\n ` allElisionsRetrieved — and reading them is NOT counted as a retrieval. --json\\n` +\n ` emits the RetrieveStats verbatim in its own versioned envelope.\\n` +\n `\\n` +\n ` Both need somewhere for elisions to outlive the run that made them: a\\n` +\n ` smelt.config.json with a directory store (\\`${CLI_NAME} init\\` writes one). With a\\n` +\n ` memory store — or no config — every run's store dies with its process, so there\\n` +\n ` is nothing to retrieve across runs, and that is a usage error rather than a\\n` +\n ` quiet empty answer.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): StatsInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: stats takes no further arguments, got ` +\n `${positionals.slice(1).join(', ')}. It reports on the one configured store.`,\n );\n }\n return { mode: 'stats', json: values.json === true };\n },\n\n resolve(invocation: StatsInvocation, config: ConfigSource): ResolvedStatsRun {\n return { store: resolveStoreRun('stats', config()), json: invocation.json };\n },\n\n run(resolved: ResolvedStatsRun, io: CliIo): number {\n const store = openStore({ kind: 'directory', path: resolved.store.storePath });\n const stats = readCounters({ store });\n\n if (resolved.json) {\n const statsEnvelope: CliStatsJsonEnvelope = { format: CLI_STATS_JSON_FORMAT, stats };\n io.stdout(`${JSON.stringify(statsEnvelope, null, 2)}\\n`);\n return EXIT.ok;\n }\n\n io.stdout(\n [\n `elisionsStored ${String(stats.elisionsStored)}`,\n `bytesStored ${String(stats.bytesStored)}`,\n `retrieveCalls ${String(stats.retrieveCalls)}`,\n `uniqueRetrieved ${String(stats.uniqueRetrieved)}`,\n `expansionRate ${String(stats.expansionRate)}`,\n `allElisionsRetrieved ${String(stats.allElisionsRetrieved)}`,\n '',\n ].join('\\n'),\n );\n return EXIT.ok;\n },\n};\n"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { LoadedConfig } from '../config.ts';
|
|
2
|
+
import type { CliIo } from '../shell.ts';
|
|
3
|
+
import type { FlagValues, VerbFlag } from './flags.ts';
|
|
4
|
+
/**
|
|
5
|
+
* The verbs `smelt` answers to. `'smelt'` is the default verb — the bare
|
|
6
|
+
* `smelt <file>` / `smelt < input` run — and is the only one with no word on the
|
|
7
|
+
* command line, which is why a file literally named `map` needs `./map`.
|
|
8
|
+
*
|
|
9
|
+
* Order is meaningful: it is the order `--help` renders every derived list in, and the
|
|
10
|
+
* order a refusal names owners in. Keep it stable and append new verbs at the end.
|
|
11
|
+
*/
|
|
12
|
+
export type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks';
|
|
13
|
+
/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */
|
|
14
|
+
export declare const DEFAULT_VERB = "smelt";
|
|
15
|
+
/**
|
|
16
|
+
* The nearest `smelt.config.json`, loaded on demand.
|
|
17
|
+
*
|
|
18
|
+
* A thunk rather than a value, because two verbs must **not** load it: `init` and
|
|
19
|
+
* `hooks` read the config file themselves, with their own tolerance, and a wizard you
|
|
20
|
+
* run to *fix* a malformed config cannot be a wizard that refuses to start because the
|
|
21
|
+
* config is malformed. Loading eagerly in the dispatcher would quietly turn that into
|
|
22
|
+
* a usage error.
|
|
23
|
+
*/
|
|
24
|
+
export type ConfigSource = () => LoadedConfig | undefined;
|
|
25
|
+
/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */
|
|
26
|
+
export interface SubcommandUsage {
|
|
27
|
+
/**
|
|
28
|
+
* The command's everyday forms, rendered in the first block of USAGE in registry
|
|
29
|
+
* order. Each entry is the text *after* the CLI name.
|
|
30
|
+
*/
|
|
31
|
+
readonly synopsis: readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* Forms rendered in the second block, after every command's everyday forms: the
|
|
34
|
+
* round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do
|
|
35
|
+
* occasionally rather than the shape of a normal run. It keeps USAGE reading
|
|
36
|
+
* top-to-bottom by how often you type a thing without anyone arranging the list.
|
|
37
|
+
*/
|
|
38
|
+
readonly occasional?: readonly string[];
|
|
39
|
+
/**
|
|
40
|
+
* The verb's own section of the help — a heading and its body, already indented.
|
|
41
|
+
* Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &
|
|
42
|
+
* STATS, because the loop is one story); their bodies are joined under it in
|
|
43
|
+
* registry order.
|
|
44
|
+
*/
|
|
45
|
+
readonly section?: {
|
|
46
|
+
readonly heading: string;
|
|
47
|
+
readonly body: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One subcommand: everything the CLI knows about one verb, in one place.
|
|
52
|
+
*
|
|
53
|
+
* This is the seam the CLI was missing. A verb used to be a *shape* restated in four
|
|
54
|
+
* modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a
|
|
55
|
+
* `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in
|
|
56
|
+
* `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every
|
|
57
|
+
* flag it did not want. The refusals were the compounding cost: because no verb owned
|
|
58
|
+
* its flags, every verb refused every other verb's flags by hand, so an eleventh flag
|
|
59
|
+
* edited five messages.
|
|
60
|
+
*
|
|
61
|
+
* Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`
|
|
62
|
+
* is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}
|
|
63
|
+
* pattern, so totality is a compile error and every rendered list (USAGE, the sections,
|
|
64
|
+
* the `map only.` prefixes in OPTIONS) is a derived view.
|
|
65
|
+
*
|
|
66
|
+
* The type parameters are the verb's own two shapes, and they never merge:
|
|
67
|
+
*
|
|
68
|
+
* @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so
|
|
69
|
+
* the parse is testable on its own.
|
|
70
|
+
* @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single
|
|
71
|
+
* merge of flags + config + built-ins, each value carrying its provenance. Deliberately
|
|
72
|
+
* one type per verb: `map` has no store, no strategy and no stdin, and a shared struct
|
|
73
|
+
* whose fields are lies for half its users is not a seam, it is a coincidence.
|
|
74
|
+
*/
|
|
75
|
+
export interface Subcommand<I, R> {
|
|
76
|
+
/** The word on the command line — and the registry key. `'smelt'` has no word. */
|
|
77
|
+
readonly name: Verb;
|
|
78
|
+
/**
|
|
79
|
+
* The flags this verb owns. **The whole point.** Every flag outside this list is
|
|
80
|
+
* refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather
|
|
81
|
+
* than by prose the next verb has to write again. A flag silently ignored would be a
|
|
82
|
+
* setting the user believed was in force.
|
|
83
|
+
*/
|
|
84
|
+
readonly flags: readonly VerbFlag[];
|
|
85
|
+
/**
|
|
86
|
+
* What this verb *is*, as one sentence — the tail of the generated refusal, and the
|
|
87
|
+
* only part of it a verb writes. It answers "why not here?", which is the half of
|
|
88
|
+
* the old hand-written messages worth keeping; the offending flag, this verb's name
|
|
89
|
+
* and the verb the flag does belong to are all derived from the registry.
|
|
90
|
+
*/
|
|
91
|
+
readonly refusal: string;
|
|
92
|
+
/** What this verb contributes to `--help`. Rendered, never hand-arranged. */
|
|
93
|
+
readonly usage: SubcommandUsage;
|
|
94
|
+
/**
|
|
95
|
+
* The verb's own validation, over the flags it owns and the positionals it was
|
|
96
|
+
* given (index 0 is the verb word itself, except for the default verb).
|
|
97
|
+
*
|
|
98
|
+
* @throws {CliUsageError} on anything the user got wrong. Never guesses.
|
|
99
|
+
*/
|
|
100
|
+
parse(values: FlagValues, positionals: readonly string[]): I;
|
|
101
|
+
/**
|
|
102
|
+
* The verb's single merge of flags + config + built-ins. Precedence for this verb
|
|
103
|
+
* lives here and nowhere else, so a precedence question is answered by one function
|
|
104
|
+
* instead of by reading two files.
|
|
105
|
+
*
|
|
106
|
+
* @throws {CliUsageError} when a required value has no source — the budget, for the
|
|
107
|
+
* two verbs that need one.
|
|
108
|
+
*/
|
|
109
|
+
resolve(invocation: I, config: ConfigSource): R;
|
|
110
|
+
/** Execute the resolved run straight-line, and return the exit code. */
|
|
111
|
+
run(resolved: R, io: CliIo): number | Promise<number>;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=subcommand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;AAE7E,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAAyB,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,YAAY,GAAG,SAAS,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC;;;;;OAKG;IACH,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7D;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,CAAC;IAChD,wEAAwE;IACxE,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
|