@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,142 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { DEFAULT_STRATEGY } from '../plan/planners.js';
|
|
3
|
+
import { MemoryElisionStore } from '../store.js';
|
|
4
|
+
import { DirectoryElisionStore } from '../store-dir.js';
|
|
5
|
+
/**
|
|
6
|
+
* Law: **a budget is required, and there is no default.**
|
|
7
|
+
*
|
|
8
|
+
* The reasoning is the whole point of the sentence, so it is stated once here rather
|
|
9
|
+
* than paraphrased per surface: a budget smelt invented would silently decide how much
|
|
10
|
+
* of the caller's context to throw away, which is a number nobody measured making a
|
|
11
|
+
* decision nobody made.
|
|
12
|
+
*/
|
|
13
|
+
export function budgetRequired(naming) {
|
|
14
|
+
return (`${naming.knob} is required, in UTF-8 bytes. There is no default, because a budget ` +
|
|
15
|
+
`smelt invented would silently decide how much of ${naming.stake}.` +
|
|
16
|
+
(naming.advice === undefined ? '' : ` ${naming.advice}`));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Law: **a budget is a whole number of UTF-8 bytes greater than zero.**
|
|
20
|
+
*
|
|
21
|
+
* The numeric half only. Getting a candidate *number* out of a surface is that
|
|
22
|
+
* surface's own lexing and stays there: argv carries strings (`--budget 4kb` is a
|
|
23
|
+
* malformed number, and `-1` never reaches here because a leading `-` is not a
|
|
24
|
+
* budget at all), while a JSON tool argument carries whatever type the model sent.
|
|
25
|
+
* Both then ask this function the same question about the same rule.
|
|
26
|
+
*/
|
|
27
|
+
export function budgetFault(value) {
|
|
28
|
+
if (!Number.isInteger(value))
|
|
29
|
+
return 'not-an-integer';
|
|
30
|
+
if (value <= 0)
|
|
31
|
+
return 'not-positive';
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The sentence for a {@link BudgetFault}, naming the value it rejected.
|
|
36
|
+
*
|
|
37
|
+
* `got` is rendered with `JSON.stringify`, which is what both front doors already
|
|
38
|
+
* printed: a CLI passes the raw argv word and gets it back quoted (`"4kb"`), a tool
|
|
39
|
+
* passes the raw JSON value and gets numbers bare (`0`) and strings quoted. One
|
|
40
|
+
* renderer, because a value echoed back in a different shape than it was written is a
|
|
41
|
+
* value the author has to translate before they can see their own typo.
|
|
42
|
+
*/
|
|
43
|
+
export function budgetMalformed(fault, knob, got) {
|
|
44
|
+
return fault === 'not-an-integer'
|
|
45
|
+
? `${knob} must be a whole number of bytes, got ${JSON.stringify(got)}.`
|
|
46
|
+
: `${knob} must be greater than zero, got ${JSON.stringify(got)}.`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The strategy a run falls back to when neither the caller nor a config names one.
|
|
50
|
+
*
|
|
51
|
+
* Re-exported rather than restated: {@link DEFAULT_STRATEGY} is the planner registry's
|
|
52
|
+
* own fact, and a second spelling here would be the fork this seam exists to close.
|
|
53
|
+
*/
|
|
54
|
+
export { DEFAULT_STRATEGY } from '../plan/planners.js';
|
|
55
|
+
/**
|
|
56
|
+
* Law: **an explicit strategy wins over a configured one, and `lexical` fills last.**
|
|
57
|
+
*
|
|
58
|
+
* Both front doors had this precedence written out, and both spelled the built-in
|
|
59
|
+
* `'lexical'` inline — so a sixth planner promoted to the default would have been a
|
|
60
|
+
* two-package edit with no compiler and no test to notice half of it. `chosen` is
|
|
61
|
+
* whatever the caller said explicitly (a `--strategy` flag, a `"strategy"` argument);
|
|
62
|
+
* `configured` is what the nearest `smelt.config.json` says. Validating the name is
|
|
63
|
+
* *not* part of this law — membership in the `PLANNERS` registry (`isStrategy`) is
|
|
64
|
+
* that, already shared, and the sentence each surface refuses an unknown name with is
|
|
65
|
+
* its own register: a flag and a JSON argument do not read alike.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveStrategy(chosen, configured) {
|
|
68
|
+
if (chosen !== undefined)
|
|
69
|
+
return { strategy: chosen, source: 'flag' };
|
|
70
|
+
if (configured !== undefined)
|
|
71
|
+
return { strategy: configured, source: 'config' };
|
|
72
|
+
return { strategy: DEFAULT_STRATEGY, source: 'builtin' };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Law: **read a path, or fail naming it.**
|
|
76
|
+
*
|
|
77
|
+
* `shownAs` is the path as its author wrote it, which is not always the path opened:
|
|
78
|
+
* a tool resolves a relative argument against the server's working directory, and
|
|
79
|
+
* echoing back the absolute result would answer a question nobody asked. The cause is
|
|
80
|
+
* carried through verbatim — an `EACCES` and an `ENOENT` call for different
|
|
81
|
+
* responses, and flattening them to "could not read" throws that away.
|
|
82
|
+
*/
|
|
83
|
+
export function readBlob(fullPath, shownAs) {
|
|
84
|
+
try {
|
|
85
|
+
return { ok: true, value: readFileSync(fullPath, 'utf8') };
|
|
86
|
+
}
|
|
87
|
+
catch (cause) {
|
|
88
|
+
return { ok: false, refusal: `cannot read "${shownAs}": ${describe(cause)}` };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Law: **a tree reader reads a directory, and says so when handed a file.**
|
|
93
|
+
*
|
|
94
|
+
* Two distinct refusals, kept distinct: a path that cannot be statted at all
|
|
95
|
+
* (misspelled, unreadable) and a path that is a perfectly good *file*. The second is
|
|
96
|
+
* the interesting one — it is a caller who wanted the other verb, so the refusal
|
|
97
|
+
* names the other verb instead of leaving them to guess which of their two options
|
|
98
|
+
* was wrong.
|
|
99
|
+
*/
|
|
100
|
+
export function readTree(fullPath, shownAs, naming) {
|
|
101
|
+
let isDirectory;
|
|
102
|
+
try {
|
|
103
|
+
isDirectory = statSync(fullPath).isDirectory();
|
|
104
|
+
}
|
|
105
|
+
catch (cause) {
|
|
106
|
+
return { ok: false, refusal: `cannot read directory "${shownAs}": ${describe(cause)}` };
|
|
107
|
+
}
|
|
108
|
+
if (!isDirectory) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
refusal: `"${shownAs}" is not a directory. ${naming.tree} reads a whole tree; for one ` +
|
|
112
|
+
`file, use ${naming.file}.`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { ok: true, value: fullPath };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Law: **a store decision, opened.**
|
|
119
|
+
*
|
|
120
|
+
* The *decision* — which kind, and where — is `configuredStore()` in `cli/config.ts`,
|
|
121
|
+
* one reading of one config key. This is the other half: turning that decision into a
|
|
122
|
+
* live {@link ElisionStore}. It was the missing half. The MCP package needed exactly
|
|
123
|
+
* this and could not import it, so it re-derived the decision *and* the construction
|
|
124
|
+
* from the config keys directly, and a second reading of `store.kind` came into
|
|
125
|
+
* existence in another package.
|
|
126
|
+
*
|
|
127
|
+
* What each front door does with a memory store stays the front door's: `smelt
|
|
128
|
+
* retrieve` and `smelt stats` refuse one (a marker's hash from an earlier run names
|
|
129
|
+
* bytes no memory store ever held), while the MCP server accepts one and says so at
|
|
130
|
+
* the moment it bites. That divergence is deliberate and documented; it is a policy
|
|
131
|
+
* about a store, not a fact about opening one.
|
|
132
|
+
*/
|
|
133
|
+
export function openStore(decision) {
|
|
134
|
+
return decision.kind === 'memory'
|
|
135
|
+
? new MemoryElisionStore()
|
|
136
|
+
: new DirectoryElisionStore(decision.path);
|
|
137
|
+
}
|
|
138
|
+
/** A thrown cause as one line, without pretending an unknown throw was an Error. */
|
|
139
|
+
function describe(cause) {
|
|
140
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=inputs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inputs.js","sourceRoot":"","sources":["../../src/ops/inputs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAiExD;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,MAAoB;IACjD,OAAO,CACL,GAAG,MAAM,CAAC,IAAI,sEAAsE;QACpF,oDAAoD,MAAM,CAAC,KAAK,GAAG;QACnE,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CACzD,CAAC;AACJ,CAAC;AAKD;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;QAAE,OAAO,gBAAgB,CAAC;IACtD,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,cAAc,CAAC;IACtC,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,KAAkB,EAAE,IAAY,EAAE,GAAY;IAC5E,OAAO,KAAK,KAAK,gBAAgB;QAC/B,CAAC,CAAC,GAAG,IAAI,yCAAyC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG;QACxE,CAAC,CAAC,GAAG,IAAI,mCAAmC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC;AACvE,CAAC;AAED;;;;;GAKG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAWvD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAC7B,MAA4B,EAC5B,UAAgC;IAEhC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACtE,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAChF,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAC3D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,OAAe;IACxD,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;IAC7D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,gBAAgB,OAAO,MAAM,QAAQ,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;IAChF,CAAC;AACH,CAAC;AAUD;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,OAAe,EAAE,MAAkB;IAC5E,IAAI,WAAoB,CAAC;IACzB,IAAI,CAAC;QACH,WAAW,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,0BAA0B,OAAO,MAAM,QAAQ,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;IAC1F,CAAC;IACD,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EACL,IAAI,OAAO,yBAAyB,MAAM,CAAC,IAAI,+BAA+B;gBAC9E,aAAa,MAAM,CAAC,IAAI,GAAG;SAC9B,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,SAAS,CAAC,QAAyB;IACjD,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ;QAC/B,CAAC,CAAC,IAAI,kBAAkB,EAAE;QAC1B,CAAC,CAAC,IAAI,qBAAqB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED,oFAAoF;AACpF,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC","sourcesContent":["import { readFileSync, statSync } from 'node:fs';\n\nimport type { ConfiguredStore } from '../cli/config.ts';\nimport { DEFAULT_STRATEGY } from '../plan/planners.ts';\nimport type { Strategy } from '../plan/planners.ts';\nimport { MemoryElisionStore } from '../store.ts';\nimport { DirectoryElisionStore } from '../store-dir.ts';\nimport type { ElisionStore } from '../types.ts';\n\n/**\n * The laws an operation's input must satisfy — stated once, for every front door.\n *\n * Each rule here was, until this seam existed, written twice: once in a\n * `cli/subcommands/*` verb and once in the MCP server's tool handlers. Two copies of a\n * law is two laws, and the copies had already begun to differ — the `smelt` CLI and\n * the `smelt_file` tool refuse a missing budget with the same reasoning in two\n * separately-maintained sentences, and only one of them was ever edited at a time.\n *\n * **What is shared and what is not.** A law has two halves, and only one of them can\n * be shared honestly:\n *\n * - the **rule and its reasoning** — that a budget is a whole number of bytes\n * greater than zero, that there is no default and why, that a tree-reader refuses\n * a file, that a strategy falls back to `lexical` last. That half lives here.\n * - the **naming** — a CLI spells its budget `--budget` and points at\n * `smelt.config.json`; a tool spells it `\"budgetBytes\"` and points at nothing.\n * That half is the front door's own, and every function here takes it as an\n * argument rather than guessing.\n *\n * Nothing here throws. The two front doors refuse in different currencies — the CLI\n * with a {@link CliUsageError} that exits 2, the MCP server with a tool-level error\n * carrying `isError: true` — and a shared law that threw would force one of them to\n * catch and re-wrap the other's error type, which is how an exit code changes by\n * accident. So a law that can refuse returns a {@link Ruling}: the value, or the one\n * sentence that refuses it, for the caller to throw in its own currency.\n *\n * Nothing here *finds* a `smelt.config.json` either. Config discovery is a CLI\n * concern by design (see `cli/config.ts`); {@link openStore} takes a decision that\n * has already been made and opens it, so no library call's behaviour depends on the\n * directory it was invoked from.\n */\n\n/**\n * The answer to a law that can refuse: the value, or the one sentence refusing it.\n *\n * Deliberately not an exception — see the module comment. The refusal is the\n * complete sentence *minus* whatever prefix the front door puts on its own errors\n * (`smelt: ` for the CLI, the tool name for MCP), because that prefix is naming, and\n * naming belongs to the caller.\n */\nexport type Ruling<T> =\n { readonly ok: true; readonly value: T } | { readonly ok: false; readonly refusal: string };\n\n/** How one front door spells the budget it is refusing, and what it points at next. */\nexport interface BudgetNaming {\n /** The knob, as this surface spells it: `--budget`, `\"budgetBytes\"`. */\n readonly knob: string;\n /**\n * What a budget smelt invented would silently decide — the back half of the\n * no-default sentence. `'your context to throw away'` for a blob run, `'the map to\n * leave out'` for a tree.\n */\n readonly stake: string;\n /**\n * Anything this surface adds after the law: where else the value can come from, and\n * an example. Appended after a single space. The CLI names `defaultBudgetBytes` and\n * `smelt init` here; a tool whose schema already says `required` adds nothing.\n */\n readonly advice?: string;\n}\n\n/**\n * Law: **a budget is required, and there is no default.**\n *\n * The reasoning is the whole point of the sentence, so it is stated once here rather\n * than paraphrased per surface: a budget smelt invented would silently decide how much\n * of the caller's context to throw away, which is a number nobody measured making a\n * decision nobody made.\n */\nexport function budgetRequired(naming: BudgetNaming): string {\n return (\n `${naming.knob} is required, in UTF-8 bytes. There is no default, because a budget ` +\n `smelt invented would silently decide how much of ${naming.stake}.` +\n (naming.advice === undefined ? '' : ` ${naming.advice}`)\n );\n}\n\n/** The two ways a budget that *was* given can still be wrong. See {@link budgetFault}. */\nexport type BudgetFault = 'not-an-integer' | 'not-positive';\n\n/**\n * Law: **a budget is a whole number of UTF-8 bytes greater than zero.**\n *\n * The numeric half only. Getting a candidate *number* out of a surface is that\n * surface's own lexing and stays there: argv carries strings (`--budget 4kb` is a\n * malformed number, and `-1` never reaches here because a leading `-` is not a\n * budget at all), while a JSON tool argument carries whatever type the model sent.\n * Both then ask this function the same question about the same rule.\n */\nexport function budgetFault(value: number): BudgetFault | undefined {\n if (!Number.isInteger(value)) return 'not-an-integer';\n if (value <= 0) return 'not-positive';\n return undefined;\n}\n\n/**\n * The sentence for a {@link BudgetFault}, naming the value it rejected.\n *\n * `got` is rendered with `JSON.stringify`, which is what both front doors already\n * printed: a CLI passes the raw argv word and gets it back quoted (`\"4kb\"`), a tool\n * passes the raw JSON value and gets numbers bare (`0`) and strings quoted. One\n * renderer, because a value echoed back in a different shape than it was written is a\n * value the author has to translate before they can see their own typo.\n */\nexport function budgetMalformed(fault: BudgetFault, knob: string, got: unknown): string {\n return fault === 'not-an-integer'\n ? `${knob} must be a whole number of bytes, got ${JSON.stringify(got)}.`\n : `${knob} must be greater than zero, got ${JSON.stringify(got)}.`;\n}\n\n/**\n * The strategy a run falls back to when neither the caller nor a config names one.\n *\n * Re-exported rather than restated: {@link DEFAULT_STRATEGY} is the planner registry's\n * own fact, and a second spelling here would be the fork this seam exists to close.\n */\nexport { DEFAULT_STRATEGY } from '../plan/planners.ts';\n\n/** Where a resolved strategy came from — the receipt {@link resolveStrategy} returns. */\nexport type StrategySource = 'flag' | 'config' | 'builtin';\n\n/** A strategy, with the provenance that explains it. */\nexport interface ResolvedStrategy {\n readonly strategy: Strategy;\n readonly source: StrategySource;\n}\n\n/**\n * Law: **an explicit strategy wins over a configured one, and `lexical` fills last.**\n *\n * Both front doors had this precedence written out, and both spelled the built-in\n * `'lexical'` inline — so a sixth planner promoted to the default would have been a\n * two-package edit with no compiler and no test to notice half of it. `chosen` is\n * whatever the caller said explicitly (a `--strategy` flag, a `\"strategy\"` argument);\n * `configured` is what the nearest `smelt.config.json` says. Validating the name is\n * *not* part of this law — membership in the `PLANNERS` registry (`isStrategy`) is\n * that, already shared, and the sentence each surface refuses an unknown name with is\n * its own register: a flag and a JSON argument do not read alike.\n */\nexport function resolveStrategy(\n chosen: Strategy | undefined,\n configured: Strategy | undefined,\n): ResolvedStrategy {\n if (chosen !== undefined) return { strategy: chosen, source: 'flag' };\n if (configured !== undefined) return { strategy: configured, source: 'config' };\n return { strategy: DEFAULT_STRATEGY, source: 'builtin' };\n}\n\n/**\n * Law: **read a path, or fail naming it.**\n *\n * `shownAs` is the path as its author wrote it, which is not always the path opened:\n * a tool resolves a relative argument against the server's working directory, and\n * echoing back the absolute result would answer a question nobody asked. The cause is\n * carried through verbatim — an `EACCES` and an `ENOENT` call for different\n * responses, and flattening them to \"could not read\" throws that away.\n */\nexport function readBlob(fullPath: string, shownAs: string): Ruling<string> {\n try {\n return { ok: true, value: readFileSync(fullPath, 'utf8') };\n } catch (cause) {\n return { ok: false, refusal: `cannot read \"${shownAs}\": ${describe(cause)}` };\n }\n}\n\n/** How one front door names the tree verb and its single-file sibling. */\nexport interface TreeNaming {\n /** The whole-tree reader, as this surface spells it: `map`, `repo_map`. */\n readonly tree: string;\n /** Where to go for one file instead: `` `smelt <file>` ``, `smelt_file`. */\n readonly file: string;\n}\n\n/**\n * Law: **a tree reader reads a directory, and says so when handed a file.**\n *\n * Two distinct refusals, kept distinct: a path that cannot be statted at all\n * (misspelled, unreadable) and a path that is a perfectly good *file*. The second is\n * the interesting one — it is a caller who wanted the other verb, so the refusal\n * names the other verb instead of leaving them to guess which of their two options\n * was wrong.\n */\nexport function readTree(fullPath: string, shownAs: string, naming: TreeNaming): Ruling<string> {\n let isDirectory: boolean;\n try {\n isDirectory = statSync(fullPath).isDirectory();\n } catch (cause) {\n return { ok: false, refusal: `cannot read directory \"${shownAs}\": ${describe(cause)}` };\n }\n if (!isDirectory) {\n return {\n ok: false,\n refusal:\n `\"${shownAs}\" is not a directory. ${naming.tree} reads a whole tree; for one ` +\n `file, use ${naming.file}.`,\n };\n }\n return { ok: true, value: fullPath };\n}\n\n/**\n * Law: **a store decision, opened.**\n *\n * The *decision* — which kind, and where — is `configuredStore()` in `cli/config.ts`,\n * one reading of one config key. This is the other half: turning that decision into a\n * live {@link ElisionStore}. It was the missing half. The MCP package needed exactly\n * this and could not import it, so it re-derived the decision *and* the construction\n * from the config keys directly, and a second reading of `store.kind` came into\n * existence in another package.\n *\n * What each front door does with a memory store stays the front door's: `smelt\n * retrieve` and `smelt stats` refuse one (a marker's hash from an earlier run names\n * bytes no memory store ever held), while the MCP server accepts one and says so at\n * the moment it bites. That divergence is deliberate and documented; it is a policy\n * about a store, not a fact about opening one.\n */\nexport function openStore(decision: ConfiguredStore): ElisionStore {\n return decision.kind === 'memory'\n ? new MemoryElisionStore()\n : new DirectoryElisionStore(decision.path);\n}\n\n/** A thrown cause as one line, without pretending an unknown throw was an Error. */\nfunction describe(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n"]}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { RepoMap } from '../repomap/map.ts';
|
|
2
|
+
import type { Strategy } from '../plan/planners.ts';
|
|
3
|
+
import type { DetectedLanguage, ElisionStore, RetrieveStats, SmeltResult } from '../types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* The four verbs, as library functions over already-resolved inputs.
|
|
6
|
+
*
|
|
7
|
+
* This is the seam that sits **below both front doors**. The `smelt` CLI and the
|
|
8
|
+
* `@smeltjs/mcp` server are the same four operations wearing two different coats, and
|
|
9
|
+
* before this file existed each of them owned a full copy of the middle: build a
|
|
10
|
+
* smelter from a strategy and a store, call it, hand the totals to a report; stat a
|
|
11
|
+
* directory, build a map, render its warnings; retrieve one hash; read the counters.
|
|
12
|
+
* The library exported the pieces, so both copies were *correct* — and both had to be
|
|
13
|
+
* edited, in two packages, for one change to how a verb runs.
|
|
14
|
+
*
|
|
15
|
+
* What an op takes is already resolved: a budget that survived its law, a strategy
|
|
16
|
+
* that won its precedence, a store that has been opened, text that has been read. What
|
|
17
|
+
* an op returns is data — the smelted text and the values a report needs, a
|
|
18
|
+
* {@link RepoMap}, bytes, counters. An op never sees argv, never writes to a stream,
|
|
19
|
+
* never returns an exit code, and knows nothing about `CallToolResult`. Those are the
|
|
20
|
+
* adapters' four jobs, and keeping them out here is what makes the same operation
|
|
21
|
+
* testable once instead of twice.
|
|
22
|
+
*
|
|
23
|
+
* The division of labour, in one line: **a front door parses and resolves, an op runs,
|
|
24
|
+
* a front door renders.**
|
|
25
|
+
*/
|
|
26
|
+
/** One blob to smelt, fully resolved. */
|
|
27
|
+
export interface SmeltBlobOp {
|
|
28
|
+
/** The text itself. Reading a file into it is the front door's job (`readBlob`). */
|
|
29
|
+
readonly text: string;
|
|
30
|
+
/** What to call the input in a report: a path, `'<stdin>'`, `'<text>'`. */
|
|
31
|
+
readonly source: string;
|
|
32
|
+
/** UTF-8 bytes. Already past the budget law — see `ops/inputs.ts`. */
|
|
33
|
+
readonly budgetBytes: number;
|
|
34
|
+
/** Already past its precedence — see `resolveStrategy` in `ops/inputs.ts`. */
|
|
35
|
+
readonly strategy: Strategy;
|
|
36
|
+
/**
|
|
37
|
+
* Where elided bytes go. Optional only so a caller with nothing to persist gets the
|
|
38
|
+
* library's own default (a fresh {@link MemoryElisionStore}); a front door that read
|
|
39
|
+
* a config opens the store it decided on and passes it.
|
|
40
|
+
*/
|
|
41
|
+
readonly store?: ElisionStore;
|
|
42
|
+
/** The path the text came from, for language detection. Absent for a pasted blob. */
|
|
43
|
+
readonly path?: string;
|
|
44
|
+
/** Overrides detection entirely, when a surface offers that. */
|
|
45
|
+
readonly language?: DetectedLanguage;
|
|
46
|
+
/** What the task is about. Empty and absent mean the same thing to every planner. */
|
|
47
|
+
readonly focus?: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What one smelt run produced.
|
|
51
|
+
*
|
|
52
|
+
* The fields a report needs are the fields `formatReport` takes, deliberately: an op
|
|
53
|
+
* returns exactly the values the render step reads off, so no front door assembles a
|
|
54
|
+
* report input by hand and no front door counts a byte itself. (`formatReport(outcome)`
|
|
55
|
+
* typechecks as written.) The store comes back too, because the `--json` envelope has
|
|
56
|
+
* to `peek` at the bytes the run just stored, and a caller that passed no store never
|
|
57
|
+
* had a handle on the one the library made.
|
|
58
|
+
*/
|
|
59
|
+
export interface SmeltBlobOutcome {
|
|
60
|
+
readonly result: SmeltResult;
|
|
61
|
+
/** What the input was called. Echoed back so a report never re-derives it. */
|
|
62
|
+
readonly source: string;
|
|
63
|
+
/** The budget the run was given, so a report can say when it was missed. */
|
|
64
|
+
readonly budgetBytes: number;
|
|
65
|
+
/** The exact text that was smelted. */
|
|
66
|
+
readonly inputText: string;
|
|
67
|
+
/** The store the run actually used — the one passed in, or the library's default. */
|
|
68
|
+
readonly store: ElisionStore;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Verb: **cut one blob to a budget.**
|
|
72
|
+
*
|
|
73
|
+
* Every refusal the library can raise passes straight through — a `structural` run on
|
|
74
|
+
* a language with no bundled grammar throws `GrammarUnavailableError` here exactly as
|
|
75
|
+
* it does anywhere else, because a planner that quietly fell back to line windows
|
|
76
|
+
* would be undetectable from outside. Coming back *over budget* is not a refusal and
|
|
77
|
+
* not an error: the plan is returned as it came back, and whether that deserves a
|
|
78
|
+
* non-zero exit code is a question only a front door can answer.
|
|
79
|
+
*/
|
|
80
|
+
export declare function smeltBlob(op: SmeltBlobOp): Promise<SmeltBlobOutcome>;
|
|
81
|
+
/** One tree to map, fully resolved. */
|
|
82
|
+
export interface MapTreeOp {
|
|
83
|
+
/** A directory. Proving it is one is the front door's job (`readTree`). */
|
|
84
|
+
readonly root: string;
|
|
85
|
+
/** UTF-8 bytes. The map fits itself to this by construction. */
|
|
86
|
+
readonly budgetBytes: number;
|
|
87
|
+
readonly focus?: readonly string[];
|
|
88
|
+
/** Replaces the built-in ignore list when present. */
|
|
89
|
+
readonly ignore?: readonly string[];
|
|
90
|
+
/** Only when present does the map write anything to disk. */
|
|
91
|
+
readonly cacheDir?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Verb: **map a whole tree inside a budget.**
|
|
95
|
+
*
|
|
96
|
+
* Returns the {@link RepoMap} itself — the map *is* the data, warnings and cache
|
|
97
|
+
* counts included, and wrapping it in an outcome struct would only give two front
|
|
98
|
+
* doors a second place to disagree about what a map is. Nothing is elided, stored or
|
|
99
|
+
* reversible, so there is nothing here to retrieve and no over-budget case to report.
|
|
100
|
+
*/
|
|
101
|
+
export declare function mapTree(op: MapTreeOp): Promise<RepoMap>;
|
|
102
|
+
/** One hash to turn back into bytes. */
|
|
103
|
+
export interface RetrieveBytesOp {
|
|
104
|
+
/** The store holding them — already opened. See `openStore` in `ops/inputs.ts`. */
|
|
105
|
+
readonly store: ElisionStore;
|
|
106
|
+
/** The hash exactly as a marker printed it. Validated by the store, not here. */
|
|
107
|
+
readonly hash: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Verb: **the counted read.**
|
|
111
|
+
*
|
|
112
|
+
* Thin on purpose, and named anyway: this is the expansion rate moving. `retrieve()`
|
|
113
|
+
* journals the hit, so a shell user typing `smelt retrieve` and a model calling
|
|
114
|
+
* `smelt_retrieve` move the same counter through the same call — which is the only
|
|
115
|
+
* reason the number means anything. The exact original bytes come back, nothing
|
|
116
|
+
* appended and nothing re-encoded, and an unknown or damaged hash throws the store's
|
|
117
|
+
* own distinct error rather than an empty string.
|
|
118
|
+
*
|
|
119
|
+
* @throws {UnknownHashError} for a hash the store never held.
|
|
120
|
+
* @throws {StoreCorruptionError} for bytes that no longer hash to their name.
|
|
121
|
+
*/
|
|
122
|
+
export declare function retrieveBytes(op: RetrieveBytesOp): string;
|
|
123
|
+
/** One store to read the counters off. */
|
|
124
|
+
export interface ReadCountersOp {
|
|
125
|
+
readonly store: ElisionStore;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Verb: **the uncounted read.**
|
|
129
|
+
*
|
|
130
|
+
* The sibling of {@link retrieveBytes}, and the asymmetry is the point: `stats()`
|
|
131
|
+
* folds the journal without writing to it, so watching the expansion rate can never
|
|
132
|
+
* move it. An observer that inflated its own metric would make the honest signal
|
|
133
|
+
* dishonest.
|
|
134
|
+
*/
|
|
135
|
+
export declare function readCounters(op: ReadCountersOp): RetrieveStats;
|
|
136
|
+
//# sourceMappingURL=verbs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verbs.d.ts","sourceRoot":"","sources":["../../src/ops/verbs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE9F;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,oFAAoF;IACpF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,qFAAqF;IACrF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,qFAAqF;IACrF,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAkB1E;AAED,uCAAuC;AACvC,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAQvD;AAED,wCAAwC;AACxC,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,eAAe,GAAG,MAAM,CAEzD;AAED,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,aAAa,CAE9D","sourcesContent":["import { buildRepoMap } from '../repomap/map.ts';\nimport type { RepoMap } from '../repomap/map.ts';\nimport { createSmelter } from '../smelter.ts';\nimport type { Strategy } from '../plan/planners.ts';\nimport type { DetectedLanguage, ElisionStore, RetrieveStats, SmeltResult } from '../types.ts';\n\n/**\n * The four verbs, as library functions over already-resolved inputs.\n *\n * This is the seam that sits **below both front doors**. The `smelt` CLI and the\n * `@smeltjs/mcp` server are the same four operations wearing two different coats, and\n * before this file existed each of them owned a full copy of the middle: build a\n * smelter from a strategy and a store, call it, hand the totals to a report; stat a\n * directory, build a map, render its warnings; retrieve one hash; read the counters.\n * The library exported the pieces, so both copies were *correct* — and both had to be\n * edited, in two packages, for one change to how a verb runs.\n *\n * What an op takes is already resolved: a budget that survived its law, a strategy\n * that won its precedence, a store that has been opened, text that has been read. What\n * an op returns is data — the smelted text and the values a report needs, a\n * {@link RepoMap}, bytes, counters. An op never sees argv, never writes to a stream,\n * never returns an exit code, and knows nothing about `CallToolResult`. Those are the\n * adapters' four jobs, and keeping them out here is what makes the same operation\n * testable once instead of twice.\n *\n * The division of labour, in one line: **a front door parses and resolves, an op runs,\n * a front door renders.**\n */\n\n/** One blob to smelt, fully resolved. */\nexport interface SmeltBlobOp {\n /** The text itself. Reading a file into it is the front door's job (`readBlob`). */\n readonly text: string;\n /** What to call the input in a report: a path, `'<stdin>'`, `'<text>'`. */\n readonly source: string;\n /** UTF-8 bytes. Already past the budget law — see `ops/inputs.ts`. */\n readonly budgetBytes: number;\n /** Already past its precedence — see `resolveStrategy` in `ops/inputs.ts`. */\n readonly strategy: Strategy;\n /**\n * Where elided bytes go. Optional only so a caller with nothing to persist gets the\n * library's own default (a fresh {@link MemoryElisionStore}); a front door that read\n * a config opens the store it decided on and passes it.\n */\n readonly store?: ElisionStore;\n /** The path the text came from, for language detection. Absent for a pasted blob. */\n readonly path?: string;\n /** Overrides detection entirely, when a surface offers that. */\n readonly language?: DetectedLanguage;\n /** What the task is about. Empty and absent mean the same thing to every planner. */\n readonly focus?: readonly string[];\n}\n\n/**\n * What one smelt run produced.\n *\n * The fields a report needs are the fields `formatReport` takes, deliberately: an op\n * returns exactly the values the render step reads off, so no front door assembles a\n * report input by hand and no front door counts a byte itself. (`formatReport(outcome)`\n * typechecks as written.) The store comes back too, because the `--json` envelope has\n * to `peek` at the bytes the run just stored, and a caller that passed no store never\n * had a handle on the one the library made.\n */\nexport interface SmeltBlobOutcome {\n readonly result: SmeltResult;\n /** What the input was called. Echoed back so a report never re-derives it. */\n readonly source: string;\n /** The budget the run was given, so a report can say when it was missed. */\n readonly budgetBytes: number;\n /** The exact text that was smelted. */\n readonly inputText: string;\n /** The store the run actually used — the one passed in, or the library's default. */\n readonly store: ElisionStore;\n}\n\n/**\n * Verb: **cut one blob to a budget.**\n *\n * Every refusal the library can raise passes straight through — a `structural` run on\n * a language with no bundled grammar throws `GrammarUnavailableError` here exactly as\n * it does anywhere else, because a planner that quietly fell back to line windows\n * would be undetectable from outside. Coming back *over budget* is not a refusal and\n * not an error: the plan is returned as it came back, and whether that deserves a\n * non-zero exit code is a question only a front door can answer.\n */\nexport async function smeltBlob(op: SmeltBlobOp): Promise<SmeltBlobOutcome> {\n const smelter = createSmelter({\n strategy: op.strategy,\n ...(op.store === undefined ? {} : { store: op.store }),\n });\n const result = await smelter.smelt(op.text, {\n budgetBytes: op.budgetBytes,\n ...(op.path === undefined ? {} : { path: op.path }),\n ...(op.language === undefined ? {} : { language: op.language }),\n ...(op.focus === undefined || op.focus.length === 0 ? {} : { focus: op.focus }),\n });\n return {\n result,\n source: op.source,\n budgetBytes: op.budgetBytes,\n inputText: op.text,\n store: smelter.store,\n };\n}\n\n/** One tree to map, fully resolved. */\nexport interface MapTreeOp {\n /** A directory. Proving it is one is the front door's job (`readTree`). */\n readonly root: string;\n /** UTF-8 bytes. The map fits itself to this by construction. */\n readonly budgetBytes: number;\n readonly focus?: readonly string[];\n /** Replaces the built-in ignore list when present. */\n readonly ignore?: readonly string[];\n /** Only when present does the map write anything to disk. */\n readonly cacheDir?: string;\n}\n\n/**\n * Verb: **map a whole tree inside a budget.**\n *\n * Returns the {@link RepoMap} itself — the map *is* the data, warnings and cache\n * counts included, and wrapping it in an outcome struct would only give two front\n * doors a second place to disagree about what a map is. Nothing is elided, stored or\n * reversible, so there is nothing here to retrieve and no over-budget case to report.\n */\nexport function mapTree(op: MapTreeOp): Promise<RepoMap> {\n return buildRepoMap({\n root: op.root,\n budgetBytes: op.budgetBytes,\n ...(op.focus === undefined || op.focus.length === 0 ? {} : { focus: op.focus }),\n ...(op.ignore === undefined || op.ignore.length === 0 ? {} : { ignore: op.ignore }),\n ...(op.cacheDir === undefined ? {} : { cacheDir: op.cacheDir }),\n });\n}\n\n/** One hash to turn back into bytes. */\nexport interface RetrieveBytesOp {\n /** The store holding them — already opened. See `openStore` in `ops/inputs.ts`. */\n readonly store: ElisionStore;\n /** The hash exactly as a marker printed it. Validated by the store, not here. */\n readonly hash: string;\n}\n\n/**\n * Verb: **the counted read.**\n *\n * Thin on purpose, and named anyway: this is the expansion rate moving. `retrieve()`\n * journals the hit, so a shell user typing `smelt retrieve` and a model calling\n * `smelt_retrieve` move the same counter through the same call — which is the only\n * reason the number means anything. The exact original bytes come back, nothing\n * appended and nothing re-encoded, and an unknown or damaged hash throws the store's\n * own distinct error rather than an empty string.\n *\n * @throws {UnknownHashError} for a hash the store never held.\n * @throws {StoreCorruptionError} for bytes that no longer hash to their name.\n */\nexport function retrieveBytes(op: RetrieveBytesOp): string {\n return op.store.retrieve(op.hash);\n}\n\n/** One store to read the counters off. */\nexport interface ReadCountersOp {\n readonly store: ElisionStore;\n}\n\n/**\n * Verb: **the uncounted read.**\n *\n * The sibling of {@link retrieveBytes}, and the asymmetry is the point: `stats()`\n * folds the journal without writing to it, so watching the expansion rate can never\n * move it. An observer that inflated its own metric would make the honest signal\n * dishonest.\n */\nexport function readCounters(op: ReadCountersOp): RetrieveStats {\n return op.store.stats();\n}\n"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { buildRepoMap } from '../repomap/map.js';
|
|
2
|
+
import { createSmelter } from '../smelter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Verb: **cut one blob to a budget.**
|
|
5
|
+
*
|
|
6
|
+
* Every refusal the library can raise passes straight through — a `structural` run on
|
|
7
|
+
* a language with no bundled grammar throws `GrammarUnavailableError` here exactly as
|
|
8
|
+
* it does anywhere else, because a planner that quietly fell back to line windows
|
|
9
|
+
* would be undetectable from outside. Coming back *over budget* is not a refusal and
|
|
10
|
+
* not an error: the plan is returned as it came back, and whether that deserves a
|
|
11
|
+
* non-zero exit code is a question only a front door can answer.
|
|
12
|
+
*/
|
|
13
|
+
export async function smeltBlob(op) {
|
|
14
|
+
const smelter = createSmelter({
|
|
15
|
+
strategy: op.strategy,
|
|
16
|
+
...(op.store === undefined ? {} : { store: op.store }),
|
|
17
|
+
});
|
|
18
|
+
const result = await smelter.smelt(op.text, {
|
|
19
|
+
budgetBytes: op.budgetBytes,
|
|
20
|
+
...(op.path === undefined ? {} : { path: op.path }),
|
|
21
|
+
...(op.language === undefined ? {} : { language: op.language }),
|
|
22
|
+
...(op.focus === undefined || op.focus.length === 0 ? {} : { focus: op.focus }),
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
result,
|
|
26
|
+
source: op.source,
|
|
27
|
+
budgetBytes: op.budgetBytes,
|
|
28
|
+
inputText: op.text,
|
|
29
|
+
store: smelter.store,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Verb: **map a whole tree inside a budget.**
|
|
34
|
+
*
|
|
35
|
+
* Returns the {@link RepoMap} itself — the map *is* the data, warnings and cache
|
|
36
|
+
* counts included, and wrapping it in an outcome struct would only give two front
|
|
37
|
+
* doors a second place to disagree about what a map is. Nothing is elided, stored or
|
|
38
|
+
* reversible, so there is nothing here to retrieve and no over-budget case to report.
|
|
39
|
+
*/
|
|
40
|
+
export function mapTree(op) {
|
|
41
|
+
return buildRepoMap({
|
|
42
|
+
root: op.root,
|
|
43
|
+
budgetBytes: op.budgetBytes,
|
|
44
|
+
...(op.focus === undefined || op.focus.length === 0 ? {} : { focus: op.focus }),
|
|
45
|
+
...(op.ignore === undefined || op.ignore.length === 0 ? {} : { ignore: op.ignore }),
|
|
46
|
+
...(op.cacheDir === undefined ? {} : { cacheDir: op.cacheDir }),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Verb: **the counted read.**
|
|
51
|
+
*
|
|
52
|
+
* Thin on purpose, and named anyway: this is the expansion rate moving. `retrieve()`
|
|
53
|
+
* journals the hit, so a shell user typing `smelt retrieve` and a model calling
|
|
54
|
+
* `smelt_retrieve` move the same counter through the same call — which is the only
|
|
55
|
+
* reason the number means anything. The exact original bytes come back, nothing
|
|
56
|
+
* appended and nothing re-encoded, and an unknown or damaged hash throws the store's
|
|
57
|
+
* own distinct error rather than an empty string.
|
|
58
|
+
*
|
|
59
|
+
* @throws {UnknownHashError} for a hash the store never held.
|
|
60
|
+
* @throws {StoreCorruptionError} for bytes that no longer hash to their name.
|
|
61
|
+
*/
|
|
62
|
+
export function retrieveBytes(op) {
|
|
63
|
+
return op.store.retrieve(op.hash);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Verb: **the uncounted read.**
|
|
67
|
+
*
|
|
68
|
+
* The sibling of {@link retrieveBytes}, and the asymmetry is the point: `stats()`
|
|
69
|
+
* folds the journal without writing to it, so watching the expansion rate can never
|
|
70
|
+
* move it. An observer that inflated its own metric would make the honest signal
|
|
71
|
+
* dishonest.
|
|
72
|
+
*/
|
|
73
|
+
export function readCounters(op) {
|
|
74
|
+
return op.store.stats();
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=verbs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verbs.js","sourceRoot":"","sources":["../../src/ops/verbs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAyE9C;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,EAAe;IAC7C,MAAM,OAAO,GAAG,aAAa,CAAC;QAC5B,QAAQ,EAAE,EAAE,CAAC,QAAQ;QACrB,GAAG,CAAC,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC;KACvD,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE;QAC1C,WAAW,EAAE,EAAE,CAAC,WAAW;QAC3B,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;QACnD,GAAG,CAAC,EAAE,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC;QAC/D,GAAG,CAAC,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC;KAChF,CAAC,CAAC;IACH,OAAO;QACL,MAAM;QACN,MAAM,EAAE,EAAE,CAAC,MAAM;QACjB,WAAW,EAAE,EAAE,CAAC,WAAW;QAC3B,SAAS,EAAE,EAAE,CAAC,IAAI;QAClB,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC;AACJ,CAAC;AAeD;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,EAAa;IACnC,OAAO,YAAY,CAAC;QAClB,IAAI,EAAE,EAAE,CAAC,IAAI;QACb,WAAW,EAAE,EAAE,CAAC,WAAW;QAC3B,GAAG,CAAC,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC;QAC/E,GAAG,CAAC,EAAE,CAAC,MAAM,KAAK,SAAS,IAAI,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC;QACnF,GAAG,CAAC,EAAE,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC;KAChE,CAAC,CAAC;AACL,CAAC;AAUD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,EAAmB;IAC/C,OAAO,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC;AAOD;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,EAAkB;IAC7C,OAAO,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;AAC1B,CAAC","sourcesContent":["import { buildRepoMap } from '../repomap/map.ts';\nimport type { RepoMap } from '../repomap/map.ts';\nimport { createSmelter } from '../smelter.ts';\nimport type { Strategy } from '../plan/planners.ts';\nimport type { DetectedLanguage, ElisionStore, RetrieveStats, SmeltResult } from '../types.ts';\n\n/**\n * The four verbs, as library functions over already-resolved inputs.\n *\n * This is the seam that sits **below both front doors**. The `smelt` CLI and the\n * `@smeltjs/mcp` server are the same four operations wearing two different coats, and\n * before this file existed each of them owned a full copy of the middle: build a\n * smelter from a strategy and a store, call it, hand the totals to a report; stat a\n * directory, build a map, render its warnings; retrieve one hash; read the counters.\n * The library exported the pieces, so both copies were *correct* — and both had to be\n * edited, in two packages, for one change to how a verb runs.\n *\n * What an op takes is already resolved: a budget that survived its law, a strategy\n * that won its precedence, a store that has been opened, text that has been read. What\n * an op returns is data — the smelted text and the values a report needs, a\n * {@link RepoMap}, bytes, counters. An op never sees argv, never writes to a stream,\n * never returns an exit code, and knows nothing about `CallToolResult`. Those are the\n * adapters' four jobs, and keeping them out here is what makes the same operation\n * testable once instead of twice.\n *\n * The division of labour, in one line: **a front door parses and resolves, an op runs,\n * a front door renders.**\n */\n\n/** One blob to smelt, fully resolved. */\nexport interface SmeltBlobOp {\n /** The text itself. Reading a file into it is the front door's job (`readBlob`). */\n readonly text: string;\n /** What to call the input in a report: a path, `'<stdin>'`, `'<text>'`. */\n readonly source: string;\n /** UTF-8 bytes. Already past the budget law — see `ops/inputs.ts`. */\n readonly budgetBytes: number;\n /** Already past its precedence — see `resolveStrategy` in `ops/inputs.ts`. */\n readonly strategy: Strategy;\n /**\n * Where elided bytes go. Optional only so a caller with nothing to persist gets the\n * library's own default (a fresh {@link MemoryElisionStore}); a front door that read\n * a config opens the store it decided on and passes it.\n */\n readonly store?: ElisionStore;\n /** The path the text came from, for language detection. Absent for a pasted blob. */\n readonly path?: string;\n /** Overrides detection entirely, when a surface offers that. */\n readonly language?: DetectedLanguage;\n /** What the task is about. Empty and absent mean the same thing to every planner. */\n readonly focus?: readonly string[];\n}\n\n/**\n * What one smelt run produced.\n *\n * The fields a report needs are the fields `formatReport` takes, deliberately: an op\n * returns exactly the values the render step reads off, so no front door assembles a\n * report input by hand and no front door counts a byte itself. (`formatReport(outcome)`\n * typechecks as written.) The store comes back too, because the `--json` envelope has\n * to `peek` at the bytes the run just stored, and a caller that passed no store never\n * had a handle on the one the library made.\n */\nexport interface SmeltBlobOutcome {\n readonly result: SmeltResult;\n /** What the input was called. Echoed back so a report never re-derives it. */\n readonly source: string;\n /** The budget the run was given, so a report can say when it was missed. */\n readonly budgetBytes: number;\n /** The exact text that was smelted. */\n readonly inputText: string;\n /** The store the run actually used — the one passed in, or the library's default. */\n readonly store: ElisionStore;\n}\n\n/**\n * Verb: **cut one blob to a budget.**\n *\n * Every refusal the library can raise passes straight through — a `structural` run on\n * a language with no bundled grammar throws `GrammarUnavailableError` here exactly as\n * it does anywhere else, because a planner that quietly fell back to line windows\n * would be undetectable from outside. Coming back *over budget* is not a refusal and\n * not an error: the plan is returned as it came back, and whether that deserves a\n * non-zero exit code is a question only a front door can answer.\n */\nexport async function smeltBlob(op: SmeltBlobOp): Promise<SmeltBlobOutcome> {\n const smelter = createSmelter({\n strategy: op.strategy,\n ...(op.store === undefined ? {} : { store: op.store }),\n });\n const result = await smelter.smelt(op.text, {\n budgetBytes: op.budgetBytes,\n ...(op.path === undefined ? {} : { path: op.path }),\n ...(op.language === undefined ? {} : { language: op.language }),\n ...(op.focus === undefined || op.focus.length === 0 ? {} : { focus: op.focus }),\n });\n return {\n result,\n source: op.source,\n budgetBytes: op.budgetBytes,\n inputText: op.text,\n store: smelter.store,\n };\n}\n\n/** One tree to map, fully resolved. */\nexport interface MapTreeOp {\n /** A directory. Proving it is one is the front door's job (`readTree`). */\n readonly root: string;\n /** UTF-8 bytes. The map fits itself to this by construction. */\n readonly budgetBytes: number;\n readonly focus?: readonly string[];\n /** Replaces the built-in ignore list when present. */\n readonly ignore?: readonly string[];\n /** Only when present does the map write anything to disk. */\n readonly cacheDir?: string;\n}\n\n/**\n * Verb: **map a whole tree inside a budget.**\n *\n * Returns the {@link RepoMap} itself — the map *is* the data, warnings and cache\n * counts included, and wrapping it in an outcome struct would only give two front\n * doors a second place to disagree about what a map is. Nothing is elided, stored or\n * reversible, so there is nothing here to retrieve and no over-budget case to report.\n */\nexport function mapTree(op: MapTreeOp): Promise<RepoMap> {\n return buildRepoMap({\n root: op.root,\n budgetBytes: op.budgetBytes,\n ...(op.focus === undefined || op.focus.length === 0 ? {} : { focus: op.focus }),\n ...(op.ignore === undefined || op.ignore.length === 0 ? {} : { ignore: op.ignore }),\n ...(op.cacheDir === undefined ? {} : { cacheDir: op.cacheDir }),\n });\n}\n\n/** One hash to turn back into bytes. */\nexport interface RetrieveBytesOp {\n /** The store holding them — already opened. See `openStore` in `ops/inputs.ts`. */\n readonly store: ElisionStore;\n /** The hash exactly as a marker printed it. Validated by the store, not here. */\n readonly hash: string;\n}\n\n/**\n * Verb: **the counted read.**\n *\n * Thin on purpose, and named anyway: this is the expansion rate moving. `retrieve()`\n * journals the hit, so a shell user typing `smelt retrieve` and a model calling\n * `smelt_retrieve` move the same counter through the same call — which is the only\n * reason the number means anything. The exact original bytes come back, nothing\n * appended and nothing re-encoded, and an unknown or damaged hash throws the store's\n * own distinct error rather than an empty string.\n *\n * @throws {UnknownHashError} for a hash the store never held.\n * @throws {StoreCorruptionError} for bytes that no longer hash to their name.\n */\nexport function retrieveBytes(op: RetrieveBytesOp): string {\n return op.store.retrieve(op.hash);\n}\n\n/** One store to read the counters off. */\nexport interface ReadCountersOp {\n readonly store: ElisionStore;\n}\n\n/**\n * Verb: **the uncounted read.**\n *\n * The sibling of {@link retrieveBytes}, and the asymmetry is the point: `stats()`\n * folds the journal without writing to it, so watching the expansion rate can never\n * move it. An observer that inflated its own metric would make the honest signal\n * dishonest.\n */\nexport function readCounters(op: ReadCountersOp): RetrieveStats {\n return op.store.stats();\n}\n"]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ElisionPlan, PlanInput, Planner } from '../types.ts';
|
|
2
|
+
import type { LexicalPlannerOptions } from './lexical.ts';
|
|
3
|
+
import type { StructuralPlannerOptions } from './structural.ts';
|
|
4
|
+
/**
|
|
5
|
+
* The id of the *selector*, never of a plan.
|
|
6
|
+
*
|
|
7
|
+
* Every plan `auto` returns carries the id of the planner that actually ran —
|
|
8
|
+
* `lexical/v1` or `structural/v1` — because a caller reading `result.planner` is
|
|
9
|
+
* asking what happened to their bytes, not which name they typed. This constant
|
|
10
|
+
* exists so `PLANNERS.auto({}).id` says something true about the object it is on, and
|
|
11
|
+
* `test/guards/auto-strategy.test.ts` pins that it never reaches an `ElisionPlan`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const AUTO_PLANNER_ID = "auto/v1";
|
|
14
|
+
export interface AutoPlannerOptions {
|
|
15
|
+
/** Passed through when auto picks the lexical planner. */
|
|
16
|
+
readonly lexical?: LexicalPlannerOptions;
|
|
17
|
+
/** Passed through when auto picks the structural planner. */
|
|
18
|
+
readonly structural?: StructuralPlannerOptions;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The strategy that picks a strategy: **structural where a grammar is bundled,
|
|
22
|
+
* lexical everywhere else, and the result says which one ran.**
|
|
23
|
+
*
|
|
24
|
+
* It exists because the choice it makes is one a caller cannot make once. A consumer
|
|
25
|
+
* smelting whatever a tool handed it — a `.ts` file this call, a build log the next —
|
|
26
|
+
* has to name a strategy per call or accept the wrong one every other call, and the
|
|
27
|
+
* two wrong answers are not symmetric: `lexical` on TypeScript is a working planner
|
|
28
|
+
* doing a weak job, while `structural` on a build log is a `GrammarUnavailableError`.
|
|
29
|
+
* So the honest default for a *mixed* stream was neither name, and callers picked one
|
|
30
|
+
* anyway.
|
|
31
|
+
*
|
|
32
|
+
* **This is a selector, not a fallback, and the distinction is the whole design.**
|
|
33
|
+
*
|
|
34
|
+
* - It decides on a *fact*: whether the language carries a bundled grammar
|
|
35
|
+
* ({@link STRUCTURAL_LANGUAGES}). That is knowable before a byte is parsed, so
|
|
36
|
+
* the decision is made up front and stated in the result's `planner` field.
|
|
37
|
+
* - It never decides on an *accident*. A grammar that fails to load on a language
|
|
38
|
+
* smelt claims to support is a broken install, and `GrammarUnavailableError`
|
|
39
|
+
* travels straight out of here exactly as it does under `strategy: 'structural'`.
|
|
40
|
+
* Catching it and answering with line windows would turn a loud environment fault
|
|
41
|
+
* into quietly worse output — the failure mode Law 2's no-silent-downgrade
|
|
42
|
+
* reasoning is about, wearing a friendlier name.
|
|
43
|
+
* - It changes nothing about an explicit `strategy: 'structural'`. That still
|
|
44
|
+
* refuses an unsupported language, because a caller who named the planner asked
|
|
45
|
+
* for *its* guarantees, and a refusal is the only answer that does not fabricate
|
|
46
|
+
* them. `auto` is a different request — "pick for me" — and answers it in the
|
|
47
|
+
* open.
|
|
48
|
+
*
|
|
49
|
+
* `DEFAULT_STRATEGY` stays `lexical`: auto is opt-in, and a default that changed
|
|
50
|
+
* which planner ran would change what existing callers' results are labelled without
|
|
51
|
+
* anyone asking for it.
|
|
52
|
+
*/
|
|
53
|
+
export declare class AutoPlanner implements Planner {
|
|
54
|
+
#private;
|
|
55
|
+
readonly id = "auto/v1";
|
|
56
|
+
constructor(options?: AutoPlannerOptions);
|
|
57
|
+
plan(input: PlanInput): Promise<ElisionPlan>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The selector as a function, exported like {@link planLexical} and
|
|
61
|
+
* {@link planStructural} so it can be tested and reused directly. Deterministic: the
|
|
62
|
+
* choice is a lookup in {@link STRUCTURAL_LANGUAGES}, and each planner it delegates to
|
|
63
|
+
* is deterministic in turn.
|
|
64
|
+
*
|
|
65
|
+
* @throws {GrammarUnavailableError} when the language *is* one smelt claims to parse
|
|
66
|
+
* and its grammar cannot be loaded. Never caught here — see {@link AutoPlanner}.
|
|
67
|
+
*/
|
|
68
|
+
export declare function planAuto(input: PlanInput, options?: AutoPlannerOptions): Promise<ElisionPlan>;
|
|
69
|
+
//# sourceMappingURL=auto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto.d.ts","sourceRoot":"","sources":["../../src/plan/auto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAGnE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,YAAY,CAAC;AAEzC,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC;IACzC,6DAA6D;IAC7D,QAAQ,CAAC,UAAU,CAAC,EAAE,wBAAwB,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,WAAY,YAAW,OAAO;;IACzC,QAAQ,CAAC,EAAE,aAAmB;IAG9B,YAAY,OAAO,GAAE,kBAAuB,EAE3C;IAED,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAE3C;CACF;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAIjG","sourcesContent":["import type { ElisionPlan, PlanInput, Planner } from '../types.ts';\n\nimport { planLexical } from './lexical.ts';\nimport type { LexicalPlannerOptions } from './lexical.ts';\nimport { isStructuralLanguage, planStructural } from './structural.ts';\nimport type { StructuralPlannerOptions } from './structural.ts';\n\n/**\n * The id of the *selector*, never of a plan.\n *\n * Every plan `auto` returns carries the id of the planner that actually ran —\n * `lexical/v1` or `structural/v1` — because a caller reading `result.planner` is\n * asking what happened to their bytes, not which name they typed. This constant\n * exists so `PLANNERS.auto({}).id` says something true about the object it is on, and\n * `test/guards/auto-strategy.test.ts` pins that it never reaches an `ElisionPlan`.\n */\nexport const AUTO_PLANNER_ID = 'auto/v1';\n\nexport interface AutoPlannerOptions {\n /** Passed through when auto picks the lexical planner. */\n readonly lexical?: LexicalPlannerOptions;\n /** Passed through when auto picks the structural planner. */\n readonly structural?: StructuralPlannerOptions;\n}\n\n/**\n * The strategy that picks a strategy: **structural where a grammar is bundled,\n * lexical everywhere else, and the result says which one ran.**\n *\n * It exists because the choice it makes is one a caller cannot make once. A consumer\n * smelting whatever a tool handed it — a `.ts` file this call, a build log the next —\n * has to name a strategy per call or accept the wrong one every other call, and the\n * two wrong answers are not symmetric: `lexical` on TypeScript is a working planner\n * doing a weak job, while `structural` on a build log is a `GrammarUnavailableError`.\n * So the honest default for a *mixed* stream was neither name, and callers picked one\n * anyway.\n *\n * **This is a selector, not a fallback, and the distinction is the whole design.**\n *\n * - It decides on a *fact*: whether the language carries a bundled grammar\n * ({@link STRUCTURAL_LANGUAGES}). That is knowable before a byte is parsed, so\n * the decision is made up front and stated in the result's `planner` field.\n * - It never decides on an *accident*. A grammar that fails to load on a language\n * smelt claims to support is a broken install, and `GrammarUnavailableError`\n * travels straight out of here exactly as it does under `strategy: 'structural'`.\n * Catching it and answering with line windows would turn a loud environment fault\n * into quietly worse output — the failure mode Law 2's no-silent-downgrade\n * reasoning is about, wearing a friendlier name.\n * - It changes nothing about an explicit `strategy: 'structural'`. That still\n * refuses an unsupported language, because a caller who named the planner asked\n * for *its* guarantees, and a refusal is the only answer that does not fabricate\n * them. `auto` is a different request — \"pick for me\" — and answers it in the\n * open.\n *\n * `DEFAULT_STRATEGY` stays `lexical`: auto is opt-in, and a default that changed\n * which planner ran would change what existing callers' results are labelled without\n * anyone asking for it.\n */\nexport class AutoPlanner implements Planner {\n readonly id = AUTO_PLANNER_ID;\n readonly #options: AutoPlannerOptions;\n\n constructor(options: AutoPlannerOptions = {}) {\n this.#options = options;\n }\n\n plan(input: PlanInput): Promise<ElisionPlan> {\n return planAuto(input, this.#options);\n }\n}\n\n/**\n * The selector as a function, exported like {@link planLexical} and\n * {@link planStructural} so it can be tested and reused directly. Deterministic: the\n * choice is a lookup in {@link STRUCTURAL_LANGUAGES}, and each planner it delegates to\n * is deterministic in turn.\n *\n * @throws {GrammarUnavailableError} when the language *is* one smelt claims to parse\n * and its grammar cannot be loaded. Never caught here — see {@link AutoPlanner}.\n */\nexport function planAuto(input: PlanInput, options: AutoPlannerOptions = {}): Promise<ElisionPlan> {\n return isStructuralLanguage(input.language)\n ? planStructural(input, options.structural ?? {})\n : Promise.resolve(planLexical(input, options.lexical ?? {}));\n}\n"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { planLexical } from './lexical.js';
|
|
2
|
+
import { isStructuralLanguage, planStructural } from './structural.js';
|
|
3
|
+
/**
|
|
4
|
+
* The id of the *selector*, never of a plan.
|
|
5
|
+
*
|
|
6
|
+
* Every plan `auto` returns carries the id of the planner that actually ran —
|
|
7
|
+
* `lexical/v1` or `structural/v1` — because a caller reading `result.planner` is
|
|
8
|
+
* asking what happened to their bytes, not which name they typed. This constant
|
|
9
|
+
* exists so `PLANNERS.auto({}).id` says something true about the object it is on, and
|
|
10
|
+
* `test/guards/auto-strategy.test.ts` pins that it never reaches an `ElisionPlan`.
|
|
11
|
+
*/
|
|
12
|
+
export const AUTO_PLANNER_ID = 'auto/v1';
|
|
13
|
+
/**
|
|
14
|
+
* The strategy that picks a strategy: **structural where a grammar is bundled,
|
|
15
|
+
* lexical everywhere else, and the result says which one ran.**
|
|
16
|
+
*
|
|
17
|
+
* It exists because the choice it makes is one a caller cannot make once. A consumer
|
|
18
|
+
* smelting whatever a tool handed it — a `.ts` file this call, a build log the next —
|
|
19
|
+
* has to name a strategy per call or accept the wrong one every other call, and the
|
|
20
|
+
* two wrong answers are not symmetric: `lexical` on TypeScript is a working planner
|
|
21
|
+
* doing a weak job, while `structural` on a build log is a `GrammarUnavailableError`.
|
|
22
|
+
* So the honest default for a *mixed* stream was neither name, and callers picked one
|
|
23
|
+
* anyway.
|
|
24
|
+
*
|
|
25
|
+
* **This is a selector, not a fallback, and the distinction is the whole design.**
|
|
26
|
+
*
|
|
27
|
+
* - It decides on a *fact*: whether the language carries a bundled grammar
|
|
28
|
+
* ({@link STRUCTURAL_LANGUAGES}). That is knowable before a byte is parsed, so
|
|
29
|
+
* the decision is made up front and stated in the result's `planner` field.
|
|
30
|
+
* - It never decides on an *accident*. A grammar that fails to load on a language
|
|
31
|
+
* smelt claims to support is a broken install, and `GrammarUnavailableError`
|
|
32
|
+
* travels straight out of here exactly as it does under `strategy: 'structural'`.
|
|
33
|
+
* Catching it and answering with line windows would turn a loud environment fault
|
|
34
|
+
* into quietly worse output — the failure mode Law 2's no-silent-downgrade
|
|
35
|
+
* reasoning is about, wearing a friendlier name.
|
|
36
|
+
* - It changes nothing about an explicit `strategy: 'structural'`. That still
|
|
37
|
+
* refuses an unsupported language, because a caller who named the planner asked
|
|
38
|
+
* for *its* guarantees, and a refusal is the only answer that does not fabricate
|
|
39
|
+
* them. `auto` is a different request — "pick for me" — and answers it in the
|
|
40
|
+
* open.
|
|
41
|
+
*
|
|
42
|
+
* `DEFAULT_STRATEGY` stays `lexical`: auto is opt-in, and a default that changed
|
|
43
|
+
* which planner ran would change what existing callers' results are labelled without
|
|
44
|
+
* anyone asking for it.
|
|
45
|
+
*/
|
|
46
|
+
export class AutoPlanner {
|
|
47
|
+
id = AUTO_PLANNER_ID;
|
|
48
|
+
#options;
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.#options = options;
|
|
51
|
+
}
|
|
52
|
+
plan(input) {
|
|
53
|
+
return planAuto(input, this.#options);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The selector as a function, exported like {@link planLexical} and
|
|
58
|
+
* {@link planStructural} so it can be tested and reused directly. Deterministic: the
|
|
59
|
+
* choice is a lookup in {@link STRUCTURAL_LANGUAGES}, and each planner it delegates to
|
|
60
|
+
* is deterministic in turn.
|
|
61
|
+
*
|
|
62
|
+
* @throws {GrammarUnavailableError} when the language *is* one smelt claims to parse
|
|
63
|
+
* and its grammar cannot be loaded. Never caught here — see {@link AutoPlanner}.
|
|
64
|
+
*/
|
|
65
|
+
export function planAuto(input, options = {}) {
|
|
66
|
+
return isStructuralLanguage(input.language)
|
|
67
|
+
? planStructural(input, options.structural ?? {})
|
|
68
|
+
: Promise.resolve(planLexical(input, options.lexical ?? {}));
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=auto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto.js","sourceRoot":"","sources":["../../src/plan/auto.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGvE;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,SAAS,CAAC;AASzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,OAAO,WAAW;IACb,EAAE,GAAG,eAAe,CAAC;IACrB,QAAQ,CAAqB;IAEtC,YAAY,OAAO,GAAuB,EAAE;QAC1C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,IAAI,CAAC,KAAgB;QACnB,OAAO,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAgB,EAAE,OAAO,GAAuB,EAAE;IACzE,OAAO,oBAAoB,CAAC,KAAK,CAAC,QAAQ,CAAC;QACzC,CAAC,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC;QACjD,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC;AACjE,CAAC","sourcesContent":["import type { ElisionPlan, PlanInput, Planner } from '../types.ts';\n\nimport { planLexical } from './lexical.ts';\nimport type { LexicalPlannerOptions } from './lexical.ts';\nimport { isStructuralLanguage, planStructural } from './structural.ts';\nimport type { StructuralPlannerOptions } from './structural.ts';\n\n/**\n * The id of the *selector*, never of a plan.\n *\n * Every plan `auto` returns carries the id of the planner that actually ran —\n * `lexical/v1` or `structural/v1` — because a caller reading `result.planner` is\n * asking what happened to their bytes, not which name they typed. This constant\n * exists so `PLANNERS.auto({}).id` says something true about the object it is on, and\n * `test/guards/auto-strategy.test.ts` pins that it never reaches an `ElisionPlan`.\n */\nexport const AUTO_PLANNER_ID = 'auto/v1';\n\nexport interface AutoPlannerOptions {\n /** Passed through when auto picks the lexical planner. */\n readonly lexical?: LexicalPlannerOptions;\n /** Passed through when auto picks the structural planner. */\n readonly structural?: StructuralPlannerOptions;\n}\n\n/**\n * The strategy that picks a strategy: **structural where a grammar is bundled,\n * lexical everywhere else, and the result says which one ran.**\n *\n * It exists because the choice it makes is one a caller cannot make once. A consumer\n * smelting whatever a tool handed it — a `.ts` file this call, a build log the next —\n * has to name a strategy per call or accept the wrong one every other call, and the\n * two wrong answers are not symmetric: `lexical` on TypeScript is a working planner\n * doing a weak job, while `structural` on a build log is a `GrammarUnavailableError`.\n * So the honest default for a *mixed* stream was neither name, and callers picked one\n * anyway.\n *\n * **This is a selector, not a fallback, and the distinction is the whole design.**\n *\n * - It decides on a *fact*: whether the language carries a bundled grammar\n * ({@link STRUCTURAL_LANGUAGES}). That is knowable before a byte is parsed, so\n * the decision is made up front and stated in the result's `planner` field.\n * - It never decides on an *accident*. A grammar that fails to load on a language\n * smelt claims to support is a broken install, and `GrammarUnavailableError`\n * travels straight out of here exactly as it does under `strategy: 'structural'`.\n * Catching it and answering with line windows would turn a loud environment fault\n * into quietly worse output — the failure mode Law 2's no-silent-downgrade\n * reasoning is about, wearing a friendlier name.\n * - It changes nothing about an explicit `strategy: 'structural'`. That still\n * refuses an unsupported language, because a caller who named the planner asked\n * for *its* guarantees, and a refusal is the only answer that does not fabricate\n * them. `auto` is a different request — \"pick for me\" — and answers it in the\n * open.\n *\n * `DEFAULT_STRATEGY` stays `lexical`: auto is opt-in, and a default that changed\n * which planner ran would change what existing callers' results are labelled without\n * anyone asking for it.\n */\nexport class AutoPlanner implements Planner {\n readonly id = AUTO_PLANNER_ID;\n readonly #options: AutoPlannerOptions;\n\n constructor(options: AutoPlannerOptions = {}) {\n this.#options = options;\n }\n\n plan(input: PlanInput): Promise<ElisionPlan> {\n return planAuto(input, this.#options);\n }\n}\n\n/**\n * The selector as a function, exported like {@link planLexical} and\n * {@link planStructural} so it can be tested and reused directly. Deterministic: the\n * choice is a lookup in {@link STRUCTURAL_LANGUAGES}, and each planner it delegates to\n * is deterministic in turn.\n *\n * @throws {GrammarUnavailableError} when the language *is* one smelt claims to parse\n * and its grammar cannot be loaded. Never caught here — see {@link AutoPlanner}.\n */\nexport function planAuto(input: PlanInput, options: AutoPlannerOptions = {}): Promise<ElisionPlan> {\n return isStructuralLanguage(input.language)\n ? planStructural(input, options.structural ?? {})\n : Promise.resolve(planLexical(input, options.lexical ?? {}));\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MarkerPricing, PlannedElision } from '../types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* What a plan will cost once its markers land — the arithmetic both planners do.
|
|
4
|
+
*
|
|
5
|
+
* Two planners now read `budgetBytes`, and both answer the same question with it:
|
|
6
|
+
* *given these elisions, how big is the output?* The lexical planner asks it to pick a
|
|
7
|
+
* ladder rung; the structural planner asks it to decide whether its budget rung is
|
|
8
|
+
* needed at all. Written twice it would be two answers to one question, which is how
|
|
9
|
+
* one planner ends up believing a marker costs something the other does not — the
|
|
10
|
+
* exact fork the {@link MarkerPricing} seam exists to prevent, one level up.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here estimates. Every byte comes from `pricing.costBytes`, the seam
|
|
13
|
+
* `apply.ts` builds from the exact builder `applyPlan` will use.
|
|
14
|
+
*/
|
|
15
|
+
/** The exact UTF-8 cost of the marker this elision would earn. Asked, not guessed. */
|
|
16
|
+
export declare function markerBytes(elision: PlannedElision, pricing: MarkerPricing): number;
|
|
17
|
+
/** What one elision saves: the bytes it removes, less the marker that replaces them. */
|
|
18
|
+
export declare function savingBytes(elision: PlannedElision, pricing: MarkerPricing): number;
|
|
19
|
+
/** The output size these elisions predict, markers included. */
|
|
20
|
+
export declare function predictOutputBytes(inputBytes: number, elisions: readonly PlannedElision[], pricing: MarkerPricing): number;
|
|
21
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../../src/plan/budget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEjE;;;;;;;;;;;;GAYG;AAEH,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,CAEnF;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,CAEnF;AAED,gEAAgE;AAChE,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,OAAO,EAAE,aAAa,GACrB,MAAM,CAER","sourcesContent":["import type { MarkerPricing, PlannedElision } from '../types.ts';\n\n/**\n * What a plan will cost once its markers land — the arithmetic both planners do.\n *\n * Two planners now read `budgetBytes`, and both answer the same question with it:\n * *given these elisions, how big is the output?* The lexical planner asks it to pick a\n * ladder rung; the structural planner asks it to decide whether its budget rung is\n * needed at all. Written twice it would be two answers to one question, which is how\n * one planner ends up believing a marker costs something the other does not — the\n * exact fork the {@link MarkerPricing} seam exists to prevent, one level up.\n *\n * Nothing here estimates. Every byte comes from `pricing.costBytes`, the seam\n * `apply.ts` builds from the exact builder `applyPlan` will use.\n */\n\n/** The exact UTF-8 cost of the marker this elision would earn. Asked, not guessed. */\nexport function markerBytes(elision: PlannedElision, pricing: MarkerPricing): number {\n return pricing.costBytes(elision.reason, elision.range.end - elision.range.start);\n}\n\n/** What one elision saves: the bytes it removes, less the marker that replaces them. */\nexport function savingBytes(elision: PlannedElision, pricing: MarkerPricing): number {\n return elision.range.end - elision.range.start - markerBytes(elision, pricing);\n}\n\n/** The output size these elisions predict, markers included. */\nexport function predictOutputBytes(\n inputBytes: number,\n elisions: readonly PlannedElision[],\n pricing: MarkerPricing,\n): number {\n return elisions.reduce((bytes, elision) => bytes - savingBytes(elision, pricing), inputBytes);\n}\n"]}
|