@smeltjs/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/apply.d.ts.map +1 -1
- package/dist/apply.js.map +1 -1
- package/dist/cache/prefix.d.ts.map +1 -1
- package/dist/cache/prefix.js.map +1 -1
- package/dist/cli/args.d.ts +16 -70
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +18 -338
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/config.d.ts +40 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +55 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/hooks.d.ts +33 -38
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +140 -485
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/init.d.ts +30 -15
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +167 -38
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +2 -2
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/run.d.ts +25 -88
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +32 -341
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/shell.d.ts +106 -0
- package/dist/cli/shell.d.ts.map +1 -0
- package/dist/cli/shell.js +100 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/subcommands/flags.d.ts +141 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -0
- package/dist/cli/subcommands/flags.js +208 -0
- package/dist/cli/subcommands/flags.js.map +1 -0
- package/dist/cli/subcommands/hooks.d.ts +18 -0
- package/dist/cli/subcommands/hooks.d.ts.map +1 -0
- package/dist/cli/subcommands/hooks.js +64 -0
- package/dist/cli/subcommands/hooks.js.map +1 -0
- package/dist/cli/subcommands/init.d.ts +19 -0
- package/dist/cli/subcommands/init.d.ts.map +1 -0
- package/dist/cli/subcommands/init.js +44 -0
- package/dist/cli/subcommands/init.js.map +1 -0
- package/dist/cli/subcommands/map.d.ts +75 -0
- package/dist/cli/subcommands/map.d.ts.map +1 -0
- package/dist/cli/subcommands/map.js +132 -0
- package/dist/cli/subcommands/map.js.map +1 -0
- package/dist/cli/subcommands/registry.d.ts +80 -0
- package/dist/cli/subcommands/registry.d.ts.map +1 -0
- package/dist/cli/subcommands/registry.js +138 -0
- package/dist/cli/subcommands/registry.js.map +1 -0
- package/dist/cli/subcommands/retrieve.d.ts +74 -0
- package/dist/cli/subcommands/retrieve.d.ts.map +1 -0
- package/dist/cli/subcommands/retrieve.js +84 -0
- package/dist/cli/subcommands/retrieve.js.map +1 -0
- package/dist/cli/subcommands/smelt.d.ts +107 -0
- package/dist/cli/subcommands/smelt.d.ts.map +1 -0
- package/dist/cli/subcommands/smelt.js +304 -0
- package/dist/cli/subcommands/smelt.js.map +1 -0
- package/dist/cli/subcommands/stats.d.ts +43 -0
- package/dist/cli/subcommands/stats.d.ts.map +1 -0
- package/dist/cli/subcommands/stats.js +62 -0
- package/dist/cli/subcommands/stats.js.map +1 -0
- package/dist/cli/subcommands/subcommand.d.ts +113 -0
- package/dist/cli/subcommands/subcommand.d.ts.map +1 -0
- package/dist/cli/subcommands/subcommand.js +3 -0
- package/dist/cli/subcommands/subcommand.js.map +1 -0
- package/dist/cli/usage.d.ts +3 -0
- package/dist/cli/usage.d.ts.map +1 -0
- package/dist/cli/usage.js +95 -0
- package/dist/cli/usage.js.map +1 -0
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js.map +1 -1
- package/dist/errors.d.ts +21 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +33 -1
- package/dist/errors.js.map +1 -1
- package/dist/harness/aider.d.ts +10 -0
- package/dist/harness/aider.d.ts.map +1 -0
- package/dist/harness/aider.js +21 -0
- package/dist/harness/aider.js.map +1 -0
- package/dist/harness/claude-code.d.ts +3 -0
- package/dist/harness/claude-code.d.ts.map +1 -0
- package/dist/harness/claude-code.js +68 -0
- package/dist/harness/claude-code.js.map +1 -0
- package/dist/harness/cline.d.ts +3 -0
- package/dist/harness/cline.d.ts.map +1 -0
- package/dist/harness/cline.js +54 -0
- package/dist/harness/cline.js.map +1 -0
- package/dist/harness/codex.d.ts +3 -0
- package/dist/harness/codex.d.ts.map +1 -0
- package/dist/harness/codex.js +91 -0
- package/dist/harness/codex.js.map +1 -0
- package/dist/harness/cursor.d.ts +3 -0
- package/dist/harness/cursor.d.ts.map +1 -0
- package/dist/harness/cursor.js +54 -0
- package/dist/harness/cursor.js.map +1 -0
- package/dist/harness/gemini.d.ts +3 -0
- package/dist/harness/gemini.d.ts.map +1 -0
- package/dist/harness/gemini.js +59 -0
- package/dist/harness/gemini.js.map +1 -0
- package/dist/harness/grok.d.ts +3 -0
- package/dist/harness/grok.d.ts.map +1 -0
- package/dist/harness/grok.js +46 -0
- package/dist/harness/grok.js.map +1 -0
- package/dist/harness/hermes.d.ts +3 -0
- package/dist/harness/hermes.d.ts.map +1 -0
- package/dist/harness/hermes.js +71 -0
- package/dist/harness/hermes.js.map +1 -0
- package/dist/harness/kilocode.d.ts +3 -0
- package/dist/harness/kilocode.d.ts.map +1 -0
- package/dist/harness/kilocode.js +35 -0
- package/dist/harness/kilocode.js.map +1 -0
- package/dist/harness/opencode.d.ts +3 -0
- package/dist/harness/opencode.d.ts.map +1 -0
- package/dist/harness/opencode.js +97 -0
- package/dist/harness/opencode.js.map +1 -0
- package/dist/harness/paths.d.ts +17 -0
- package/dist/harness/paths.d.ts.map +1 -0
- package/dist/harness/paths.js +45 -0
- package/dist/harness/paths.js.map +1 -0
- package/dist/harness/profile.d.ts +184 -0
- package/dist/harness/profile.d.ts.map +1 -0
- package/dist/harness/profile.js +26 -0
- package/dist/harness/profile.js.map +1 -0
- package/dist/harness/registry.d.ts +51 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +87 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/snippet.d.ts +19 -0
- package/dist/harness/snippet.d.ts.map +1 -0
- package/dist/harness/snippet.js +41 -0
- package/dist/harness/snippet.js.map +1 -0
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js.map +1 -1
- package/dist/hooks/guard-core.d.ts +14 -14
- package/dist/hooks/guard-core.d.ts.map +1 -1
- package/dist/hooks/guard-core.js +15 -37
- package/dist/hooks/guard-core.js.map +1 -1
- package/dist/hooks/shim.d.ts +95 -4
- package/dist/hooks/shim.d.ts.map +1 -1
- package/dist/hooks/shim.js +87 -2
- package/dist/hooks/shim.js.map +1 -1
- package/dist/hooks/shims/claude-code.d.ts +5 -18
- package/dist/hooks/shims/claude-code.d.ts.map +1 -1
- package/dist/hooks/shims/claude-code.js +9 -56
- package/dist/hooks/shims/claude-code.js.map +1 -1
- package/dist/hooks/shims/cline.d.ts +5 -12
- package/dist/hooks/shims/cline.d.ts.map +1 -1
- package/dist/hooks/shims/cline.js +9 -34
- package/dist/hooks/shims/cline.js.map +1 -1
- package/dist/hooks/shims/codex.d.ts +5 -18
- package/dist/hooks/shims/codex.d.ts.map +1 -1
- package/dist/hooks/shims/codex.js +9 -51
- package/dist/hooks/shims/codex.js.map +1 -1
- package/dist/hooks/shims/cursor.d.ts +5 -14
- package/dist/hooks/shims/cursor.d.ts.map +1 -1
- package/dist/hooks/shims/cursor.js +9 -42
- package/dist/hooks/shims/cursor.js.map +1 -1
- package/dist/hooks/shims/gemini.d.ts +5 -18
- package/dist/hooks/shims/gemini.d.ts.map +1 -1
- package/dist/hooks/shims/gemini.js +9 -48
- package/dist/hooks/shims/gemini.js.map +1 -1
- package/dist/hooks/shims/grok.d.ts +5 -13
- package/dist/hooks/shims/grok.d.ts.map +1 -1
- package/dist/hooks/shims/grok.js +9 -32
- package/dist/hooks/shims/grok.js.map +1 -1
- package/dist/hooks/shims/hermes.d.ts +5 -17
- package/dist/hooks/shims/hermes.d.ts.map +1 -1
- package/dist/hooks/shims/hermes.js +9 -45
- package/dist/hooks/shims/hermes.js.map +1 -1
- package/dist/index.d.ts +33 -76
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -67
- package/dist/index.js.map +1 -1
- package/dist/lang/bash.d.ts.map +1 -1
- package/dist/lang/bash.js.map +1 -1
- package/dist/lang/c.d.ts.map +1 -1
- package/dist/lang/c.js.map +1 -1
- package/dist/lang/c_sharp.d.ts.map +1 -1
- package/dist/lang/c_sharp.js.map +1 -1
- package/dist/lang/cpp.d.ts.map +1 -1
- package/dist/lang/cpp.js.map +1 -1
- package/dist/lang/go.d.ts.map +1 -1
- package/dist/lang/go.js.map +1 -1
- package/dist/lang/java.d.ts.map +1 -1
- package/dist/lang/java.js.map +1 -1
- package/dist/lang/javascript.d.ts.map +1 -1
- package/dist/lang/javascript.js.map +1 -1
- package/dist/lang/kotlin.d.ts.map +1 -1
- package/dist/lang/kotlin.js.map +1 -1
- package/dist/lang/php.d.ts.map +1 -1
- package/dist/lang/php.js.map +1 -1
- package/dist/lang/profile.d.ts.map +1 -1
- package/dist/lang/profile.js.map +1 -1
- package/dist/lang/python.d.ts.map +1 -1
- package/dist/lang/python.js.map +1 -1
- package/dist/lang/registry.d.ts.map +1 -1
- package/dist/lang/registry.js.map +1 -1
- package/dist/lang/ruby.d.ts.map +1 -1
- package/dist/lang/ruby.js.map +1 -1
- package/dist/lang/rust.d.ts.map +1 -1
- package/dist/lang/rust.js.map +1 -1
- package/dist/lang/swift.d.ts.map +1 -1
- package/dist/lang/swift.js.map +1 -1
- package/dist/lang/tsx.d.ts.map +1 -1
- package/dist/lang/tsx.js.map +1 -1
- package/dist/lang/typescript.d.ts.map +1 -1
- package/dist/lang/typescript.js.map +1 -1
- package/dist/net/policy.d.ts +18 -1
- package/dist/net/policy.d.ts.map +1 -1
- package/dist/net/policy.js +6 -4
- package/dist/net/policy.js.map +1 -1
- package/dist/ops/index.d.ts +39 -0
- package/dist/ops/index.d.ts.map +1 -0
- package/dist/ops/index.js +37 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/ops/inputs.d.ts +170 -0
- package/dist/ops/inputs.d.ts.map +1 -0
- package/dist/ops/inputs.js +142 -0
- package/dist/ops/inputs.js.map +1 -0
- package/dist/ops/verbs.d.ts +136 -0
- package/dist/ops/verbs.d.ts.map +1 -0
- package/dist/ops/verbs.js +76 -0
- package/dist/ops/verbs.js.map +1 -0
- package/dist/plan/auto.d.ts +69 -0
- package/dist/plan/auto.d.ts.map +1 -0
- package/dist/plan/auto.js +70 -0
- package/dist/plan/auto.js.map +1 -0
- package/dist/plan/budget.d.ts +21 -0
- package/dist/plan/budget.d.ts.map +1 -0
- package/dist/plan/budget.js +26 -0
- package/dist/plan/budget.js.map +1 -0
- package/dist/plan/grammar.d.ts +14 -0
- package/dist/plan/grammar.d.ts.map +1 -1
- package/dist/plan/grammar.js +47 -6
- package/dist/plan/grammar.js.map +1 -1
- package/dist/plan/lexical.d.ts.map +1 -1
- package/dist/plan/lexical.js +1 -7
- package/dist/plan/lexical.js.map +1 -1
- package/dist/plan/planners.d.ts +35 -2
- package/dist/plan/planners.d.ts.map +1 -1
- package/dist/plan/planners.js +35 -2
- package/dist/plan/planners.js.map +1 -1
- package/dist/plan/structural.d.ts +19 -5
- package/dist/plan/structural.d.ts.map +1 -1
- package/dist/plan/structural.js +180 -33
- package/dist/plan/structural.js.map +1 -1
- package/dist/repomap/cache.d.ts +48 -0
- package/dist/repomap/cache.d.ts.map +1 -1
- package/dist/repomap/cache.js +112 -13
- package/dist/repomap/cache.js.map +1 -1
- package/dist/repomap/io.d.ts +24 -0
- package/dist/repomap/io.d.ts.map +1 -0
- package/dist/repomap/io.js +34 -0
- package/dist/repomap/io.js.map +1 -0
- package/dist/repomap/map.d.ts +69 -6
- package/dist/repomap/map.d.ts.map +1 -1
- package/dist/repomap/map.js +104 -22
- package/dist/repomap/map.js.map +1 -1
- package/dist/repomap/rank.d.ts +26 -2
- package/dist/repomap/rank.d.ts.map +1 -1
- package/dist/repomap/rank.js +20 -0
- package/dist/repomap/rank.js.map +1 -1
- package/dist/repomap/reader.d.ts +73 -0
- package/dist/repomap/reader.d.ts.map +1 -0
- package/dist/repomap/reader.js +30 -0
- package/dist/repomap/reader.js.map +1 -0
- package/dist/repomap/tags.d.ts.map +1 -1
- package/dist/repomap/tags.js.map +1 -1
- package/dist/retrieve.d.ts +8 -0
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +12 -1
- package/dist/retrieve.js.map +1 -1
- package/dist/smelter.d.ts +79 -0
- package/dist/smelter.d.ts.map +1 -0
- package/dist/smelter.js +65 -0
- package/dist/smelter.js.map +1 -0
- package/dist/stages.d.ts.map +1 -1
- package/dist/stages.js +2 -2
- package/dist/stages.js.map +1 -1
- package/dist/stats.d.ts +2 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +2 -1
- package/dist/stats.js.map +1 -1
- package/dist/store-dir.d.ts +43 -4
- package/dist/store-dir.d.ts.map +1 -1
- package/dist/store-dir.js +48 -5
- package/dist/store-dir.js.map +1 -1
- package/dist/store.d.ts +6 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +6 -0
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +35 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +11 -11
- package/dist/cli/resolve.d.ts +0 -103
- package/dist/cli/resolve.d.ts.map +0 -1
- package/dist/cli/resolve.js +0 -106
- package/dist/cli/resolve.js.map +0 -1
|
@@ -0,0 +1,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"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a plan will cost once its markers land — the arithmetic both planners do.
|
|
3
|
+
*
|
|
4
|
+
* Two planners now read `budgetBytes`, and both answer the same question with it:
|
|
5
|
+
* *given these elisions, how big is the output?* The lexical planner asks it to pick a
|
|
6
|
+
* ladder rung; the structural planner asks it to decide whether its budget rung is
|
|
7
|
+
* needed at all. Written twice it would be two answers to one question, which is how
|
|
8
|
+
* one planner ends up believing a marker costs something the other does not — the
|
|
9
|
+
* exact fork the {@link MarkerPricing} seam exists to prevent, one level up.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here estimates. Every byte comes from `pricing.costBytes`, the seam
|
|
12
|
+
* `apply.ts` builds from the exact builder `applyPlan` will use.
|
|
13
|
+
*/
|
|
14
|
+
/** The exact UTF-8 cost of the marker this elision would earn. Asked, not guessed. */
|
|
15
|
+
export function markerBytes(elision, pricing) {
|
|
16
|
+
return pricing.costBytes(elision.reason, elision.range.end - elision.range.start);
|
|
17
|
+
}
|
|
18
|
+
/** What one elision saves: the bytes it removes, less the marker that replaces them. */
|
|
19
|
+
export function savingBytes(elision, pricing) {
|
|
20
|
+
return elision.range.end - elision.range.start - markerBytes(elision, pricing);
|
|
21
|
+
}
|
|
22
|
+
/** The output size these elisions predict, markers included. */
|
|
23
|
+
export function predictOutputBytes(inputBytes, elisions, pricing) {
|
|
24
|
+
return elisions.reduce((bytes, elision) => bytes - savingBytes(elision, pricing), inputBytes);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/plan/budget.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,sFAAsF;AACtF,MAAM,UAAU,WAAW,CAAC,OAAuB,EAAE,OAAsB;IACzE,OAAO,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACpF,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,WAAW,CAAC,OAAuB,EAAE,OAAsB;IACzE,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AACjF,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,kBAAkB,CAChC,UAAkB,EAClB,QAAmC,EACnC,OAAsB;IAEtB,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,KAAK,GAAG,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC;AAChG,CAAC","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"]}
|
package/dist/plan/grammar.d.ts
CHANGED
|
@@ -31,6 +31,20 @@ export declare function grammarPath(language: LanguageId): string;
|
|
|
31
31
|
* ordinary happy path. Reading the file ourselves removes that capability instead of
|
|
32
32
|
* documenting it, and {@link assertLocalResource} rejects a remote path before we get
|
|
33
33
|
* that far.
|
|
34
|
+
*
|
|
35
|
+
* **Every failure here is a `GrammarUnavailableError`.** The consumer contract makes
|
|
36
|
+
* exactly one promise about errors — every error smelt throws is an `instanceof
|
|
37
|
+
* SmeltError` — and this function is on the path of both `smelt()` and
|
|
38
|
+
* `buildRepoMap()`, for every file in a language smelt claims to parse. Only
|
|
39
|
+
* *resolution* used to be inside the contract: `grammarPath` throws for a grammar it
|
|
40
|
+
* cannot find, and {@link existsSync} then reports a file's **presence**, never its
|
|
41
|
+
* readability. So a grammar that resolved and then would not load leaked the raw
|
|
42
|
+
* error — `EACCES` from an unreadable `.wasm`, a V8 `CompileError` or `RangeError`
|
|
43
|
+
* from a truncated one, a half-extracted tarball — straight past a caller's
|
|
44
|
+
* documented `catch`. Each step is wrapped instead, naming the language and the path
|
|
45
|
+
* and keeping the original as `cause`: bringing the failure inside the contract, not
|
|
46
|
+
* hiding what Node or V8 said. A promise with one undocumented exception is no
|
|
47
|
+
* promise at all.
|
|
34
48
|
*/
|
|
35
49
|
export declare function loadGrammar(language: LanguageId): Promise<Language>;
|
|
36
50
|
/** Reset the grammar cache. Tests use it; production has no reason to. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/plan/grammar.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAU,MAAM,iBAAiB,CAAC;AAKnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAEnC,CAAC;AAkBhC;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAexD;AAED
|
|
1
|
+
{"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/plan/grammar.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAU,MAAM,iBAAiB,CAAC;AAKnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAEnC,CAAC;AAkBhC;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAexD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,WAAW,CAAC,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqBzE;AA4BD,0EAA0E;AAC1E,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC","sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { fileURLToPath } from 'node:url';\n\nimport { Language, Parser } from 'web-tree-sitter';\n\nimport { GrammarUnavailableError, SmeltError } from '../errors.ts';\nimport { LANGUAGE_PROFILES } from '../lang/registry.ts';\nimport { assertLocalResource } from '../net/policy.ts';\nimport type { LanguageId } from '../types.ts';\n\n/**\n * Grammar file for each language smelt claims to parse — the registry's `wasm`\n * facts, as a map. Derived from `LANGUAGE_PROFILES` (which is\n * `Record<LanguageId, LanguageProfile>`, so adding a `LanguageId` without a profile —\n * and its grammar — is a type error and the two cannot drift). It stays exported\n * because `scripts/bundle-grammars.mjs` and the attribution generator both read it —\n * a hand-written second list of grammar filenames would be exactly the drift the\n * registry exists to prevent.\n */\nexport const WASM_BY_LANGUAGE: Readonly<Record<LanguageId, string>> = Object.fromEntries(\n Object.values(LANGUAGE_PROFILES).map((profile) => [profile.id, profile.wasm]),\n) as Record<LanguageId, string>;\n\n/**\n * Where the bundled grammars live, relative to this module.\n *\n * From `dist/plan/grammar.js` and from `src/plan/grammar.ts` alike, `../../grammars/`\n * is this package's own `grammars/` directory — filled by `pnpm build` and shipped\n * inside the npm tarball. That is what makes \"zero native compilation, works offline\"\n * true rather than aspirational: whoever installs the package has the parsers, with no\n * post-install download and no optional peer dependency to remember. It is also\n * redistribution, which is why `THIRD-PARTY.md` exists and is generated.\n */\nconst BUNDLED_GRAMMAR_DIR = new URL('../../grammars/', import.meta.url);\n\nconst require = createRequire(import.meta.url);\nconst cache = new Map<LanguageId, Language>();\nlet runtimeReady: Promise<void> | undefined;\n\n/**\n * Resolve a grammar to a path on this machine.\n *\n * The copy bundled in this package wins; `tree-sitter-wasms` is the fallback, for a\n * source checkout that has not run `pnpm build` yet. Note what this function does *not*\n * do: it never constructs a URL from a version string, a CDN base, or anything else.\n * Grammars come off disk — either the ones shipped here or the ones a package manager\n * already installed. A \"fetch the grammar on first use\" cache is the most natural way\n * to break Law 1 without noticing, because it works perfectly on the machine that\n * wrote it.\n */\nexport function grammarPath(language: LanguageId): string {\n const file = WASM_BY_LANGUAGE[language];\n\n const bundled = fileURLToPath(new URL(file, BUNDLED_GRAMMAR_DIR));\n if (existsSync(bundled)) return bundled;\n\n try {\n return require.resolve(`tree-sitter-wasms/out/${file}`);\n } catch {\n throw new GrammarUnavailableError(\n `smelt: no grammar for \"${language}\". The bundled copy is missing (run ` +\n `\\`pnpm build\\` in a source checkout) and \\`tree-sitter-wasms\\` is not installed ` +\n `either. Pass \\`language: 'unknown'\\` to use the lexical planner.`,\n );\n }\n}\n\n/**\n * Load a grammar, from disk, once.\n *\n * The bytes are read here and handed to tree-sitter as a `Uint8Array` rather than\n * passing it a path. `Language.load()` accepts `string | URL`, and a `URL` with an\n * `https:` scheme would make it fetch — inside the elision path, from a dependency's\n * ordinary happy path. Reading the file ourselves removes that capability instead of\n * documenting it, and {@link assertLocalResource} rejects a remote path before we get\n * that far.\n *\n * **Every failure here is a `GrammarUnavailableError`.** The consumer contract makes\n * exactly one promise about errors — every error smelt throws is an `instanceof\n * SmeltError` — and this function is on the path of both `smelt()` and\n * `buildRepoMap()`, for every file in a language smelt claims to parse. Only\n * *resolution* used to be inside the contract: `grammarPath` throws for a grammar it\n * cannot find, and {@link existsSync} then reports a file's **presence**, never its\n * readability. So a grammar that resolved and then would not load leaked the raw\n * error — `EACCES` from an unreadable `.wasm`, a V8 `CompileError` or `RangeError`\n * from a truncated one, a half-extracted tarball — straight past a caller's\n * documented `catch`. Each step is wrapped instead, naming the language and the path\n * and keeping the original as `cause`: bringing the failure inside the contract, not\n * hiding what Node or V8 said. A promise with one undocumented exception is no\n * promise at all.\n */\nexport async function loadGrammar(language: LanguageId): Promise<Language> {\n const cached = cache.get(language);\n if (cached !== undefined) return cached;\n\n const path = fileURLToPath(assertLocalResource(grammarPath(language)).href);\n\n const ready = (runtimeReady ??= Parser.init());\n await inContract(\n () => ready,\n `smelt: the tree-sitter WASM runtime would not start, so no grammar can be loaded`,\n );\n const bytes = await inContract(\n () => readFile(path),\n `smelt: the grammar for \"${language}\" could not be read from \"${path}\"`,\n );\n const grammar = await inContract(\n () => Language.load(new Uint8Array(bytes)),\n `smelt: the file at \"${path}\" is not a loadable tree-sitter grammar for \"${language}\"`,\n );\n cache.set(language, grammar);\n return grammar;\n}\n\n/**\n * Run one step of the load and keep its failure inside the consumer contract.\n *\n * A {@link SmeltError} passes through untouched — `assertLocalResource` and\n * `grammarPath` already refuse in smelt's own currency, and rewrapping would bury a\n * sentence written deliberately under a generic one.\n */\nasync function inContract<T>(run: () => Promise<T>, what: string): Promise<T> {\n try {\n return await run();\n } catch (error) {\n if (error instanceof SmeltError) throw error;\n throw new GrammarUnavailableError(`${what}: ${describeFailure(error)}.`, { cause: error });\n }\n}\n\n/**\n * What Node or V8 actually said. Never invented, never swallowed — the wasm cases\n * (`CompileError`, `RangeError`) carry no `errno`, and their message is the whole\n * diagnosis.\n */\nfunction describeFailure(cause: unknown): string {\n if (cause instanceof Error && cause.message !== '') return cause.message;\n return String(cause);\n}\n\n/** Reset the grammar cache. Tests use it; production has no reason to. */\nexport function clearGrammarCache(): void {\n cache.clear();\n runtimeReady = undefined;\n}\n"]}
|
package/dist/plan/grammar.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { Language, Parser } from 'web-tree-sitter';
|
|
6
|
-
import { GrammarUnavailableError } from '../errors.js';
|
|
6
|
+
import { GrammarUnavailableError, SmeltError } from '../errors.js';
|
|
7
7
|
import { LANGUAGE_PROFILES } from '../lang/registry.js';
|
|
8
8
|
import { assertLocalResource } from '../net/policy.js';
|
|
9
9
|
/**
|
|
@@ -64,19 +64,60 @@ export function grammarPath(language) {
|
|
|
64
64
|
* ordinary happy path. Reading the file ourselves removes that capability instead of
|
|
65
65
|
* documenting it, and {@link assertLocalResource} rejects a remote path before we get
|
|
66
66
|
* that far.
|
|
67
|
+
*
|
|
68
|
+
* **Every failure here is a `GrammarUnavailableError`.** The consumer contract makes
|
|
69
|
+
* exactly one promise about errors — every error smelt throws is an `instanceof
|
|
70
|
+
* SmeltError` — and this function is on the path of both `smelt()` and
|
|
71
|
+
* `buildRepoMap()`, for every file in a language smelt claims to parse. Only
|
|
72
|
+
* *resolution* used to be inside the contract: `grammarPath` throws for a grammar it
|
|
73
|
+
* cannot find, and {@link existsSync} then reports a file's **presence**, never its
|
|
74
|
+
* readability. So a grammar that resolved and then would not load leaked the raw
|
|
75
|
+
* error — `EACCES` from an unreadable `.wasm`, a V8 `CompileError` or `RangeError`
|
|
76
|
+
* from a truncated one, a half-extracted tarball — straight past a caller's
|
|
77
|
+
* documented `catch`. Each step is wrapped instead, naming the language and the path
|
|
78
|
+
* and keeping the original as `cause`: bringing the failure inside the contract, not
|
|
79
|
+
* hiding what Node or V8 said. A promise with one undocumented exception is no
|
|
80
|
+
* promise at all.
|
|
67
81
|
*/
|
|
68
82
|
export async function loadGrammar(language) {
|
|
69
83
|
const cached = cache.get(language);
|
|
70
84
|
if (cached !== undefined)
|
|
71
85
|
return cached;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const bytes = await
|
|
76
|
-
const grammar = await Language.load(new Uint8Array(bytes));
|
|
86
|
+
const path = fileURLToPath(assertLocalResource(grammarPath(language)).href);
|
|
87
|
+
const ready = (runtimeReady ??= Parser.init());
|
|
88
|
+
await inContract(() => ready, `smelt: the tree-sitter WASM runtime would not start, so no grammar can be loaded`);
|
|
89
|
+
const bytes = await inContract(() => readFile(path), `smelt: the grammar for "${language}" could not be read from "${path}"`);
|
|
90
|
+
const grammar = await inContract(() => Language.load(new Uint8Array(bytes)), `smelt: the file at "${path}" is not a loadable tree-sitter grammar for "${language}"`);
|
|
77
91
|
cache.set(language, grammar);
|
|
78
92
|
return grammar;
|
|
79
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Run one step of the load and keep its failure inside the consumer contract.
|
|
96
|
+
*
|
|
97
|
+
* A {@link SmeltError} passes through untouched — `assertLocalResource` and
|
|
98
|
+
* `grammarPath` already refuse in smelt's own currency, and rewrapping would bury a
|
|
99
|
+
* sentence written deliberately under a generic one.
|
|
100
|
+
*/
|
|
101
|
+
async function inContract(run, what) {
|
|
102
|
+
try {
|
|
103
|
+
return await run();
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof SmeltError)
|
|
107
|
+
throw error;
|
|
108
|
+
throw new GrammarUnavailableError(`${what}: ${describeFailure(error)}.`, { cause: error });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* What Node or V8 actually said. Never invented, never swallowed — the wasm cases
|
|
113
|
+
* (`CompileError`, `RangeError`) carry no `errno`, and their message is the whole
|
|
114
|
+
* diagnosis.
|
|
115
|
+
*/
|
|
116
|
+
function describeFailure(cause) {
|
|
117
|
+
if (cause instanceof Error && cause.message !== '')
|
|
118
|
+
return cause.message;
|
|
119
|
+
return String(cause);
|
|
120
|
+
}
|
|
80
121
|
/** Reset the grammar cache. Tests use it; production has no reason to. */
|
|
81
122
|
export function clearGrammarCache() {
|
|
82
123
|
cache.clear();
|
package/dist/plan/grammar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grammar.js","sourceRoot":"","sources":["../../src/plan/grammar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"grammar.js","sourceRoot":"","sources":["../../src/plan/grammar.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { fileURLToPath } from 'node:url';\n\nimport { Language, Parser } from 'web-tree-sitter';\n\nimport { GrammarUnavailableError, SmeltError } from '../errors.ts';\nimport { LANGUAGE_PROFILES } from '../lang/registry.ts';\nimport { assertLocalResource } from '../net/policy.ts';\nimport type { LanguageId } from '../types.ts';\n\n/**\n * Grammar file for each language smelt claims to parse — the registry's `wasm`\n * facts, as a map. Derived from `LANGUAGE_PROFILES` (which is\n * `Record<LanguageId, LanguageProfile>`, so adding a `LanguageId` without a profile —\n * and its grammar — is a type error and the two cannot drift). It stays exported\n * because `scripts/bundle-grammars.mjs` and the attribution generator both read it —\n * a hand-written second list of grammar filenames would be exactly the drift the\n * registry exists to prevent.\n */\nexport const WASM_BY_LANGUAGE: Readonly<Record<LanguageId, string>> = Object.fromEntries(\n Object.values(LANGUAGE_PROFILES).map((profile) => [profile.id, profile.wasm]),\n) as Record<LanguageId, string>;\n\n/**\n * Where the bundled grammars live, relative to this module.\n *\n * From `dist/plan/grammar.js` and from `src/plan/grammar.ts` alike, `../../grammars/`\n * is this package's own `grammars/` directory — filled by `pnpm build` and shipped\n * inside the npm tarball. That is what makes \"zero native compilation, works offline\"\n * true rather than aspirational: whoever installs the package has the parsers, with no\n * post-install download and no optional peer dependency to remember. It is also\n * redistribution, which is why `THIRD-PARTY.md` exists and is generated.\n */\nconst BUNDLED_GRAMMAR_DIR = new URL('../../grammars/', import.meta.url);\n\nconst require = createRequire(import.meta.url);\nconst cache = new Map<LanguageId, Language>();\nlet runtimeReady: Promise<void> | undefined;\n\n/**\n * Resolve a grammar to a path on this machine.\n *\n * The copy bundled in this package wins; `tree-sitter-wasms` is the fallback, for a\n * source checkout that has not run `pnpm build` yet. Note what this function does *not*\n * do: it never constructs a URL from a version string, a CDN base, or anything else.\n * Grammars come off disk — either the ones shipped here or the ones a package manager\n * already installed. A \"fetch the grammar on first use\" cache is the most natural way\n * to break Law 1 without noticing, because it works perfectly on the machine that\n * wrote it.\n */\nexport function grammarPath(language: LanguageId): string {\n const file = WASM_BY_LANGUAGE[language];\n\n const bundled = fileURLToPath(new URL(file, BUNDLED_GRAMMAR_DIR));\n if (existsSync(bundled)) return bundled;\n\n try {\n return require.resolve(`tree-sitter-wasms/out/${file}`);\n } catch {\n throw new GrammarUnavailableError(\n `smelt: no grammar for \"${language}\". The bundled copy is missing (run ` +\n `\\`pnpm build\\` in a source checkout) and \\`tree-sitter-wasms\\` is not installed ` +\n `either. Pass \\`language: 'unknown'\\` to use the lexical planner.`,\n );\n }\n}\n\n/**\n * Load a grammar, from disk, once.\n *\n * The bytes are read here and handed to tree-sitter as a `Uint8Array` rather than\n * passing it a path. `Language.load()` accepts `string | URL`, and a `URL` with an\n * `https:` scheme would make it fetch — inside the elision path, from a dependency's\n * ordinary happy path. Reading the file ourselves removes that capability instead of\n * documenting it, and {@link assertLocalResource} rejects a remote path before we get\n * that far.\n *\n * **Every failure here is a `GrammarUnavailableError`.** The consumer contract makes\n * exactly one promise about errors — every error smelt throws is an `instanceof\n * SmeltError` — and this function is on the path of both `smelt()` and\n * `buildRepoMap()`, for every file in a language smelt claims to parse. Only\n * *resolution* used to be inside the contract: `grammarPath` throws for a grammar it\n * cannot find, and {@link existsSync} then reports a file's **presence**, never its\n * readability. So a grammar that resolved and then would not load leaked the raw\n * error — `EACCES` from an unreadable `.wasm`, a V8 `CompileError` or `RangeError`\n * from a truncated one, a half-extracted tarball — straight past a caller's\n * documented `catch`. Each step is wrapped instead, naming the language and the path\n * and keeping the original as `cause`: bringing the failure inside the contract, not\n * hiding what Node or V8 said. A promise with one undocumented exception is no\n * promise at all.\n */\nexport async function loadGrammar(language: LanguageId): Promise<Language> {\n const cached = cache.get(language);\n if (cached !== undefined) return cached;\n\n const path = fileURLToPath(assertLocalResource(grammarPath(language)).href);\n\n const ready = (runtimeReady ??= Parser.init());\n await inContract(\n () => ready,\n `smelt: the tree-sitter WASM runtime would not start, so no grammar can be loaded`,\n );\n const bytes = await inContract(\n () => readFile(path),\n `smelt: the grammar for \"${language}\" could not be read from \"${path}\"`,\n );\n const grammar = await inContract(\n () => Language.load(new Uint8Array(bytes)),\n `smelt: the file at \"${path}\" is not a loadable tree-sitter grammar for \"${language}\"`,\n );\n cache.set(language, grammar);\n return grammar;\n}\n\n/**\n * Run one step of the load and keep its failure inside the consumer contract.\n *\n * A {@link SmeltError} passes through untouched — `assertLocalResource` and\n * `grammarPath` already refuse in smelt's own currency, and rewrapping would bury a\n * sentence written deliberately under a generic one.\n */\nasync function inContract<T>(run: () => Promise<T>, what: string): Promise<T> {\n try {\n return await run();\n } catch (error) {\n if (error instanceof SmeltError) throw error;\n throw new GrammarUnavailableError(`${what}: ${describeFailure(error)}.`, { cause: error });\n }\n}\n\n/**\n * What Node or V8 actually said. Never invented, never swallowed — the wasm cases\n * (`CompileError`, `RangeError`) carry no `errno`, and their message is the whole\n * diagnosis.\n */\nfunction describeFailure(cause: unknown): string {\n if (cause instanceof Error && cause.message !== '') return cause.message;\n return String(cause);\n}\n\n/** Reset the grammar cache. Tests use it; production has no reason to. */\nexport function clearGrammarCache(): void {\n cache.clear();\n runtimeReady = undefined;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lexical.d.ts","sourceRoot":"","sources":["../../src/plan/lexical.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAiB,SAAS,EAAkB,OAAO,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"lexical.d.ts","sourceRoot":"","sources":["../../src/plan/lexical.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAiB,SAAS,EAAkB,OAAO,EAAE,MAAM,aAAa,CAAC;AAIlG,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAK/C,MAAM,WAAW,qBAAqB;IACpC,yFAAyF;IACzF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAUD;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAe,YAAW,OAAO;;IAC5C,QAAQ,CAAC,EAAE,gBAAsB;IAGjC,YAAY,OAAO,GAAE,qBAA0B,EAE9C;IAED,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAE3C;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,qBAA0B,GAAG,WAAW,CAsC9F","sourcesContent":["import { MissingMarkerPricingError } from '../errors.ts';\nimport type { ElisionPlan, MarkerPricing, PlanInput, PlannedElision, Planner } from '../types.ts';\n\nimport { markerBytes, predictOutputBytes } from './budget.ts';\n\nexport const LEXICAL_PLANNER_ID = 'lexical/v1';\n\n/** How hard the head/tail strategy squeezes, in order, when the budget is not met. */\nconst HEAD_TAIL_LADDER: readonly number[] = [1, 0.5, 0.25, 0.1, 0.05];\n\nexport interface LexicalPlannerOptions {\n /** Lines of context kept either side of a focus match. Shrinks under budget pressure. */\n readonly contextLines?: number;\n /** Never collapse a run shorter than this, however tempting. */\n readonly minRunLines?: number;\n /** With no focus terms: lines kept at the top. */\n readonly headLines?: number;\n /** With no focus terms: lines kept at the bottom. */\n readonly tailLines?: number;\n /** Focus matching is substring, case-insensitive by default. */\n readonly caseSensitive?: boolean;\n}\n\ninterface Line {\n /** Byte offset of the first byte of the line. */\n readonly start: number;\n /** Byte offset one past the last byte of the line, *excluding* its newline. */\n readonly end: number;\n readonly text: string;\n}\n\n/**\n * The fallback planner, and the reference implementation of the whole pipeline.\n *\n * It knows nothing about syntax: it keeps the lines you were looking for plus a window\n * of context, and collapses the runs in between. That is a weak strategy on code and a\n * perfectly good one on log files, stack traces, JSON dumps and every other thing a\n * coding agent shovels into a prompt — which is why it is the fallback rather than an\n * embarrassment. It also runs on languages smelt has no grammar for, so\n * `language: 'unknown'` is never a dead end.\n *\n * Everything here is deterministic and rule-named. Same input, same plan, and every\n * elision can say which rule produced it.\n */\nexport class LexicalPlanner implements Planner {\n readonly id = LEXICAL_PLANNER_ID;\n readonly #options: LexicalPlannerOptions;\n\n constructor(options: LexicalPlannerOptions = {}) {\n this.#options = options;\n }\n\n plan(input: PlanInput): Promise<ElisionPlan> {\n return Promise.resolve(planLexical(input, this.#options));\n }\n}\n\n/**\n * The synchronous core, exported because it is worth testing and reusing directly.\n *\n * `budgetBytes` is a target, not a guarantee. If the smallest context window still\n * exceeds it, the plan comes back over budget rather than eliding the matches the\n * caller asked to keep — an optimizer that silently drops the thing you searched for is\n * the exact failure this design is built to prevent. Callers who need a hard ceiling\n * check `outputBytes` and decide; smelt will not decide for them.\n *\n * Profitability and budget prediction are **measured, not estimated**: every candidate\n * elision is priced through the input's {@link MarkerPricing} — the seam `apply.ts`\n * builds from the exact builder `applyPlan` will use, comment leader and custom\n * builder included. A guessed constant here once under-counted real ~105-byte markers\n * as 64, so a plan could be \"chosen as fitting\" and then come back over budget after\n * the markers landed.\n */\nexport function planLexical(input: PlanInput, options: LexicalPlannerOptions = {}): ElisionPlan {\n const pricing = requirePricing(input);\n const lines = splitLines(input.text);\n const focus = (input.focus ?? []).filter((term) => term.length > 0);\n const minRunLines = options.minRunLines ?? 3;\n\n const attempts: readonly (readonly PlannedElision[])[] =\n focus.length > 0\n ? ladder(options.contextLines ?? 4).map((context) =>\n collapse(lines, keepByFocus(lines, focus, context, options.caseSensitive ?? false), {\n minRunLines,\n rule: 'focus-window',\n pricing,\n }),\n )\n : HEAD_TAIL_LADDER.map((shrink) =>\n collapse(\n lines,\n keepByHeadTail(\n lines,\n Math.max(3, Math.round((options.headLines ?? 40) * shrink)),\n Math.max(3, Math.round((options.tailLines ?? 20) * shrink)),\n ),\n { minRunLines, rule: 'head-tail', pricing },\n ),\n );\n\n const inputBytes = Buffer.byteLength(input.text, 'utf8');\n const chosen =\n attempts.find(\n (elisions) => predictOutputBytes(inputBytes, elisions, pricing) <= input.budgetBytes,\n ) ?? attempts[attempts.length - 1]!;\n\n return {\n planner: LEXICAL_PLANNER_ID,\n language: input.language,\n elisions: chosen,\n };\n}\n\n/** Context-window sizes to try, largest first. */\nfunction ladder(start: number): readonly number[] {\n const sizes: number[] = [];\n for (let n = start; n >= 0; n -= 1) sizes.push(n);\n if (sizes.length === 0) sizes.push(0);\n return sizes;\n}\n\n/**\n * The runtime backstop for JS callers: TypeScript makes `pricing` required, but a JS\n * caller can omit it, and the honest answer is a named refusal rather than a planner\n * quietly pricing markers itself — the inversion the seam removed.\n */\nfunction requirePricing(input: PlanInput): MarkerPricing {\n const pricing: MarkerPricing | undefined = input.pricing;\n if (pricing === undefined) throw new MissingMarkerPricingError(LEXICAL_PLANNER_ID);\n return pricing;\n}\n\nfunction splitLines(text: string): readonly Line[] {\n const lines: Line[] = [];\n let start = 0;\n let byte = 0;\n const raw = text.split('\\n');\n for (let i = 0; i < raw.length; i += 1) {\n const content = raw[i]!;\n const contentBytes = Buffer.byteLength(content, 'utf8');\n byte = start + contentBytes;\n lines.push({ start, end: byte, text: content });\n // +1 for the '\\n' we split on; the final fragment has none.\n start = byte + 1;\n }\n return lines;\n}\n\nfunction keepByFocus(\n lines: readonly Line[],\n focus: readonly string[],\n contextLines: number,\n caseSensitive: boolean,\n): boolean[] {\n const needles = caseSensitive ? focus : focus.map((t) => t.toLowerCase());\n const keep: boolean[] = Array.from({ length: lines.length }, () => false);\n\n for (let i = 0; i < lines.length; i += 1) {\n const haystack = caseSensitive ? lines[i]!.text : lines[i]!.text.toLowerCase();\n if (!needles.some((needle) => haystack.includes(needle))) continue;\n const from = Math.max(0, i - contextLines);\n const to = Math.min(lines.length - 1, i + contextLines);\n for (let j = from; j <= to; j += 1) keep[j] = true;\n }\n return keep;\n}\n\nfunction keepByHeadTail(lines: readonly Line[], head: number, tail: number): boolean[] {\n const keep: boolean[] = Array.from({ length: lines.length }, () => false);\n for (let i = 0; i < Math.min(head, lines.length); i += 1) keep[i] = true;\n for (let i = Math.max(0, lines.length - tail); i < lines.length; i += 1) keep[i] = true;\n return keep;\n}\n\nfunction collapse(\n lines: readonly Line[],\n keep: readonly boolean[],\n config: { readonly minRunLines: number; readonly rule: string; readonly pricing: MarkerPricing },\n): readonly PlannedElision[] {\n const elisions: PlannedElision[] = [];\n let runStart = -1;\n\n const flush = (endExclusive: number): void => {\n if (runStart < 0) return;\n const count = endExclusive - runStart;\n const range = { start: lines[runStart]!.start, end: lines[endExclusive - 1]!.end };\n runStart = -1;\n if (count < config.minRunLines) return;\n const candidate: PlannedElision = {\n range,\n reason: {\n rule: config.rule,\n explanation: `collapsed ${String(count)} ${count === 1 ? 'line' : 'lines'}${describe(\n config.rule,\n )}`,\n },\n };\n // Profitability, measured rather than estimated: a marker that costs at least as\n // many bytes as it removes grows the output — same rule, same seam, as the\n // structural planner.\n if (range.end - range.start <= markerBytes(candidate, config.pricing)) return;\n elisions.push(candidate);\n };\n\n for (let i = 0; i < lines.length; i += 1) {\n if (keep[i] === true) {\n flush(i);\n } else if (runStart < 0) {\n runStart = i;\n }\n }\n flush(lines.length);\n return elisions;\n}\n\nfunction describe(rule: string): string {\n return rule === 'focus-window' ? ' with no match for the focus terms' : ' from the middle';\n}\n"]}
|
package/dist/plan/lexical.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MissingMarkerPricingError } from '../errors.js';
|
|
2
|
+
import { markerBytes, predictOutputBytes } from './budget.js';
|
|
2
3
|
export const LEXICAL_PLANNER_ID = 'lexical/v1';
|
|
3
4
|
/** How hard the head/tail strategy squeezes, in order, when the budget is not met. */
|
|
4
5
|
const HEAD_TAIL_LADDER = [1, 0.5, 0.25, 0.1, 0.05];
|
|
@@ -81,13 +82,6 @@ function requirePricing(input) {
|
|
|
81
82
|
throw new MissingMarkerPricingError(LEXICAL_PLANNER_ID);
|
|
82
83
|
return pricing;
|
|
83
84
|
}
|
|
84
|
-
/** The exact UTF-8 cost of the marker this elision would earn. Asked, not guessed. */
|
|
85
|
-
function markerBytes(elision, pricing) {
|
|
86
|
-
return pricing.costBytes(elision.reason, elision.range.end - elision.range.start);
|
|
87
|
-
}
|
|
88
|
-
function predictOutputBytes(inputBytes, elisions, pricing) {
|
|
89
|
-
return elisions.reduce((bytes, elision) => bytes - (elision.range.end - elision.range.start) + markerBytes(elision, pricing), inputBytes);
|
|
90
|
-
}
|
|
91
85
|
function splitLines(text) {
|
|
92
86
|
const lines = [];
|
|
93
87
|
let start = 0;
|
package/dist/plan/lexical.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lexical.js","sourceRoot":"","sources":["../../src/plan/lexical.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lexical.js","sourceRoot":"","sources":["../../src/plan/lexical.ts"],"names":[],"mappings":"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","sourcesContent":["import { MissingMarkerPricingError } from '../errors.ts';\nimport type { ElisionPlan, MarkerPricing, PlanInput, PlannedElision, Planner } from '../types.ts';\n\nimport { markerBytes, predictOutputBytes } from './budget.ts';\n\nexport const LEXICAL_PLANNER_ID = 'lexical/v1';\n\n/** How hard the head/tail strategy squeezes, in order, when the budget is not met. */\nconst HEAD_TAIL_LADDER: readonly number[] = [1, 0.5, 0.25, 0.1, 0.05];\n\nexport interface LexicalPlannerOptions {\n /** Lines of context kept either side of a focus match. Shrinks under budget pressure. */\n readonly contextLines?: number;\n /** Never collapse a run shorter than this, however tempting. */\n readonly minRunLines?: number;\n /** With no focus terms: lines kept at the top. */\n readonly headLines?: number;\n /** With no focus terms: lines kept at the bottom. */\n readonly tailLines?: number;\n /** Focus matching is substring, case-insensitive by default. */\n readonly caseSensitive?: boolean;\n}\n\ninterface Line {\n /** Byte offset of the first byte of the line. */\n readonly start: number;\n /** Byte offset one past the last byte of the line, *excluding* its newline. */\n readonly end: number;\n readonly text: string;\n}\n\n/**\n * The fallback planner, and the reference implementation of the whole pipeline.\n *\n * It knows nothing about syntax: it keeps the lines you were looking for plus a window\n * of context, and collapses the runs in between. That is a weak strategy on code and a\n * perfectly good one on log files, stack traces, JSON dumps and every other thing a\n * coding agent shovels into a prompt — which is why it is the fallback rather than an\n * embarrassment. It also runs on languages smelt has no grammar for, so\n * `language: 'unknown'` is never a dead end.\n *\n * Everything here is deterministic and rule-named. Same input, same plan, and every\n * elision can say which rule produced it.\n */\nexport class LexicalPlanner implements Planner {\n readonly id = LEXICAL_PLANNER_ID;\n readonly #options: LexicalPlannerOptions;\n\n constructor(options: LexicalPlannerOptions = {}) {\n this.#options = options;\n }\n\n plan(input: PlanInput): Promise<ElisionPlan> {\n return Promise.resolve(planLexical(input, this.#options));\n }\n}\n\n/**\n * The synchronous core, exported because it is worth testing and reusing directly.\n *\n * `budgetBytes` is a target, not a guarantee. If the smallest context window still\n * exceeds it, the plan comes back over budget rather than eliding the matches the\n * caller asked to keep — an optimizer that silently drops the thing you searched for is\n * the exact failure this design is built to prevent. Callers who need a hard ceiling\n * check `outputBytes` and decide; smelt will not decide for them.\n *\n * Profitability and budget prediction are **measured, not estimated**: every candidate\n * elision is priced through the input's {@link MarkerPricing} — the seam `apply.ts`\n * builds from the exact builder `applyPlan` will use, comment leader and custom\n * builder included. A guessed constant here once under-counted real ~105-byte markers\n * as 64, so a plan could be \"chosen as fitting\" and then come back over budget after\n * the markers landed.\n */\nexport function planLexical(input: PlanInput, options: LexicalPlannerOptions = {}): ElisionPlan {\n const pricing = requirePricing(input);\n const lines = splitLines(input.text);\n const focus = (input.focus ?? []).filter((term) => term.length > 0);\n const minRunLines = options.minRunLines ?? 3;\n\n const attempts: readonly (readonly PlannedElision[])[] =\n focus.length > 0\n ? ladder(options.contextLines ?? 4).map((context) =>\n collapse(lines, keepByFocus(lines, focus, context, options.caseSensitive ?? false), {\n minRunLines,\n rule: 'focus-window',\n pricing,\n }),\n )\n : HEAD_TAIL_LADDER.map((shrink) =>\n collapse(\n lines,\n keepByHeadTail(\n lines,\n Math.max(3, Math.round((options.headLines ?? 40) * shrink)),\n Math.max(3, Math.round((options.tailLines ?? 20) * shrink)),\n ),\n { minRunLines, rule: 'head-tail', pricing },\n ),\n );\n\n const inputBytes = Buffer.byteLength(input.text, 'utf8');\n const chosen =\n attempts.find(\n (elisions) => predictOutputBytes(inputBytes, elisions, pricing) <= input.budgetBytes,\n ) ?? attempts[attempts.length - 1]!;\n\n return {\n planner: LEXICAL_PLANNER_ID,\n language: input.language,\n elisions: chosen,\n };\n}\n\n/** Context-window sizes to try, largest first. */\nfunction ladder(start: number): readonly number[] {\n const sizes: number[] = [];\n for (let n = start; n >= 0; n -= 1) sizes.push(n);\n if (sizes.length === 0) sizes.push(0);\n return sizes;\n}\n\n/**\n * The runtime backstop for JS callers: TypeScript makes `pricing` required, but a JS\n * caller can omit it, and the honest answer is a named refusal rather than a planner\n * quietly pricing markers itself — the inversion the seam removed.\n */\nfunction requirePricing(input: PlanInput): MarkerPricing {\n const pricing: MarkerPricing | undefined = input.pricing;\n if (pricing === undefined) throw new MissingMarkerPricingError(LEXICAL_PLANNER_ID);\n return pricing;\n}\n\nfunction splitLines(text: string): readonly Line[] {\n const lines: Line[] = [];\n let start = 0;\n let byte = 0;\n const raw = text.split('\\n');\n for (let i = 0; i < raw.length; i += 1) {\n const content = raw[i]!;\n const contentBytes = Buffer.byteLength(content, 'utf8');\n byte = start + contentBytes;\n lines.push({ start, end: byte, text: content });\n // +1 for the '\\n' we split on; the final fragment has none.\n start = byte + 1;\n }\n return lines;\n}\n\nfunction keepByFocus(\n lines: readonly Line[],\n focus: readonly string[],\n contextLines: number,\n caseSensitive: boolean,\n): boolean[] {\n const needles = caseSensitive ? focus : focus.map((t) => t.toLowerCase());\n const keep: boolean[] = Array.from({ length: lines.length }, () => false);\n\n for (let i = 0; i < lines.length; i += 1) {\n const haystack = caseSensitive ? lines[i]!.text : lines[i]!.text.toLowerCase();\n if (!needles.some((needle) => haystack.includes(needle))) continue;\n const from = Math.max(0, i - contextLines);\n const to = Math.min(lines.length - 1, i + contextLines);\n for (let j = from; j <= to; j += 1) keep[j] = true;\n }\n return keep;\n}\n\nfunction keepByHeadTail(lines: readonly Line[], head: number, tail: number): boolean[] {\n const keep: boolean[] = Array.from({ length: lines.length }, () => false);\n for (let i = 0; i < Math.min(head, lines.length); i += 1) keep[i] = true;\n for (let i = Math.max(0, lines.length - tail); i < lines.length; i += 1) keep[i] = true;\n return keep;\n}\n\nfunction collapse(\n lines: readonly Line[],\n keep: readonly boolean[],\n config: { readonly minRunLines: number; readonly rule: string; readonly pricing: MarkerPricing },\n): readonly PlannedElision[] {\n const elisions: PlannedElision[] = [];\n let runStart = -1;\n\n const flush = (endExclusive: number): void => {\n if (runStart < 0) return;\n const count = endExclusive - runStart;\n const range = { start: lines[runStart]!.start, end: lines[endExclusive - 1]!.end };\n runStart = -1;\n if (count < config.minRunLines) return;\n const candidate: PlannedElision = {\n range,\n reason: {\n rule: config.rule,\n explanation: `collapsed ${String(count)} ${count === 1 ? 'line' : 'lines'}${describe(\n config.rule,\n )}`,\n },\n };\n // Profitability, measured rather than estimated: a marker that costs at least as\n // many bytes as it removes grows the output — same rule, same seam, as the\n // structural planner.\n if (range.end - range.start <= markerBytes(candidate, config.pricing)) return;\n elisions.push(candidate);\n };\n\n for (let i = 0; i < lines.length; i += 1) {\n if (keep[i] === true) {\n flush(i);\n } else if (runStart < 0) {\n runStart = i;\n }\n }\n flush(lines.length);\n return elisions;\n}\n\nfunction describe(rule: string): string {\n return rule === 'focus-window' ? ' with no match for the focus terms' : ' from the middle';\n}\n"]}
|
package/dist/plan/planners.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { StructuralPlannerOptions } from './structural.ts';
|
|
|
4
4
|
/**
|
|
5
5
|
* The option bags a strategy factory may draw from — the same fields
|
|
6
6
|
* `SmelterConfig` carries, so the config object itself can be handed to a factory.
|
|
7
|
+
* `auto` draws from both, because it may run either planner.
|
|
7
8
|
*/
|
|
8
9
|
export interface PlannerFactoryOptions {
|
|
9
10
|
readonly lexical?: LexicalPlannerOptions;
|
|
@@ -13,23 +14,55 @@ export interface PlannerFactoryOptions {
|
|
|
13
14
|
* The one registry of planner strategies — string in, constructed {@link Planner} out.
|
|
14
15
|
*
|
|
15
16
|
* This object is the single place the strategy names live. `createSmelter` builds from
|
|
16
|
-
* it, `--strategy` and `smelt.config.json` validation accept exactly its keys,
|
|
17
|
-
* `--help` text
|
|
17
|
+
* it, `--strategy` and `smelt.config.json` validation accept exactly its keys, the
|
|
18
|
+
* `--help` text and the `init` wizard render its keys, and the `smelt_file` tool's
|
|
19
|
+
* JSON Schema enumerates them — so a strategy cannot exist in one of those faces and
|
|
18
20
|
* be missing from another. Before this registry the pair was restated in three places,
|
|
19
21
|
* which is how help text rots.
|
|
20
22
|
*
|
|
21
23
|
* `'structural'` parses every language named in {@link STRUCTURAL_LANGUAGES} with a
|
|
22
24
|
* bundled grammar and throws {@link GrammarUnavailableError} for anything else — never
|
|
23
25
|
* a silent lexical fallback. See {@link StructuralPlanner}.
|
|
26
|
+
*
|
|
27
|
+
* `'auto'` picks between the two on the language and **labels what ran**: its plans
|
|
28
|
+
* come back as `lexical/v1` or `structural/v1`, never as `auto`. It is a selector, not
|
|
29
|
+
* a fallback — an explicit `'structural'` on an unsupported language still refuses,
|
|
30
|
+
* because a caller who named the planner asked for its guarantees. See
|
|
31
|
+
* {@link AutoPlanner}, whose doc comment carries the reasoning.
|
|
32
|
+
*
|
|
33
|
+
* Key order is the order every rendered list uses, so append rather than reorder.
|
|
24
34
|
*/
|
|
25
35
|
export declare const PLANNERS: {
|
|
26
36
|
readonly lexical: (options: PlannerFactoryOptions) => Planner;
|
|
27
37
|
readonly structural: (options: PlannerFactoryOptions) => Planner;
|
|
38
|
+
readonly auto: (options: PlannerFactoryOptions) => Planner;
|
|
28
39
|
};
|
|
29
40
|
/** Which planner a smelter uses, named by string. Exactly the keys of {@link PLANNERS}. */
|
|
30
41
|
export type Strategy = keyof typeof PLANNERS;
|
|
31
42
|
/** The registry's keys, in declaration order, for help text and error messages. */
|
|
32
43
|
export declare const STRATEGIES: readonly Strategy[];
|
|
44
|
+
/**
|
|
45
|
+
* The strategy a caller who names none gets — the registry's own default, beside the
|
|
46
|
+
* names it defaults among.
|
|
47
|
+
*
|
|
48
|
+
* Every `?? 'lexical'` in this repository reads this constant instead: `createSmelter`,
|
|
49
|
+
* the `smelt` verb's merge, the `init` wizard's starting choice, and the MCP server's
|
|
50
|
+
* `smelt_file`. The names were already derived from {@link PLANNERS} while the default
|
|
51
|
+
* stayed hand-typed in four places across two packages — so the one fact every caller
|
|
52
|
+
* needs was the one the registry did not carry, and a changed default would have moved
|
|
53
|
+
* on some faces and not others.
|
|
54
|
+
*
|
|
55
|
+
* `'lexical'` is the default because it works on any text: `'structural'` refuses a
|
|
56
|
+
* language it has no grammar for rather than approximating (see {@link StructuralPlanner}),
|
|
57
|
+
* which is right when a caller asked for it and wrong as the answer to "no preference".
|
|
58
|
+
*
|
|
59
|
+
* `'auto'` refuses nothing either, and is the better answer for a caller smelting a
|
|
60
|
+
* mixed stream — but it stays **opt-in**. Promoting it would change which planner runs,
|
|
61
|
+
* and therefore what `result.planner` says, for every existing caller who never named a
|
|
62
|
+
* strategy: a behaviour change delivered to people who asked for nothing. A caller who
|
|
63
|
+
* wants it says so, in a flag, a config, or a tool argument.
|
|
64
|
+
*/
|
|
65
|
+
export declare const DEFAULT_STRATEGY: Strategy;
|
|
33
66
|
/** The one membership test `--strategy` and config validation both use. */
|
|
34
67
|
export declare function isStrategy(value: string): value is Strategy;
|
|
35
68
|
//# sourceMappingURL=planners.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planners.d.ts","sourceRoot":"","sources":["../../src/plan/planners.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"planners.d.ts","sourceRoot":"","sources":["../../src/plan/planners.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC;IACzC,QAAQ,CAAC,UAAU,CAAC,EAAE,wBAAwB,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,QAAQ;gCACA,qBAAqB,KAAG,OAAO;mCAC5B,qBAAqB,KAAG,OAAO;6BAErC,qBAAqB,KAAG,OAAO;CAC+B,CAAC;AAEjF,2FAA2F;AAC3F,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,QAAQ,CAAC;AAE7C,mFAAmF;AACnF,eAAO,MAAM,UAAU,EAA4B,SAAS,QAAQ,EAAE,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAoB,CAAC;AAEpD,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,QAAQ,CAE3D","sourcesContent":["import type { Planner } from '../types.ts';\n\nimport { AutoPlanner } from './auto.ts';\nimport { LexicalPlanner } from './lexical.ts';\nimport type { LexicalPlannerOptions } from './lexical.ts';\nimport { StructuralPlanner } from './structural.ts';\nimport type { StructuralPlannerOptions } from './structural.ts';\n\n/**\n * The option bags a strategy factory may draw from — the same fields\n * `SmelterConfig` carries, so the config object itself can be handed to a factory.\n * `auto` draws from both, because it may run either planner.\n */\nexport interface PlannerFactoryOptions {\n readonly lexical?: LexicalPlannerOptions;\n readonly structural?: StructuralPlannerOptions;\n}\n\n/**\n * The one registry of planner strategies — string in, constructed {@link Planner} out.\n *\n * This object is the single place the strategy names live. `createSmelter` builds from\n * it, `--strategy` and `smelt.config.json` validation accept exactly its keys, the\n * `--help` text and the `init` wizard render its keys, and the `smelt_file` tool's\n * JSON Schema enumerates them — so a strategy cannot exist in one of those faces and\n * be missing from another. Before this registry the pair was restated in three places,\n * which is how help text rots.\n *\n * `'structural'` parses every language named in {@link STRUCTURAL_LANGUAGES} with a\n * bundled grammar and throws {@link GrammarUnavailableError} for anything else — never\n * a silent lexical fallback. See {@link StructuralPlanner}.\n *\n * `'auto'` picks between the two on the language and **labels what ran**: its plans\n * come back as `lexical/v1` or `structural/v1`, never as `auto`. It is a selector, not\n * a fallback — an explicit `'structural'` on an unsupported language still refuses,\n * because a caller who named the planner asked for its guarantees. See\n * {@link AutoPlanner}, whose doc comment carries the reasoning.\n *\n * Key order is the order every rendered list uses, so append rather than reorder.\n */\nexport const PLANNERS = {\n lexical: (options: PlannerFactoryOptions): Planner => new LexicalPlanner(options.lexical ?? {}),\n structural: (options: PlannerFactoryOptions): Planner =>\n new StructuralPlanner(options.structural ?? {}),\n auto: (options: PlannerFactoryOptions): Planner => new AutoPlanner(options),\n} as const satisfies Record<string, (options: PlannerFactoryOptions) => Planner>;\n\n/** Which planner a smelter uses, named by string. Exactly the keys of {@link PLANNERS}. */\nexport type Strategy = keyof typeof PLANNERS;\n\n/** The registry's keys, in declaration order, for help text and error messages. */\nexport const STRATEGIES = Object.keys(PLANNERS) as readonly Strategy[];\n\n/**\n * The strategy a caller who names none gets — the registry's own default, beside the\n * names it defaults among.\n *\n * Every `?? 'lexical'` in this repository reads this constant instead: `createSmelter`,\n * the `smelt` verb's merge, the `init` wizard's starting choice, and the MCP server's\n * `smelt_file`. The names were already derived from {@link PLANNERS} while the default\n * stayed hand-typed in four places across two packages — so the one fact every caller\n * needs was the one the registry did not carry, and a changed default would have moved\n * on some faces and not others.\n *\n * `'lexical'` is the default because it works on any text: `'structural'` refuses a\n * language it has no grammar for rather than approximating (see {@link StructuralPlanner}),\n * which is right when a caller asked for it and wrong as the answer to \"no preference\".\n *\n * `'auto'` refuses nothing either, and is the better answer for a caller smelting a\n * mixed stream — but it stays **opt-in**. Promoting it would change which planner runs,\n * and therefore what `result.planner` says, for every existing caller who never named a\n * strategy: a behaviour change delivered to people who asked for nothing. A caller who\n * wants it says so, in a flag, a config, or a tool argument.\n */\nexport const DEFAULT_STRATEGY: Strategy = 'lexical';\n\n/** The one membership test `--strategy` and config validation both use. */\nexport function isStrategy(value: string): value is Strategy {\n return Object.hasOwn(PLANNERS, value);\n}\n"]}
|