@smeltjs/core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/agents/guide.d.ts +68 -0
- package/dist/agents/guide.d.ts.map +1 -0
- package/dist/agents/guide.js +72 -0
- package/dist/agents/guide.js.map +1 -0
- package/dist/agents/instructions.d.ts +133 -0
- package/dist/agents/instructions.d.ts.map +1 -0
- package/dist/agents/instructions.js +212 -0
- package/dist/agents/instructions.js.map +1 -0
- package/dist/agents/lint.d.ts +154 -0
- package/dist/agents/lint.d.ts.map +1 -0
- package/dist/agents/lint.js +761 -0
- package/dist/agents/lint.js.map +1 -0
- package/dist/agents/split.d.ts +113 -0
- package/dist/agents/split.d.ts.map +1 -0
- package/dist/agents/split.js +234 -0
- package/dist/agents/split.js.map +1 -0
- 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/agents.d.ts +39 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +121 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/args.d.ts +17 -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 +60 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +89 -2
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/hooks.d.ts +55 -51
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +206 -658
- 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 +23 -0
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +142 -4
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/run.d.ts +29 -90
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +34 -342
- 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/agents.d.ts +72 -0
- package/dist/cli/subcommands/agents.d.ts.map +1 -0
- package/dist/cli/subcommands/agents.js +162 -0
- package/dist/cli/subcommands/agents.js.map +1 -0
- package/dist/cli/subcommands/flags.d.ts +144 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -0
- package/dist/cli/subcommands/flags.js +220 -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 +80 -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 +81 -0
- package/dist/cli/subcommands/registry.d.ts.map +1 -0
- package/dist/cli/subcommands/registry.js +141 -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 +92 -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 +60 -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 +47 -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 +72 -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 +209 -0
- package/dist/harness/profile.d.ts.map +1 -0
- package/dist/harness/profile.js +41 -0
- package/dist/harness/profile.js.map +1 -0
- package/dist/harness/registry.d.ts +100 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +144 -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 +43 -76
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -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/text/json-edit.d.ts +71 -0
- package/dist/text/json-edit.d.ts.map +1 -0
- package/dist/text/json-edit.js +219 -0
- package/dist/text/json-edit.js.map +1 -0
- package/dist/types.d.ts +35 -14
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"retrieve.js","sourceRoot":"","sources":["../../../src/cli/subcommands/retrieve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAyD7C,MAAM,CAAC,MAAM,eAAe,GAAwD;IAClF,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,EAAE;IACT,OAAO,EACL,6EAA6E;QAC7E,iDAAiD;IACnD,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,iBAAiB,CAAC;QAC7B,OAAO,EAAE;YACP,OAAO,EAAE,kBAAkB;YAC3B,IAAI,EACF,8EAA8E;gBAC9E,6EAA6E;gBAC7E,KAAK,QAAQ,wEAAwE;gBACrF,mFAAmF;gBACnF,mFAAmF;gBACnF,mFAAmF;gBACnF,sFAAsF;gBACtF,8EAA8E;SACjF;KACF;IAED;;;;;;OAMG;IACH,KAAK,CAAC,OAAmB,EAAE,WAA8B;QACvD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,iEAAiE;gBAC1E,KAAK,QAAQ,4BAA4B,CAC5C,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAE,EAAE,CAAC;IACrD,CAAC;IAED,OAAO,CAAC,UAA8B,EAAE,MAAoB;QAC1D,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;IACjF,CAAC;IAED,GAAG,CAAC,QAA6B,EAAE,EAAS;QAC1C,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;QAC/E,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;CACF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,eAAe,CAC7B,OAA6B,EAC7B,MAAgC;IAEhC,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC/B,MAAM,KAAK,GACT,MAAM,KAAK,SAAS;YAClB,CAAC,CAAC,eAAe,gBAAgB,OAAO;YACxC,CAAC,CAAC,OAAO,gBAAgB,OAAO,MAAM,CAAC,IAAI,sBAAsB,CAAC;QACtE,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,KAAK,OAAO,kCAAkC,KAAK,cAAc;YAC1E,4EAA4E;YAC5E,4EAA4E;YAC5E,uEAAuE;YACvE,GAAG,gBAAgB,QAAQ,QAAQ,qBAAqB,CAC3D,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;AACnC,CAAC","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { openStore } from '../../ops/inputs.ts';\nimport { retrieveBytes } from '../../ops/verbs.ts';\nimport { CONFIG_FILE_NAME, configuredStore } from '../config.ts';\nimport type { LoadedConfig } from '../config.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt retrieve <hash>` — the marker's `retrieve(\"hash\")`, as a real command.\n *\n * The exact original bytes go to stdout and **nothing else does**: no report, no\n * trailing newline the store did not hold, no re-encoding. Trailing-newline fidelity\n * is not pedantry — the retrieved bytes get spliced back into reasoning about the\n * original, and an almost-right blob is the silent wrong answer this project refuses.\n *\n * The retrieval IS counted — that is the point. `store.retrieve()` journals the hit,\n * so an agent working from pure shell moves the same `expansionRate` a tool-calling\n * consumer moves, and over-pruning stays visible whichever loop is in use. Errors are\n * the store's own, verbatim: `UnknownHashError` for a hash never elided,\n * `StoreCorruptionError` for bytes that no longer hash to their name — distinct\n * texts, both exiting {@link EXIT.refused}.\n */\n\n/**\n * `smelt retrieve <hash>` — parsed. A sibling shape, not a field on somebody else's:\n * nothing but the hash, because the command's whole contract is \"hash in, exact bytes\n * out\" — the same contract as the `smelt_retrieve` tool, reachable from a shell.\n */\nexport interface RetrieveInvocation {\n readonly mode: 'retrieve';\n /** The hash exactly as the marker printed it. Validated by the store, not here. */\n readonly hash: string;\n}\n\n/**\n * Everything `smelt retrieve` and `smelt stats` need from the config, fully merged:\n * the persistent store's directory, already resolved against the config file. The\n * third `Resolved*Run`, and the only one two verbs share — because they share the\n * whole of it: no budget, no strategy, no file leg at all, only the store leg, which\n * is config-only. `stats` resolves through {@link resolveStoreRun} rather than\n * restating it.\n */\nexport interface ResolvedStoreRun {\n /** Absolute path of the directory store, resolved against the config file. */\n readonly storePath: string;\n}\n\n/**\n * What `retrieve` runs on: the shared store leg, plus the hash this invocation named.\n * A composition rather than a wider {@link ResolvedStoreRun}, because `stats` shares\n * the store leg and has no hash — a field that is meaningless for half its users is\n * how a shared struct starts lying.\n */\nexport interface ResolvedRetrieveRun {\n readonly store: ResolvedStoreRun;\n readonly hash: string;\n}\n\nexport const retrieveCommand: Subcommand<RetrieveInvocation, ResolvedRetrieveRun> = {\n name: 'retrieve',\n flags: [],\n refusal:\n `retrieve prints the exact original bytes for one hash, nothing else — even ` +\n `--json would wrap what must come back verbatim.`,\n usage: {\n synopsis: ['retrieve <hash>'],\n section: {\n heading: 'RETRIEVE & STATS',\n body:\n ` Every marker carries the hash of the bytes it replaced — <<smelt/v1: … —\\n` +\n ` retrieve(\"hash\")>> — and the marker's retrieve(\"hash\") is this command:\\n` +\n ` ${CLI_NAME} retrieve <hash> prints the exact original bytes on stdout, byte for\\n` +\n ` byte, nothing else. That closes the loop from pure shell: an agent that got a\\n` +\n ` marker asks for the bytes back with a command instead of a tool call, and the\\n` +\n ` retrieval is counted — asking for material back is exactly what the expansion\\n` +\n ` rate measures. An unknown hash and damaged bytes are distinct refusals (exit 3):\\n` +\n ` \"never elided\" and \"the store was corrupted\" call for different responses.`,\n },\n },\n\n /**\n * The hash and nothing else. Every flag is refused by the registry's one generated\n * message rather than ignored — the command prints the exact original bytes on\n * stdout and nothing more, so a flag that changed the output would break the one\n * contract it has, and a flag silently dropped would be a setting the user believed\n * was in force.\n */\n parse(_values: FlagValues, positionals: readonly string[]): RetrieveInvocation {\n if (positionals.length !== 2) {\n throw new CliUsageError(\n `${CLI_NAME}: retrieve needs exactly one hash — the one a marker printed.\\n` +\n ` ${CLI_NAME} retrieve 84998967370f38bc`,\n );\n }\n return { mode: 'retrieve', hash: positionals[1]! };\n },\n\n resolve(invocation: RetrieveInvocation, config: ConfigSource): ResolvedRetrieveRun {\n return { store: resolveStoreRun('retrieve', config()), hash: invocation.hash };\n },\n\n run(resolved: ResolvedRetrieveRun, io: CliIo): number {\n const store = openStore({ kind: 'directory', path: resolved.store.storePath });\n io.stdout(retrieveBytes({ store, hash: resolved.hash }));\n return EXIT.ok;\n },\n};\n\n/**\n * The store leg alone, for the two commands whose entire job is the store between\n * runs. The refusal is the point: `retrieve` exists so the marker's\n * `retrieve(\"hash\")` works from a later shell — cross-run retrieval — and a memory\n * store dies with the process that filled it, so with a memory store (or no config\n * at all) there is nothing those commands could honestly read. Answering with\n * `UnknownHashError` or all-zero stats instead would be the quiet wrong answer this\n * project refuses everywhere: the hash *was* elided, the counters *did* move — in a\n * store that no longer exists.\n *\n * **This is a policy, not a law, which is why it stays here and is not exported.** The\n * shared half — a config's store decision (`configuredStore`) and opening it\n * (`openStore` in `ops/inputs.ts`) — is what every consumer needs and now what every\n * consumer imports. This function is the CLI's ruling *on top of* that decision, and\n * the MCP server deliberately rules the other way: it accepts a memory store, serves\n * the whole session from it, and says how to get persistence at the moment an unknown\n * hash makes the difference visible. Exporting this would offer that server the CLI's\n * refusal wearing the name of a shared law — the fork this seam exists to end, running\n * in the other direction.\n *\n * @throws {CliUsageError} when no config exists, or the configured store is memory.\n */\nexport function resolveStoreRun(\n command: 'retrieve' | 'stats',\n config: LoadedConfig | undefined,\n): ResolvedStoreRun {\n const store = configuredStore(config);\n if (store.kind !== 'directory') {\n const state =\n config === undefined\n ? `there is no ${CONFIG_FILE_NAME} here`\n : `the ${CONFIG_FILE_NAME} at ${config.path} uses a memory store`;\n throw new CliUsageError(\n `${CLI_NAME}: ${command} needs a persistent store, and ${state}. Cross-run ` +\n `retrieval is the point of the store: a memory store dies with the process ` +\n `that made it, so a marker's hash from an earlier run names bytes this run ` +\n `never held. Configure {\"store\": {\"kind\": \"directory\", \"path\": …}} in ` +\n `${CONFIG_FILE_NAME} — \\`${CLI_NAME} init\\` writes one.`,\n );\n }\n return { storePath: store.path };\n}\n"]}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Strategy } from '../../plan/planners.ts';
|
|
2
|
+
import type { DetectedLanguage, SmeltResult } from '../../types.ts';
|
|
3
|
+
import type { ConfiguredStore, LoadedConfig } from '../config.ts';
|
|
4
|
+
import type { Subcommand } from './subcommand.ts';
|
|
5
|
+
/**
|
|
6
|
+
* The default verb: `smelt <file>` and `smelt < input`, plus the `--reconstruct` round
|
|
7
|
+
* trip that reads one of its own `--json` envelopes back.
|
|
8
|
+
*
|
|
9
|
+
* Two jobs, one verb, which is why USAGE lists it twice: `--reconstruct` reads a file
|
|
10
|
+
* this command wrote and puts every byte back, so it takes no budget, no focus and no
|
|
11
|
+
* strategy — but it is the same front door, and giving it a subcommand word would
|
|
12
|
+
* break every script that types `smelt --reconstruct`.
|
|
13
|
+
*/
|
|
14
|
+
/** What the CLI was asked to do. Pure data, so the parse is testable on its own. */
|
|
15
|
+
export interface SmeltInvocation {
|
|
16
|
+
readonly mode: 'smelt' | 'reconstruct' | 'help' | 'version' | 'init';
|
|
17
|
+
/** Path to read. `undefined` means stdin. */
|
|
18
|
+
readonly file?: string;
|
|
19
|
+
/**
|
|
20
|
+
* UTF-8 bytes. `undefined` in `'smelt'` mode means the flag was not given; the
|
|
21
|
+
* runner then consults `smelt.config.json` and errors if that has no default either.
|
|
22
|
+
*/
|
|
23
|
+
readonly budgetBytes?: number;
|
|
24
|
+
readonly focus: readonly string[];
|
|
25
|
+
readonly language?: DetectedLanguage;
|
|
26
|
+
/** `undefined` means the flag was not given — the config default may apply. */
|
|
27
|
+
readonly strategy?: Strategy;
|
|
28
|
+
readonly json: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Everything one smelt run needs, fully merged — with a receipt for where each
|
|
32
|
+
* merged value came from.
|
|
33
|
+
*
|
|
34
|
+
* This is the verb's single merge of flags + config + built-ins. Precedence lives
|
|
35
|
+
* here and nowhere else: `resolveRun` is the only code that may look at a flag and a
|
|
36
|
+
* config default side by side, so a precedence question is always answered by one
|
|
37
|
+
* function instead of by reading two files. `runSmelt` executes this object
|
|
38
|
+
* straight-line, without a `??` of its own.
|
|
39
|
+
*/
|
|
40
|
+
export interface ResolvedRun {
|
|
41
|
+
readonly budgetBytes: number;
|
|
42
|
+
/** Where the budget came from. A missing budget never gets here — it throws. */
|
|
43
|
+
readonly budgetSource: 'flag' | 'config';
|
|
44
|
+
readonly strategy: Strategy;
|
|
45
|
+
readonly strategySource: 'flag' | 'config' | 'builtin';
|
|
46
|
+
/**
|
|
47
|
+
* The store decision, with `path` already resolved against the config file's
|
|
48
|
+
* directory. `'memory'` is the built-in default — a fresh in-memory store per run.
|
|
49
|
+
*/
|
|
50
|
+
readonly store: ConfiguredStore;
|
|
51
|
+
/** Path to read. `undefined` means stdin. Flags only; the config has no say. */
|
|
52
|
+
readonly file?: string;
|
|
53
|
+
readonly focus: readonly string[];
|
|
54
|
+
readonly language?: DetectedLanguage;
|
|
55
|
+
readonly json: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The default verb's two resolved shapes.
|
|
59
|
+
*
|
|
60
|
+
* `--reconstruct` merges nothing: there is no budget to default, no strategy to pick
|
|
61
|
+
* and no store to choose, because the envelope carries its own bytes. Saying that in
|
|
62
|
+
* the type is cheaper than a {@link ResolvedRun} whose every field would be a lie for
|
|
63
|
+
* half of this verb's runs.
|
|
64
|
+
*/
|
|
65
|
+
export type ResolvedSmeltCommand = {
|
|
66
|
+
readonly kind: 'smelt';
|
|
67
|
+
readonly run: ResolvedRun;
|
|
68
|
+
} | {
|
|
69
|
+
readonly kind: 'reconstruct';
|
|
70
|
+
readonly file?: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* The `--json` envelope format, versioned like the marker for the same reason: this is
|
|
74
|
+
* a surface other programs parse, so a change to it has to be identifiable rather than
|
|
75
|
+
* silent.
|
|
76
|
+
*/
|
|
77
|
+
export declare const CLI_JSON_FORMAT = "smelt-cli/v1";
|
|
78
|
+
/**
|
|
79
|
+
* What `--json` prints, and what `--reconstruct` reads back.
|
|
80
|
+
*
|
|
81
|
+
* `result` is the {@link SmeltResult} **verbatim** — nothing renamed, nothing dropped —
|
|
82
|
+
* so it can be diffed in a test. `elided` is the other half, and it is here because a
|
|
83
|
+
* result on its own is *not* reconstructible: Law 3 is a property of the result plus
|
|
84
|
+
* the store that holds its bytes, and a file claiming to prove the round trip while
|
|
85
|
+
* carrying only half of it would prove nothing.
|
|
86
|
+
*/
|
|
87
|
+
export interface CliJsonEnvelope {
|
|
88
|
+
readonly format: string;
|
|
89
|
+
readonly result: SmeltResult;
|
|
90
|
+
/** hash → the exact elided bytes. Keys match `result.elisions[].hash`. */
|
|
91
|
+
readonly elided: Readonly<Record<string, string>>;
|
|
92
|
+
}
|
|
93
|
+
export declare const smeltCommand: Subcommand<SmeltInvocation, ResolvedSmeltCommand>;
|
|
94
|
+
/**
|
|
95
|
+
* Merge one `'smelt'`-mode invocation with the loaded config (or `undefined` when no
|
|
96
|
+
* `smelt.config.json` exists) and the built-in defaults.
|
|
97
|
+
*
|
|
98
|
+
* The precedence is strict and one-directional: an explicit flag always wins over the
|
|
99
|
+
* config, and the config only fills what the flags left unsaid. Built-ins fill last,
|
|
100
|
+
* and only where a built-in exists at all — the budget deliberately has none, so a run
|
|
101
|
+
* with no budget from either source is refused here, in the one function that owns that
|
|
102
|
+
* error.
|
|
103
|
+
*
|
|
104
|
+
* @throws {CliUsageError} when neither `--budget` nor the config names a budget.
|
|
105
|
+
*/
|
|
106
|
+
export declare function resolveRun(invocation: SmeltInvocation, config: LoadedConfig | undefined): ResolvedRun;
|
|
107
|
+
//# sourceMappingURL=smelt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smelt.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/smelt.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,KAAK,EAAE,gBAAgB,EAAgB,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElF,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAOlE,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;GAQG;AAEH,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACrE,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACvD;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D;;;;GAIG;AACH,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,oBAAoB,CA8D1E,CAAC;AAqDF;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,YAAY,GAAG,SAAS,GAC/B,WAAW,CA6Bb","sourcesContent":["import { reconstruct } from '../../apply.ts';\nimport { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError, SmeltError } from '../../errors.ts';\nimport { budgetRequired, openStore, readBlob, resolveStrategy } from '../../ops/inputs.ts';\nimport { smeltBlob } from '../../ops/verbs.ts';\nimport { isStrategy, STRATEGIES } from '../../plan/planners.ts';\nimport type { Strategy } from '../../plan/planners.ts';\nimport { MemoryElisionStore } from '../../store.ts';\nimport type { DetectedLanguage, ElisionStore, SmeltResult } from '../../types.ts';\nimport { CONFIG_FILE_NAME, configuredStore } from '../config.ts';\nimport type { ConfiguredStore, LoadedConfig } from '../config.ts';\nimport { formatReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport { flagList, parseBudget, VERB_FLAGS } from './flags.ts';\nimport type { FlagValues, VerbFlag } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * The default verb: `smelt <file>` and `smelt < input`, plus the `--reconstruct` round\n * trip that reads one of its own `--json` envelopes back.\n *\n * Two jobs, one verb, which is why USAGE lists it twice: `--reconstruct` reads a file\n * this command wrote and puts every byte back, so it takes no budget, no focus and no\n * strategy — but it is the same front door, and giving it a subcommand word would\n * break every script that types `smelt --reconstruct`.\n */\n\n/** What the CLI was asked to do. Pure data, so the parse is testable on its own. */\nexport interface SmeltInvocation {\n readonly mode: 'smelt' | 'reconstruct' | 'help' | 'version' | 'init';\n /** Path to read. `undefined` means stdin. */\n readonly file?: string;\n /**\n * UTF-8 bytes. `undefined` in `'smelt'` mode means the flag was not given; the\n * runner then consults `smelt.config.json` and errors if that has no default either.\n */\n readonly budgetBytes?: number;\n readonly focus: readonly string[];\n readonly language?: DetectedLanguage;\n /** `undefined` means the flag was not given — the config default may apply. */\n readonly strategy?: Strategy;\n readonly json: boolean;\n}\n\n/**\n * Everything one smelt run needs, fully merged — with a receipt for where each\n * merged value came from.\n *\n * This is the verb's single merge of flags + config + built-ins. Precedence lives\n * here and nowhere else: `resolveRun` is the only code that may look at a flag and a\n * config default side by side, so a precedence question is always answered by one\n * function instead of by reading two files. `runSmelt` executes this object\n * straight-line, without a `??` of its own.\n */\nexport interface ResolvedRun {\n readonly budgetBytes: number;\n /** Where the budget came from. A missing budget never gets here — it throws. */\n readonly budgetSource: 'flag' | 'config';\n readonly strategy: Strategy;\n readonly strategySource: 'flag' | 'config' | 'builtin';\n /**\n * The store decision, with `path` already resolved against the config file's\n * directory. `'memory'` is the built-in default — a fresh in-memory store per run.\n */\n readonly store: ConfiguredStore;\n /** Path to read. `undefined` means stdin. Flags only; the config has no say. */\n readonly file?: string;\n readonly focus: readonly string[];\n readonly language?: DetectedLanguage;\n readonly json: boolean;\n}\n\n/**\n * The default verb's two resolved shapes.\n *\n * `--reconstruct` merges nothing: there is no budget to default, no strategy to pick\n * and no store to choose, because the envelope carries its own bytes. Saying that in\n * the type is cheaper than a {@link ResolvedRun} whose every field would be a lie for\n * half of this verb's runs.\n */\nexport type ResolvedSmeltCommand =\n | { readonly kind: 'smelt'; readonly run: ResolvedRun }\n | { readonly kind: 'reconstruct'; readonly file?: string };\n\n/**\n * The `--json` envelope format, versioned like the marker for the same reason: this is\n * a surface other programs parse, so a change to it has to be identifiable rather than\n * silent.\n */\nexport const CLI_JSON_FORMAT = 'smelt-cli/v1';\n\n/**\n * What `--json` prints, and what `--reconstruct` reads back.\n *\n * `result` is the {@link SmeltResult} **verbatim** — nothing renamed, nothing dropped —\n * so it can be diffed in a test. `elided` is the other half, and it is here because a\n * result on its own is *not* reconstructible: Law 3 is a property of the result plus\n * the store that holds its bytes, and a file claiming to prove the round trip while\n * carrying only half of it would prove nothing.\n */\nexport interface CliJsonEnvelope {\n readonly format: string;\n readonly result: SmeltResult;\n /** hash → the exact elided bytes. Keys match `result.elisions[].hash`. */\n readonly elided: Readonly<Record<string, string>>;\n}\n\nexport const smeltCommand: Subcommand<SmeltInvocation, ResolvedSmeltCommand> = {\n name: 'smelt',\n flags: ['budget', 'focus', 'language', 'strategy', 'json', 'reconstruct'],\n refusal:\n `A single-blob run reads one file or stdin; there is no tree to walk, ` +\n `nothing to cache, and no harness to install into.`,\n usage: {\n synopsis: [\n '<file> --budget <bytes> [--focus <term>]...',\n '--budget <bytes> [--focus <term>]... < input',\n ],\n occasional: ['--reconstruct <result.json>', '--reconstruct < result.json'],\n },\n\n parse(values: FlagValues, positionals: readonly string[]): SmeltInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: expected at most one file, got ${String(positionals.length)} ` +\n `(${positionals.join(', ')}). smelt reads one blob at a time.`,\n );\n }\n const file = positionals[0];\n\n if (values.reconstruct === true) {\n refuseReconstructFlags(values);\n return {\n mode: 'reconstruct',\n ...(file === undefined ? {} : { file }),\n focus: [],\n json: false,\n };\n }\n\n const budgetBytes = parseBudget(values.budget);\n const chosenStrategy = parseStrategy(values.strategy);\n return {\n mode: 'smelt',\n ...(file === undefined ? {} : { file }),\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n focus: values.focus ?? [],\n ...(values.language === undefined ? {} : { language: parseLanguage(values.language) }),\n ...(chosenStrategy === undefined ? {} : { strategy: chosenStrategy }),\n json: values.json === true,\n };\n },\n\n resolve(invocation: SmeltInvocation, config: ConfigSource): ResolvedSmeltCommand {\n if (invocation.mode === 'reconstruct') {\n return {\n kind: 'reconstruct',\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n };\n }\n return { kind: 'smelt', run: resolveRun(invocation, config()) };\n },\n\n run(resolved: ResolvedSmeltCommand, io: CliIo): number | Promise<number> {\n if (resolved.kind === 'reconstruct') {\n return runReconstruct(readInput(resolved.file, io), io);\n }\n return runSmelt(resolved.run, io);\n },\n};\n\n/**\n * Every flag this verb owns that `--reconstruct` cannot honour, with the reason each\n * one makes no sense — the second job's flag ownership, which the registry cannot\n * express because both jobs are the same verb.\n *\n * `refuseForeignFlags` refuses a flag no verb owns *here*; it cannot refuse `--focus`\n * on a single-blob run, because a single-blob run is exactly where `--focus` belongs.\n * So the ones the round trip ignores are named here instead, and named exhaustively:\n * every flag on `smeltCommand.flags` except `--reconstruct` itself has an entry, which\n * `test/guards/subcommand-registry.test.ts` crosses. A flag added to this verb and\n * forgotten here would be silently ignored by half of the verb's runs — the failure\n * this table exists to make impossible.\n *\n * Key order is the flag table's order, so a refusal lists flags the way every other\n * refusal does.\n */\nconst RECONSTRUCT_REFUSALS = {\n budget: `Reconstruction puts every byte back; there is nothing to fit.`,\n focus:\n `Focus decides what survives a cut, and the cut has already been made — ` +\n `the envelope names every elision it took.`,\n language:\n `Nothing is detected or parsed on the way back: the envelope carries the ` +\n `bytes and the ranges the cut recorded.`,\n strategy: `No planner runs on the way back — the elisions come from the envelope.`,\n json:\n `--reconstruct reads a --json envelope and prints the original text; ` +\n `there is no second envelope to write.`,\n} as const satisfies Partial<Record<VerbFlag, string>>;\n\n/**\n * Refuse every flag `--reconstruct` would otherwise ignore, in one message shaped like\n * the ownership refusals: the flags named, then why not here. A {@link CliUsageError},\n * so it exits 2 exactly as every other refusal does.\n *\n * @throws {CliUsageError} naming each offending flag and what the round trip does instead.\n */\nfunction refuseReconstructFlags(values: FlagValues): void {\n const reasons: Partial<Record<VerbFlag, string>> = RECONSTRUCT_REFUSALS;\n const offending = VERB_FLAGS.filter(\n (flag) => reasons[flag] !== undefined && values[flag] !== undefined,\n );\n if (offending.length === 0) return;\n\n const verb = offending.length === 1 ? 'makes' : 'make';\n const why = offending.map((flag) => reasons[flag] ?? '').join(' ');\n throw new CliUsageError(\n `${CLI_NAME}: ${flagList(offending)} ${verb} no sense with --reconstruct. ${why}`,\n );\n}\n\n/**\n * Merge one `'smelt'`-mode invocation with the loaded config (or `undefined` when no\n * `smelt.config.json` exists) and the built-in defaults.\n *\n * The precedence is strict and one-directional: an explicit flag always wins over the\n * config, and the config only fills what the flags left unsaid. Built-ins fill last,\n * and only where a built-in exists at all — the budget deliberately has none, so a run\n * with no budget from either source is refused here, in the one function that owns that\n * error.\n *\n * @throws {CliUsageError} when neither `--budget` nor the config names a budget.\n */\nexport function resolveRun(\n invocation: SmeltInvocation,\n config: LoadedConfig | undefined,\n): ResolvedRun {\n const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;\n if (budgetBytes === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: ` +\n budgetRequired({\n knob: '--budget',\n stake: 'your context to throw away',\n advice:\n `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +\n `(\\`${CLI_NAME} init\\` writes one).\\n` +\n ` ${CLI_NAME} src/server.ts --budget 4000 --focus handleRequest`,\n }),\n );\n }\n\n const strategy = resolveStrategy(invocation.strategy, config?.config.strategy);\n\n return {\n budgetBytes,\n budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',\n strategy: strategy.strategy,\n strategySource: strategy.source,\n store: configuredStore(config),\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n focus: invocation.focus,\n ...(invocation.language === undefined ? {} : { language: invocation.language }),\n json: invocation.json,\n };\n}\n\n/**\n * One smelt run: read the input, hand the resolved values to {@link smeltBlob}, render.\n *\n * All merging — flags versus `smelt.config.json` versus built-ins, including the\n * budget-required refusal — happens in {@link resolveRun}, which is the only place\n * precedence lives. This function reads the resolved object and never consults a flag\n * or a config field directly. The cut itself belongs to no front door: `smeltBlob` in\n * `ops/verbs.ts` builds the smelter and returns the values this function prints, and\n * the `smelt_file` tool calls the same op with its own arguments.\n */\nasync function runSmelt(run: ResolvedRun, io: CliIo): Promise<number> {\n const inputText = readInput(run.file, io);\n\n const outcome = await smeltBlob({\n text: inputText,\n source: run.file ?? '<stdin>',\n budgetBytes: run.budgetBytes,\n strategy: run.strategy,\n store: openStore(run.store),\n ...(run.file === undefined ? {} : { path: run.file }),\n ...(run.language === undefined ? {} : { language: run.language }),\n focus: run.focus,\n });\n\n if (run.json) {\n io.stdout(`${JSON.stringify(envelope(outcome.result, outcome.store), null, 2)}\\n`);\n } else {\n io.stdout(outcome.result.text);\n }\n io.stderr(formatReport(outcome));\n\n return outcome.result.outputBytes > run.budgetBytes ? EXIT.overBudget : EXIT.ok;\n}\n\n/**\n * Law 3, from a shell.\n *\n * This is deliberately not \"print the text and hope\": it rebuilds the store from the\n * envelope, checks every hash against the bytes it claims to key, and checks the\n * reconstructed length against the `inputBytes` the result recorded at the time of the\n * cut. A round trip that quietly returns almost-right text is the failure this whole\n * repository is arranged against.\n */\nfunction runReconstruct(text: string, io: CliIo): number {\n const { result, elided } = parseEnvelope(text);\n const store = new MemoryElisionStore();\n\n for (const [hash, content] of Object.entries(elided)) {\n const actual = store.put(content);\n if (actual !== hash) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is not self-consistent — it stores bytes under ` +\n `\"${hash}\" that actually hash to \"${actual}\". Refusing to reconstruct from it.`,\n );\n }\n }\n for (const elision of result.elisions) {\n if (!store.has(elision.hash)) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is missing the bytes for \"${elision.hash}\". A result ` +\n `without its elided bytes cannot be reconstructed; re-run with --json.`,\n );\n }\n }\n\n const original = reconstruct(result, store);\n const bytes = Buffer.byteLength(original, 'utf8');\n if (bytes !== result.inputBytes) {\n throw new SmeltError(\n `${CLI_NAME}: reconstruction produced ${String(bytes)} bytes but the result ` +\n `recorded ${String(result.inputBytes)}. The round trip did not close.`,\n );\n }\n\n io.stdout(original);\n io.stderr(\n `${CLI_NAME} reconstructed ${String(result.inputBytes)} B from ` +\n `${String(result.elisions.length)} elisions — byte for byte\\n`,\n );\n return EXIT.ok;\n}\n\n/** `peek`, not `retrieve`: writing a file is not the model asking for anything back. */\nfunction envelope(result: SmeltResult, store: ElisionStore): CliJsonEnvelope {\n const elided: Record<string, string> = {};\n for (const elision of result.elisions) {\n const content = store.peek(elision.hash);\n if (content === undefined) {\n throw new SmeltError(\n `${CLI_NAME}: the store does not hold \"${elision.hash}\", which its own result ` +\n `says it elided. Refusing to write an envelope that cannot round-trip.`,\n );\n }\n elided[elision.hash] = content;\n }\n return { format: CLI_JSON_FORMAT, result, elided };\n}\n\nfunction parseEnvelope(text: string): CliJsonEnvelope {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (cause) {\n throw new CliUsageError(\n `${CLI_NAME}: --reconstruct expected a --json envelope, and this is not JSON: ` +\n `${cause instanceof Error ? cause.message : String(cause)}`,\n );\n }\n\n if (typeof value !== 'object' || value === null) {\n throw new CliUsageError(`${CLI_NAME}: --reconstruct expected a JSON object.`);\n }\n const fields = value as Record<string, unknown>;\n\n if (fields['format'] !== CLI_JSON_FORMAT) {\n throw new CliUsageError(\n `${CLI_NAME}: this envelope says format \"${String(fields['format'])}\"; this build ` +\n `reads \"${CLI_JSON_FORMAT}\". Formats are versioned so a mismatch is visible ` +\n `instead of being half-understood.`,\n );\n }\n\n const result = fields['result'];\n if (typeof result !== 'object' || result === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`result\\` object.`);\n }\n const resultFields = result as Record<string, unknown>;\n if (\n typeof resultFields['text'] !== 'string' ||\n typeof resultFields['inputBytes'] !== 'number' ||\n !Array.isArray(resultFields['elisions'])\n ) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope's \\`result\\` is missing text, inputBytes or elisions.`,\n );\n }\n\n const elided = fields['elided'];\n if (typeof elided !== 'object' || elided === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`elided\\` map.`);\n }\n for (const [hash, content] of Object.entries(elided)) {\n if (typeof content !== 'string') {\n throw new CliUsageError(`${CLI_NAME}: envelope's \\`elided[\"${hash}\"]\\` is not a string.`);\n }\n }\n\n return {\n format: CLI_JSON_FORMAT,\n result: result as unknown as SmeltResult,\n elided: elided as Record<string, string>,\n };\n}\n\n/**\n * The blob to work on: stdin when no file was named, otherwise the file — read\n * through the ops law, so \"cannot read X\" says the same thing here and in the\n * `smelt_file` tool. Only the stdin leg is the CLI's own; a tool has no stdin.\n */\nfunction readInput(file: string | undefined, io: CliIo): string {\n if (file === undefined) return io.stdin();\n const read = readBlob(file, file);\n if (!read.ok) throw new CliUsageError(`${CLI_NAME}: ${read.refusal}`);\n return read.value;\n}\n\nfunction parseLanguage(raw: string): DetectedLanguage {\n const known: readonly string[] = [...SUPPORTED_LANGUAGES, 'unknown'];\n if (!known.includes(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --language \"${raw}\". Known: ${known.join(', ')}.`,\n );\n }\n return raw as DetectedLanguage;\n}\n\n/** Membership in the {@link PLANNERS} registry is the whole validation. */\nfunction parseStrategy(raw: string | undefined): Strategy | undefined {\n if (raw === undefined) return undefined;\n if (!isStrategy(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --strategy \"${raw}\". Known: ${STRATEGIES.join(', ')}.`,\n );\n }\n return raw;\n}\n"]}
|
|
@@ -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":"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","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"]}
|