@smeltjs/core 0.3.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/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/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 +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config.d.ts +20 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +34 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/hooks.d.ts +25 -16
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +71 -178
- package/dist/cli/hooks.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 +140 -2
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/run.d.ts +5 -3
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +2 -1
- package/dist/cli/run.js.map +1 -1
- 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 +3 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -1
- package/dist/cli/subcommands/flags.js +13 -1
- package/dist/cli/subcommands/flags.js.map +1 -1
- package/dist/cli/subcommands/hooks.d.ts.map +1 -1
- package/dist/cli/subcommands/hooks.js +19 -3
- package/dist/cli/subcommands/hooks.js.map +1 -1
- package/dist/cli/subcommands/registry.d.ts +2 -1
- package/dist/cli/subcommands/registry.d.ts.map +1 -1
- package/dist/cli/subcommands/registry.js +3 -0
- package/dist/cli/subcommands/registry.js.map +1 -1
- package/dist/cli/subcommands/subcommand.d.ts +1 -1
- package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
- package/dist/cli/subcommands/subcommand.js.map +1 -1
- package/dist/harness/codex.d.ts.map +1 -1
- package/dist/harness/codex.js +1 -0
- package/dist/harness/codex.js.map +1 -1
- package/dist/harness/gemini.d.ts.map +1 -1
- package/dist/harness/gemini.js +1 -0
- package/dist/harness/gemini.js.map +1 -1
- package/dist/harness/grok.d.ts.map +1 -1
- package/dist/harness/grok.js +1 -0
- package/dist/harness/grok.js.map +1 -1
- package/dist/harness/hermes.d.ts.map +1 -1
- package/dist/harness/hermes.js +1 -0
- package/dist/harness/hermes.js.map +1 -1
- package/dist/harness/profile.d.ts +26 -1
- package/dist/harness/profile.d.ts.map +1 -1
- package/dist/harness/profile.js +15 -0
- package/dist/harness/profile.js.map +1 -1
- package/dist/harness/registry.d.ts +55 -6
- package/dist/harness/registry.d.ts.map +1 -1
- package/dist/harness/registry.js +63 -6
- package/dist/harness/registry.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.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 +0 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25,6 +25,14 @@ export { CONFIG_FILE_NAME, CONFIG_VERSION, configuredStore, findConfigFile, load
|
|
|
25
25
|
export { MEASURE_STUB_FILE, measureStubSource, RERANK_STUB_FILE, rerankStubSource, runInit, } from './cli/init.js';
|
|
26
26
|
export { retrieveStats } from './stats.js';
|
|
27
27
|
export { LANGUAGE_PROFILES, profileFor, profileForPath, structuralLanguages, } from './lang/registry.js';
|
|
28
|
+
/**
|
|
29
|
+
* The harness registry's rendered views. Public for the same reason the ops seam is:
|
|
30
|
+
* something outside this package renders them — the site's `facts.json` generator —
|
|
31
|
+
* and the alternative is a second copy of the tier table typed into a React component,
|
|
32
|
+
* which is exactly the drift `harnessesByTier()` exists to end.
|
|
33
|
+
*/
|
|
34
|
+
export { harnessesByTier, harnessNames, HARNESSES, HARNESS_IDS } from './harness/registry.js';
|
|
35
|
+
export { harnessLabel, HARNESS_TIERS, TIER_HONESTY } from './harness/profile.js';
|
|
28
36
|
export { resolveRun } from './cli/subcommands/smelt.js';
|
|
29
37
|
export { REPO_MAP_FOCUS_RULE } from './repomap/map.js';
|
|
30
38
|
export { CLI_MAP_JSON_FORMAT, formatMapReport, resolveMapRun } from './cli/run.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ApplyOptions, MarkerBuilder, MarkerInfo } from './apply.ts';\n\n/**\n * The public surface: a barrel over the modules that hold the reasoning.\n *\n * `createSmelter()` itself lives in `./smelter.ts` so that nothing inside `src/` has\n * to import this file to build a smelter — the CLI's default verb does exactly that,\n * and a barrel that imports the CLI which imports the barrel is a cycle whose only\n * symptom is a registry evaluating to `undefined` in whichever module the loader\n * entered first. Every name is re-exported here, so consumers see no difference.\n */\n\nexport type { ApplyOptions, MarkerBuilder, MarkerInfo };\nexport {\n applyPlan,\n defaultMarker,\n MARKER_FORMAT_VERSION,\n markerPricing,\n reconstruct,\n} from './apply.ts';\nexport { detectLanguage, SUPPORTED_LANGUAGES } from './detect.ts';\nexport * from './errors.ts';\nexport { contentHash, HASH_LENGTH } from './hash.ts';\nexport {\n ALLOWED_NODE_BUILTINS,\n ALLOWED_PACKAGES,\n ALLOWED_URL_SCHEMES,\n assertLocalResource,\n FORBIDDEN_GLOBALS,\n FORBIDDEN_NODE_MODULES,\n FORBIDDEN_PACKAGES,\n} from './net/policy.ts';\nexport type { LocalResource } from './net/policy.ts';\nexport { clearGrammarCache, grammarPath, loadGrammar, WASM_BY_LANGUAGE } from './plan/grammar.ts';\nexport { AUTO_PLANNER_ID, AutoPlanner, planAuto } from './plan/auto.ts';\nexport type { AutoPlannerOptions } from './plan/auto.ts';\nexport { LEXICAL_PLANNER_ID, LexicalPlanner, planLexical } from './plan/lexical.ts';\nexport type { LexicalPlannerOptions } from './plan/lexical.ts';\nexport { DEFAULT_STRATEGY, isStrategy, PLANNERS, STRATEGIES } from './plan/planners.ts';\nexport type { PlannerFactoryOptions } from './plan/planners.ts';\nexport {\n isStructuralLanguage,\n planStructural,\n STRUCTURAL_LANGUAGES,\n STRUCTURAL_PLANNER_ID,\n StructuralPlanner,\n} from './plan/structural.ts';\nexport type { StructuralLanguage, StructuralPlannerOptions } from './plan/structural.ts';\nexport { createRetrieveTool, RETRIEVE_TOOL_NAME } from './retrieve.ts';\nexport { unconfiguredDistillStage, unconfiguredRerankStage } from './stages.ts';\nexport { MemoryElisionStore } from './store.ts';\nexport {\n DIRECTORY_STORE_FORMAT,\n DIRECTORY_STORE_VERSION,\n DirectoryElisionStore,\n} from './store-dir.ts';\nexport type { DirectoryElisionStoreOptions } from './store-dir.ts';\nexport * from './types.ts';\nexport {\n CLI_JSON_FORMAT,\n CLI_NAME,\n cliUsage,\n EXIT,\n formatReport,\n parseSmeltArgs,\n runCli,\n} from './cli/run.ts';\nexport type { AnswerStream, CliIo, CliJsonEnvelope, SmeltInvocation } from './cli/run.ts';\nexport {\n ANTHROPIC_PROMPT_CACHE_FACTS,\n CACHE_BREAKER_RULES,\n detectCacheBreakers,\n findPrefixDivergence,\n} from './cache/prefix.ts';\nexport type {\n CacheWarning,\n PrefixDivergence,\n PromptStructure,\n PromptTool,\n} from './cache/prefix.ts';\nexport { MARKER_LINE_COMMENT_LEADERS, markerForLanguage } from './apply.ts';\nexport {\n buildRepoMap,\n DEFAULT_REPO_IGNORE,\n REPO_MAP_CACHE_CORRUPT_RULE,\n REPO_MAP_ID,\n REPO_MAP_PATH_ONLY_RULE,\n REPO_MAP_RANKED_RULE,\n REPO_MAP_UNREFERENCED_RULE,\n} from './repomap/map.ts';\nexport type {\n RepoMap,\n RepoMapCacheCounts,\n RepoMapEntry,\n RepoMapOptions,\n RepoMapPathEntry,\n RepoMapReason,\n RepoMapWarning,\n} from './repomap/map.ts';\nexport { PAGERANK_DAMPING, PAGERANK_ITERATIONS, rankDefinitions } from './repomap/rank.ts';\nexport type { FileTagsEntry, RankedDefinition } from './repomap/rank.ts';\nexport { extractTags } from './repomap/tags.ts';\nexport type { DefinitionTag, FileTags, ReferenceTag } from './repomap/tags.ts';\nexport { TAGS_CACHE_FORMAT, TAGS_CACHE_VERSION, tagsCacheKey } from './repomap/cache.ts';\nexport { nodeFsReader } from './repomap/reader.ts';\nexport type { DirEntry, FileStat, RepoReader } from './repomap/reader.ts';\nexport {\n CONFIG_FILE_NAME,\n CONFIG_VERSION,\n configuredStore,\n findConfigFile,\n loadNearestConfig,\n parseConfig,\n renderConfig,\n resolveStorePath,\n} from './cli/config.ts';\nexport type { ConfiguredStore, LoadedConfig, SmeltConfig, SmeltConfigStore } from './cli/config.ts';\nexport {\n MEASURE_STUB_FILE,\n measureStubSource,\n RERANK_STUB_FILE,\n rerankStubSource,\n runInit,\n} from './cli/init.ts';\nexport type { InitIo } from './cli/init.ts';\nexport { retrieveStats } from './stats.ts';\nexport type { RawRetrieveCounters } from './stats.ts';\nexport {\n LANGUAGE_PROFILES,\n profileFor,\n profileForPath,\n structuralLanguages,\n} from './lang/registry.ts';\nexport type { LanguageProfile, LanguageStructure, RepoMapFacts } from './lang/profile.ts';\nexport { resolveRun } from './cli/subcommands/smelt.ts';\nexport type { ResolvedRun } from './cli/subcommands/smelt.ts';\nexport { REPO_MAP_FOCUS_RULE } from './repomap/map.ts';\nexport { CLI_MAP_JSON_FORMAT, formatMapReport, resolveMapRun } from './cli/run.ts';\nexport type { CliInvocation, CliMapJsonEnvelope, MapInvocation } from './cli/run.ts';\nexport type { MapReportInput } from './cli/report.ts';\nexport type { ResolvedMapRun } from './cli/subcommands/map.ts';\n\n/**\n * The operations seam — the four verbs and the laws their inputs must satisfy, below\n * every front door. `@smeltjs/mcp` consumes these as an ordinary dependency, so the\n * `smelt` CLI and the MCP tools run the same middle instead of two copies of it. See\n * `src/ops/index.ts` for what belongs here and what stays in an adapter.\n */\nexport {\n budgetFault,\n budgetMalformed,\n budgetRequired,\n mapTree,\n openStore,\n readBlob,\n readCounters,\n readTree,\n resolveStrategy,\n retrieveBytes,\n smeltBlob,\n} from './ops/index.ts';\nexport type {\n BudgetFault,\n BudgetNaming,\n MapTreeOp,\n ReadCountersOp,\n ResolvedStrategy,\n RetrieveBytesOp,\n Ruling,\n SmeltBlobOp,\n SmeltBlobOutcome,\n StrategySource,\n TreeNaming,\n} from './ops/index.ts';\n\n/**\n * Which planner a smelter uses, named by string. The names, their factories, and this\n * type all come from the one {@link PLANNERS} registry in `src/plan/planners.ts`, so\n * the CLI's validation and help text cannot drift from what `createSmelter` builds.\n */\nexport type { Strategy } from './plan/planners.ts';\n\nexport { createSmelter } from './smelter.ts';\nexport type { Smelter, SmelterConfig, SmeltCallOptions } from './smelter.ts';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ApplyOptions, MarkerBuilder, MarkerInfo } from './apply.ts';\n\n/**\n * The public surface: a barrel over the modules that hold the reasoning.\n *\n * `createSmelter()` itself lives in `./smelter.ts` so that nothing inside `src/` has\n * to import this file to build a smelter — the CLI's default verb does exactly that,\n * and a barrel that imports the CLI which imports the barrel is a cycle whose only\n * symptom is a registry evaluating to `undefined` in whichever module the loader\n * entered first. Every name is re-exported here, so consumers see no difference.\n */\n\nexport type { ApplyOptions, MarkerBuilder, MarkerInfo };\nexport {\n applyPlan,\n defaultMarker,\n MARKER_FORMAT_VERSION,\n markerPricing,\n reconstruct,\n} from './apply.ts';\nexport { detectLanguage, SUPPORTED_LANGUAGES } from './detect.ts';\nexport * from './errors.ts';\nexport { contentHash, HASH_LENGTH } from './hash.ts';\nexport {\n ALLOWED_NODE_BUILTINS,\n ALLOWED_PACKAGES,\n ALLOWED_URL_SCHEMES,\n assertLocalResource,\n FORBIDDEN_GLOBALS,\n FORBIDDEN_NODE_MODULES,\n FORBIDDEN_PACKAGES,\n} from './net/policy.ts';\nexport type { LocalResource } from './net/policy.ts';\nexport { clearGrammarCache, grammarPath, loadGrammar, WASM_BY_LANGUAGE } from './plan/grammar.ts';\nexport { AUTO_PLANNER_ID, AutoPlanner, planAuto } from './plan/auto.ts';\nexport type { AutoPlannerOptions } from './plan/auto.ts';\nexport { LEXICAL_PLANNER_ID, LexicalPlanner, planLexical } from './plan/lexical.ts';\nexport type { LexicalPlannerOptions } from './plan/lexical.ts';\nexport { DEFAULT_STRATEGY, isStrategy, PLANNERS, STRATEGIES } from './plan/planners.ts';\nexport type { PlannerFactoryOptions } from './plan/planners.ts';\nexport {\n isStructuralLanguage,\n planStructural,\n STRUCTURAL_LANGUAGES,\n STRUCTURAL_PLANNER_ID,\n StructuralPlanner,\n} from './plan/structural.ts';\nexport type { StructuralLanguage, StructuralPlannerOptions } from './plan/structural.ts';\nexport { createRetrieveTool, RETRIEVE_TOOL_NAME } from './retrieve.ts';\nexport { unconfiguredDistillStage, unconfiguredRerankStage } from './stages.ts';\nexport { MemoryElisionStore } from './store.ts';\nexport {\n DIRECTORY_STORE_FORMAT,\n DIRECTORY_STORE_VERSION,\n DirectoryElisionStore,\n} from './store-dir.ts';\nexport type { DirectoryElisionStoreOptions } from './store-dir.ts';\nexport * from './types.ts';\nexport {\n CLI_JSON_FORMAT,\n CLI_NAME,\n cliUsage,\n EXIT,\n formatReport,\n parseSmeltArgs,\n runCli,\n} from './cli/run.ts';\nexport type { AnswerStream, CliIo, CliJsonEnvelope, SmeltInvocation } from './cli/run.ts';\nexport {\n ANTHROPIC_PROMPT_CACHE_FACTS,\n CACHE_BREAKER_RULES,\n detectCacheBreakers,\n findPrefixDivergence,\n} from './cache/prefix.ts';\nexport type {\n CacheWarning,\n PrefixDivergence,\n PromptStructure,\n PromptTool,\n} from './cache/prefix.ts';\nexport { MARKER_LINE_COMMENT_LEADERS, markerForLanguage } from './apply.ts';\nexport {\n buildRepoMap,\n DEFAULT_REPO_IGNORE,\n REPO_MAP_CACHE_CORRUPT_RULE,\n REPO_MAP_ID,\n REPO_MAP_PATH_ONLY_RULE,\n REPO_MAP_RANKED_RULE,\n REPO_MAP_UNREFERENCED_RULE,\n} from './repomap/map.ts';\nexport type {\n RepoMap,\n RepoMapCacheCounts,\n RepoMapEntry,\n RepoMapOptions,\n RepoMapPathEntry,\n RepoMapReason,\n RepoMapWarning,\n} from './repomap/map.ts';\nexport { PAGERANK_DAMPING, PAGERANK_ITERATIONS, rankDefinitions } from './repomap/rank.ts';\nexport type { FileTagsEntry, RankedDefinition } from './repomap/rank.ts';\nexport { extractTags } from './repomap/tags.ts';\nexport type { DefinitionTag, FileTags, ReferenceTag } from './repomap/tags.ts';\nexport { TAGS_CACHE_FORMAT, TAGS_CACHE_VERSION, tagsCacheKey } from './repomap/cache.ts';\nexport { nodeFsReader } from './repomap/reader.ts';\nexport type { DirEntry, FileStat, RepoReader } from './repomap/reader.ts';\nexport {\n CONFIG_FILE_NAME,\n CONFIG_VERSION,\n configuredStore,\n findConfigFile,\n loadNearestConfig,\n parseConfig,\n renderConfig,\n resolveStorePath,\n} from './cli/config.ts';\nexport type { ConfiguredStore, LoadedConfig, SmeltConfig, SmeltConfigStore } from './cli/config.ts';\nexport {\n MEASURE_STUB_FILE,\n measureStubSource,\n RERANK_STUB_FILE,\n rerankStubSource,\n runInit,\n} from './cli/init.ts';\nexport type { InitIo } from './cli/init.ts';\nexport { retrieveStats } from './stats.ts';\nexport type { RawRetrieveCounters } from './stats.ts';\nexport {\n LANGUAGE_PROFILES,\n profileFor,\n profileForPath,\n structuralLanguages,\n} from './lang/registry.ts';\nexport type { LanguageProfile, LanguageStructure, RepoMapFacts } from './lang/profile.ts';\n/**\n * The harness registry's rendered views. Public for the same reason the ops seam is:\n * something outside this package renders them — the site's `facts.json` generator —\n * and the alternative is a second copy of the tier table typed into a React component,\n * which is exactly the drift `harnessesByTier()` exists to end.\n */\nexport { harnessesByTier, harnessNames, HARNESSES, HARNESS_IDS } from './harness/registry.ts';\nexport type { HarnessTierGroup } from './harness/registry.ts';\nexport { harnessLabel, HARNESS_TIERS, TIER_HONESTY } from './harness/profile.ts';\nexport type { HarnessId, HarnessTier } from './harness/profile.ts';\nexport { resolveRun } from './cli/subcommands/smelt.ts';\nexport type { ResolvedRun } from './cli/subcommands/smelt.ts';\nexport { REPO_MAP_FOCUS_RULE } from './repomap/map.ts';\nexport { CLI_MAP_JSON_FORMAT, formatMapReport, resolveMapRun } from './cli/run.ts';\nexport type { CliInvocation, CliMapJsonEnvelope, MapInvocation } from './cli/run.ts';\nexport type { MapReportInput } from './cli/report.ts';\nexport type { ResolvedMapRun } from './cli/subcommands/map.ts';\n\n/**\n * The operations seam — the four verbs and the laws their inputs must satisfy, below\n * every front door. `@smeltjs/mcp` consumes these as an ordinary dependency, so the\n * `smelt` CLI and the MCP tools run the same middle instead of two copies of it. See\n * `src/ops/index.ts` for what belongs here and what stays in an adapter.\n */\nexport {\n budgetFault,\n budgetMalformed,\n budgetRequired,\n mapTree,\n openStore,\n readBlob,\n readCounters,\n readTree,\n resolveStrategy,\n retrieveBytes,\n smeltBlob,\n} from './ops/index.ts';\nexport type {\n BudgetFault,\n BudgetNaming,\n MapTreeOp,\n ReadCountersOp,\n ResolvedStrategy,\n RetrieveBytesOp,\n Ruling,\n SmeltBlobOp,\n SmeltBlobOutcome,\n StrategySource,\n TreeNaming,\n} from './ops/index.ts';\n\n/**\n * Which planner a smelter uses, named by string. The names, their factories, and this\n * type all come from the one {@link PLANNERS} registry in `src/plan/planners.ts`, so\n * the CLI's validation and help text cannot drift from what `createSmelter` builds.\n */\nexport type { Strategy } from './plan/planners.ts';\n\nexport { createSmelter } from './smelter.ts';\nexport type { Smelter, SmelterConfig, SmeltCallOptions } from './smelter.ts';\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-faithful edits to somebody else's text file.
|
|
3
|
+
*
|
|
4
|
+
* Two editors, one contract: **change the one thing you were asked to change and
|
|
5
|
+
* leave every other byte alone.** An installer that reformats a settings file has
|
|
6
|
+
* edited what it was never asked to — key order, indentation, string escapes, number
|
|
7
|
+
* spellings and unknown keys are all somebody's choices, and `JSON.parse` →
|
|
8
|
+
* `JSON.stringify` erases every one of them.
|
|
9
|
+
*
|
|
10
|
+
* - {@link editTopLevelProperty}: replace, insert or remove **one top-level property**
|
|
11
|
+
* of a JSON object, in its source text. The value is rendered fresh; nothing
|
|
12
|
+
* outside its bytes moves. Callers have already `JSON.parse`d the text and decided
|
|
13
|
+
* what the new value is; this module holds only the tokenising.
|
|
14
|
+
* - {@link upsertMarkerBlock} / {@link stripMarkerBlock}: the same idea over a
|
|
15
|
+
* delimited block in a plain-text file (an instruction file, a Markdown snippet
|
|
16
|
+
* between two marker lines).
|
|
17
|
+
*
|
|
18
|
+
* Neither knows what a harness or a hook is. `cli/hooks.ts` is the consumer today; the
|
|
19
|
+
* module lives under `src/text/` rather than `cli/` because its whole interface is
|
|
20
|
+
* strings in, strings out — it reads no argv, prints nothing and imports nothing from
|
|
21
|
+
* the CLI, and the next byte-faithful editor (an instruction-file rewrite) wants a
|
|
22
|
+
* sibling here, not a CLI import.
|
|
23
|
+
*/
|
|
24
|
+
/** How a JSON file is laid out — what a rendered value must match to blend in. */
|
|
25
|
+
export interface JsonStyle {
|
|
26
|
+
/** The indentation unit — the whitespace before a top-level key. */
|
|
27
|
+
readonly indent: string;
|
|
28
|
+
/** `'\r\n'` when the file uses it anywhere, `'\n'` otherwise. */
|
|
29
|
+
readonly newline: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The layout an existing file uses: its first indented key's leading whitespace (two
|
|
33
|
+
* spaces when nothing is indented) and its newline convention. Detected once, before
|
|
34
|
+
* any edit, so a sequence of edits renders consistently even after an earlier one has
|
|
35
|
+
* changed the first indented line.
|
|
36
|
+
*/
|
|
37
|
+
export declare function jsonStyle(text: string): JsonStyle;
|
|
38
|
+
/**
|
|
39
|
+
* Replace, insert or remove one top-level property of the JSON object in `text`,
|
|
40
|
+
* leaving every other byte verbatim.
|
|
41
|
+
*
|
|
42
|
+
* - `value` defined: the property's value is replaced in place when the key exists,
|
|
43
|
+
* otherwise the property is appended after the last one (or into an empty object).
|
|
44
|
+
* - `value === undefined`: the property is removed, with its separating comma and
|
|
45
|
+
* whitespace; a key that is not there is a no-op and the text comes back unchanged.
|
|
46
|
+
*
|
|
47
|
+
* The rendered value is `JSON.stringify(value, null, indent)`, re-indented to sit at
|
|
48
|
+
* top level. `style` defaults to {@link jsonStyle} of `text`; pass it explicitly when
|
|
49
|
+
* making several edits to one file.
|
|
50
|
+
*
|
|
51
|
+
* Returns `undefined` when `text` is not a JSON object the scanner can walk — an
|
|
52
|
+
* array, a scalar, or something that is not JSON at all. Callers should have
|
|
53
|
+
* `JSON.parse`d first and refused; this is belt and braces, not a validator.
|
|
54
|
+
*/
|
|
55
|
+
export declare function editTopLevelProperty(text: string, key: string, value: unknown, style?: JsonStyle): string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Replace the block delimited by `start` … `end` in `existingText`, or append it.
|
|
58
|
+
*
|
|
59
|
+
* An absent or blank file becomes exactly `block`. A file that carries the block has
|
|
60
|
+
* it replaced in place (one newline after `end` is absorbed, so a block that ends in
|
|
61
|
+
* its own newline does not grow a blank line per re-run). A file without it gets the
|
|
62
|
+
* block appended after exactly one blank line, whatever trailing newlines it had.
|
|
63
|
+
*/
|
|
64
|
+
export declare function upsertMarkerBlock(existingText: string | undefined, block: string, start: string, end: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Remove the block delimited by `start` … `end`. The text comes back unchanged when
|
|
67
|
+
* the block is not there; `undefined` when nothing (or only whitespace) remains — the
|
|
68
|
+
* file was entirely the block, and the caller decides whether to delete it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function stripMarkerBlock(existingText: string, start: string, end: string): string | undefined;
|
|
71
|
+
//# sourceMappingURL=json-edit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-edit.d.ts","sourceRoot":"","sources":["../../src/text/json-edit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACxB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAKjD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,EACd,KAAK,GAAE,SAA2B,GACjC,MAAM,GAAG,SAAS,CAWpB;AA+ID;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,CAUR;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAQpB","sourcesContent":["/**\n * Byte-faithful edits to somebody else's text file.\n *\n * Two editors, one contract: **change the one thing you were asked to change and\n * leave every other byte alone.** An installer that reformats a settings file has\n * edited what it was never asked to — key order, indentation, string escapes, number\n * spellings and unknown keys are all somebody's choices, and `JSON.parse` →\n * `JSON.stringify` erases every one of them.\n *\n * - {@link editTopLevelProperty}: replace, insert or remove **one top-level property**\n * of a JSON object, in its source text. The value is rendered fresh; nothing\n * outside its bytes moves. Callers have already `JSON.parse`d the text and decided\n * what the new value is; this module holds only the tokenising.\n * - {@link upsertMarkerBlock} / {@link stripMarkerBlock}: the same idea over a\n * delimited block in a plain-text file (an instruction file, a Markdown snippet\n * between two marker lines).\n *\n * Neither knows what a harness or a hook is. `cli/hooks.ts` is the consumer today; the\n * module lives under `src/text/` rather than `cli/` because its whole interface is\n * strings in, strings out — it reads no argv, prints nothing and imports nothing from\n * the CLI, and the next byte-faithful editor (an instruction-file rewrite) wants a\n * sibling here, not a CLI import.\n */\n\n/** How a JSON file is laid out — what a rendered value must match to blend in. */\nexport interface JsonStyle {\n /** The indentation unit — the whitespace before a top-level key. */\n readonly indent: string;\n /** `'\\r\\n'` when the file uses it anywhere, `'\\n'` otherwise. */\n readonly newline: string;\n}\n\n/**\n * The layout an existing file uses: its first indented key's leading whitespace (two\n * spaces when nothing is indented) and its newline convention. Detected once, before\n * any edit, so a sequence of edits renders consistently even after an earlier one has\n * changed the first indented line.\n */\nexport function jsonStyle(text: string): JsonStyle {\n return {\n newline: text.includes('\\r\\n') ? '\\r\\n' : '\\n',\n indent: /\\n([ \\t]+)\"/.exec(text)?.[1] ?? ' ',\n };\n}\n\n/**\n * Replace, insert or remove one top-level property of the JSON object in `text`,\n * leaving every other byte verbatim.\n *\n * - `value` defined: the property's value is replaced in place when the key exists,\n * otherwise the property is appended after the last one (or into an empty object).\n * - `value === undefined`: the property is removed, with its separating comma and\n * whitespace; a key that is not there is a no-op and the text comes back unchanged.\n *\n * The rendered value is `JSON.stringify(value, null, indent)`, re-indented to sit at\n * top level. `style` defaults to {@link jsonStyle} of `text`; pass it explicitly when\n * making several edits to one file.\n *\n * Returns `undefined` when `text` is not a JSON object the scanner can walk — an\n * array, a scalar, or something that is not JSON at all. Callers should have\n * `JSON.parse`d first and refused; this is belt and braces, not a validator.\n */\nexport function editTopLevelProperty(\n text: string,\n key: string,\n value: unknown,\n style: JsonStyle = jsonStyle(text),\n): string | undefined {\n const scan = scanJsonTopLevel(text);\n if (scan === undefined) return undefined;\n const property = scan.properties.find((candidate) => candidate.key === key);\n if (value === undefined) {\n return property === undefined ? text : removeJsonProperty(text, scan, property);\n }\n const rendered = renderJsonValue(value, style.indent, style.newline);\n return property !== undefined\n ? `${text.slice(0, property.valueStart)}${rendered}${text.slice(property.valueEnd)}`\n : insertJsonProperty(text, scan, key, rendered, style.indent, style.newline);\n}\n\n/** One top-level property of a JSON object, located by offsets in its source text. */\ninterface JsonTopLevelProperty {\n readonly key: string;\n /** Offset of the key's opening quote. */\n readonly keyStart: number;\n /** Offset of the value's first byte. */\n readonly valueStart: number;\n /** Offset one past the value's last byte. */\n readonly valueEnd: number;\n}\n\ninterface JsonTopLevelScan {\n /** Offset of the root object's `{`. */\n readonly open: number;\n /** Offset of the root object's `}`. */\n readonly close: number;\n readonly properties: readonly JsonTopLevelProperty[];\n}\n\n/**\n * Locate the top-level properties of a JSON object *in its source text*, so one\n * property can be replaced, inserted or removed while every other byte of the file\n * rides through verbatim. `undefined` when the text is not an object.\n */\nfunction scanJsonTopLevel(text: string): JsonTopLevelScan | undefined {\n let i = skipJsonWhitespace(text, 0);\n if (text[i] !== '{') return undefined;\n const open = i;\n i = skipJsonWhitespace(text, i + 1);\n const properties: JsonTopLevelProperty[] = [];\n if (text[i] === '}') return { open, close: i, properties };\n for (;;) {\n if (text[i] !== '\"') return undefined;\n const keyStart = i;\n const keyEnd = skipJsonString(text, i);\n if (keyEnd === undefined) return undefined;\n const key = JSON.parse(text.slice(keyStart, keyEnd)) as string;\n i = skipJsonWhitespace(text, keyEnd);\n if (text[i] !== ':') return undefined;\n const valueStart = skipJsonWhitespace(text, i + 1);\n const valueEnd = skipJsonValue(text, valueStart);\n if (valueEnd === undefined) return undefined;\n properties.push({ key, keyStart, valueStart, valueEnd });\n i = skipJsonWhitespace(text, valueEnd);\n if (text[i] === ',') {\n i = skipJsonWhitespace(text, i + 1);\n continue;\n }\n if (text[i] === '}') return { open, close: i, properties };\n return undefined;\n }\n}\n\nfunction skipJsonWhitespace(text: string, from: number): number {\n let i = from;\n while (i < text.length && ' \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i;\n}\n\n/** `from` points at `\"`; returns the offset one past the closing quote. */\nfunction skipJsonString(text: string, from: number): number | undefined {\n let i = from + 1;\n while (i < text.length) {\n if (text[i] === '\\\\') i += 2;\n else if (text[i] === '\"') return i + 1;\n else i += 1;\n }\n return undefined;\n}\n\nfunction skipJsonValue(text: string, from: number): number | undefined {\n const first = text[from];\n if (first === '\"') return skipJsonString(text, from);\n if (first === '{' || first === '[') {\n let depth = 0;\n let i = from;\n while (i < text.length) {\n const ch = text[i]!;\n if (ch === '\"') {\n const end = skipJsonString(text, i);\n if (end === undefined) return undefined;\n i = end;\n continue;\n }\n if (ch === '{' || ch === '[') depth += 1;\n else if (ch === '}' || ch === ']') {\n depth -= 1;\n if (depth === 0) return i + 1;\n }\n i += 1;\n }\n return undefined;\n }\n // number / true / false / null\n let i = from;\n while (i < text.length && !',}] \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i > from ? i : undefined;\n}\n\n/** A JSON value indented for embedding at a top-level property position. */\nfunction renderJsonValue(value: unknown, indent: string, newline: string): string {\n return JSON.stringify(value, null, indent).split('\\n').join(`${newline}${indent}`);\n}\n\nfunction removeJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n property: JsonTopLevelProperty,\n): string {\n const index = scan.properties.indexOf(property);\n const next = scan.properties[index + 1];\n if (next !== undefined) {\n // Delete through the separating comma and whitespace, up to the next key.\n return text.slice(0, property.keyStart) + text.slice(next.keyStart);\n }\n const previous = scan.properties[index - 1];\n // Last (or only) property: delete the preceding comma (if any) with it.\n const from = previous !== undefined ? previous.valueEnd : scan.open + 1;\n return text.slice(0, from) + text.slice(property.valueEnd);\n}\n\nfunction insertJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n key: string,\n renderedValue: string,\n indent: string,\n newline: string,\n): string {\n const entry = `${JSON.stringify(key)}: ${renderedValue}`;\n if (scan.properties.length === 0) {\n return `${text.slice(0, scan.open + 1)}${newline}${indent}${entry}${newline}${text.slice(scan.close)}`;\n }\n const last = scan.properties[scan.properties.length - 1]!;\n return `${text.slice(0, last.valueEnd)},${newline}${indent}${entry}${text.slice(last.valueEnd)}`;\n}\n\n/* ------------------------------------------------------------------------------------\n * Marker blocks\n * ---------------------------------------------------------------------------------- */\n\n/**\n * Replace the block delimited by `start` … `end` in `existingText`, or append it.\n *\n * An absent or blank file becomes exactly `block`. A file that carries the block has\n * it replaced in place (one newline after `end` is absorbed, so a block that ends in\n * its own newline does not grow a blank line per re-run). A file without it gets the\n * block appended after exactly one blank line, whatever trailing newlines it had.\n */\nexport function upsertMarkerBlock(\n existingText: string | undefined,\n block: string,\n start: string,\n end: string,\n): string {\n if (existingText === undefined || existingText.trim() === '') return block;\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {\n const before = existingText.slice(0, startIndex);\n const after = existingText.slice(endIndex + end.length).replace(/^\\n/, '');\n return `${before}${block}${after}`;\n }\n return `${existingText.replace(/\\n*$/, '\\n\\n')}${block}`;\n}\n\n/**\n * Remove the block delimited by `start` … `end`. The text comes back unchanged when\n * the block is not there; `undefined` when nothing (or only whitespace) remains — the\n * file was entirely the block, and the caller decides whether to delete it.\n */\nexport function stripMarkerBlock(\n existingText: string,\n start: string,\n end: string,\n): string | undefined {\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) return existingText;\n const stripped =\n existingText.slice(0, startIndex).replace(/\\n+$/, '\\n') +\n existingText.slice(endIndex + end.length).replace(/^\\n+/, '');\n return stripped.trim() === '' ? undefined : stripped;\n}\n"]}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-faithful edits to somebody else's text file.
|
|
3
|
+
*
|
|
4
|
+
* Two editors, one contract: **change the one thing you were asked to change and
|
|
5
|
+
* leave every other byte alone.** An installer that reformats a settings file has
|
|
6
|
+
* edited what it was never asked to — key order, indentation, string escapes, number
|
|
7
|
+
* spellings and unknown keys are all somebody's choices, and `JSON.parse` →
|
|
8
|
+
* `JSON.stringify` erases every one of them.
|
|
9
|
+
*
|
|
10
|
+
* - {@link editTopLevelProperty}: replace, insert or remove **one top-level property**
|
|
11
|
+
* of a JSON object, in its source text. The value is rendered fresh; nothing
|
|
12
|
+
* outside its bytes moves. Callers have already `JSON.parse`d the text and decided
|
|
13
|
+
* what the new value is; this module holds only the tokenising.
|
|
14
|
+
* - {@link upsertMarkerBlock} / {@link stripMarkerBlock}: the same idea over a
|
|
15
|
+
* delimited block in a plain-text file (an instruction file, a Markdown snippet
|
|
16
|
+
* between two marker lines).
|
|
17
|
+
*
|
|
18
|
+
* Neither knows what a harness or a hook is. `cli/hooks.ts` is the consumer today; the
|
|
19
|
+
* module lives under `src/text/` rather than `cli/` because its whole interface is
|
|
20
|
+
* strings in, strings out — it reads no argv, prints nothing and imports nothing from
|
|
21
|
+
* the CLI, and the next byte-faithful editor (an instruction-file rewrite) wants a
|
|
22
|
+
* sibling here, not a CLI import.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The layout an existing file uses: its first indented key's leading whitespace (two
|
|
26
|
+
* spaces when nothing is indented) and its newline convention. Detected once, before
|
|
27
|
+
* any edit, so a sequence of edits renders consistently even after an earlier one has
|
|
28
|
+
* changed the first indented line.
|
|
29
|
+
*/
|
|
30
|
+
export function jsonStyle(text) {
|
|
31
|
+
return {
|
|
32
|
+
newline: text.includes('\r\n') ? '\r\n' : '\n',
|
|
33
|
+
indent: /\n([ \t]+)"/.exec(text)?.[1] ?? ' ',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Replace, insert or remove one top-level property of the JSON object in `text`,
|
|
38
|
+
* leaving every other byte verbatim.
|
|
39
|
+
*
|
|
40
|
+
* - `value` defined: the property's value is replaced in place when the key exists,
|
|
41
|
+
* otherwise the property is appended after the last one (or into an empty object).
|
|
42
|
+
* - `value === undefined`: the property is removed, with its separating comma and
|
|
43
|
+
* whitespace; a key that is not there is a no-op and the text comes back unchanged.
|
|
44
|
+
*
|
|
45
|
+
* The rendered value is `JSON.stringify(value, null, indent)`, re-indented to sit at
|
|
46
|
+
* top level. `style` defaults to {@link jsonStyle} of `text`; pass it explicitly when
|
|
47
|
+
* making several edits to one file.
|
|
48
|
+
*
|
|
49
|
+
* Returns `undefined` when `text` is not a JSON object the scanner can walk — an
|
|
50
|
+
* array, a scalar, or something that is not JSON at all. Callers should have
|
|
51
|
+
* `JSON.parse`d first and refused; this is belt and braces, not a validator.
|
|
52
|
+
*/
|
|
53
|
+
export function editTopLevelProperty(text, key, value, style = jsonStyle(text)) {
|
|
54
|
+
const scan = scanJsonTopLevel(text);
|
|
55
|
+
if (scan === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
const property = scan.properties.find((candidate) => candidate.key === key);
|
|
58
|
+
if (value === undefined) {
|
|
59
|
+
return property === undefined ? text : removeJsonProperty(text, scan, property);
|
|
60
|
+
}
|
|
61
|
+
const rendered = renderJsonValue(value, style.indent, style.newline);
|
|
62
|
+
return property !== undefined
|
|
63
|
+
? `${text.slice(0, property.valueStart)}${rendered}${text.slice(property.valueEnd)}`
|
|
64
|
+
: insertJsonProperty(text, scan, key, rendered, style.indent, style.newline);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Locate the top-level properties of a JSON object *in its source text*, so one
|
|
68
|
+
* property can be replaced, inserted or removed while every other byte of the file
|
|
69
|
+
* rides through verbatim. `undefined` when the text is not an object.
|
|
70
|
+
*/
|
|
71
|
+
function scanJsonTopLevel(text) {
|
|
72
|
+
let i = skipJsonWhitespace(text, 0);
|
|
73
|
+
if (text[i] !== '{')
|
|
74
|
+
return undefined;
|
|
75
|
+
const open = i;
|
|
76
|
+
i = skipJsonWhitespace(text, i + 1);
|
|
77
|
+
const properties = [];
|
|
78
|
+
if (text[i] === '}')
|
|
79
|
+
return { open, close: i, properties };
|
|
80
|
+
for (;;) {
|
|
81
|
+
if (text[i] !== '"')
|
|
82
|
+
return undefined;
|
|
83
|
+
const keyStart = i;
|
|
84
|
+
const keyEnd = skipJsonString(text, i);
|
|
85
|
+
if (keyEnd === undefined)
|
|
86
|
+
return undefined;
|
|
87
|
+
const key = JSON.parse(text.slice(keyStart, keyEnd));
|
|
88
|
+
i = skipJsonWhitespace(text, keyEnd);
|
|
89
|
+
if (text[i] !== ':')
|
|
90
|
+
return undefined;
|
|
91
|
+
const valueStart = skipJsonWhitespace(text, i + 1);
|
|
92
|
+
const valueEnd = skipJsonValue(text, valueStart);
|
|
93
|
+
if (valueEnd === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
properties.push({ key, keyStart, valueStart, valueEnd });
|
|
96
|
+
i = skipJsonWhitespace(text, valueEnd);
|
|
97
|
+
if (text[i] === ',') {
|
|
98
|
+
i = skipJsonWhitespace(text, i + 1);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (text[i] === '}')
|
|
102
|
+
return { open, close: i, properties };
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function skipJsonWhitespace(text, from) {
|
|
107
|
+
let i = from;
|
|
108
|
+
while (i < text.length && ' \t\r\n'.includes(text[i]))
|
|
109
|
+
i += 1;
|
|
110
|
+
return i;
|
|
111
|
+
}
|
|
112
|
+
/** `from` points at `"`; returns the offset one past the closing quote. */
|
|
113
|
+
function skipJsonString(text, from) {
|
|
114
|
+
let i = from + 1;
|
|
115
|
+
while (i < text.length) {
|
|
116
|
+
if (text[i] === '\\')
|
|
117
|
+
i += 2;
|
|
118
|
+
else if (text[i] === '"')
|
|
119
|
+
return i + 1;
|
|
120
|
+
else
|
|
121
|
+
i += 1;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function skipJsonValue(text, from) {
|
|
126
|
+
const first = text[from];
|
|
127
|
+
if (first === '"')
|
|
128
|
+
return skipJsonString(text, from);
|
|
129
|
+
if (first === '{' || first === '[') {
|
|
130
|
+
let depth = 0;
|
|
131
|
+
let i = from;
|
|
132
|
+
while (i < text.length) {
|
|
133
|
+
const ch = text[i];
|
|
134
|
+
if (ch === '"') {
|
|
135
|
+
const end = skipJsonString(text, i);
|
|
136
|
+
if (end === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
i = end;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (ch === '{' || ch === '[')
|
|
142
|
+
depth += 1;
|
|
143
|
+
else if (ch === '}' || ch === ']') {
|
|
144
|
+
depth -= 1;
|
|
145
|
+
if (depth === 0)
|
|
146
|
+
return i + 1;
|
|
147
|
+
}
|
|
148
|
+
i += 1;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
// number / true / false / null
|
|
153
|
+
let i = from;
|
|
154
|
+
while (i < text.length && !',}] \t\r\n'.includes(text[i]))
|
|
155
|
+
i += 1;
|
|
156
|
+
return i > from ? i : undefined;
|
|
157
|
+
}
|
|
158
|
+
/** A JSON value indented for embedding at a top-level property position. */
|
|
159
|
+
function renderJsonValue(value, indent, newline) {
|
|
160
|
+
return JSON.stringify(value, null, indent).split('\n').join(`${newline}${indent}`);
|
|
161
|
+
}
|
|
162
|
+
function removeJsonProperty(text, scan, property) {
|
|
163
|
+
const index = scan.properties.indexOf(property);
|
|
164
|
+
const next = scan.properties[index + 1];
|
|
165
|
+
if (next !== undefined) {
|
|
166
|
+
// Delete through the separating comma and whitespace, up to the next key.
|
|
167
|
+
return text.slice(0, property.keyStart) + text.slice(next.keyStart);
|
|
168
|
+
}
|
|
169
|
+
const previous = scan.properties[index - 1];
|
|
170
|
+
// Last (or only) property: delete the preceding comma (if any) with it.
|
|
171
|
+
const from = previous !== undefined ? previous.valueEnd : scan.open + 1;
|
|
172
|
+
return text.slice(0, from) + text.slice(property.valueEnd);
|
|
173
|
+
}
|
|
174
|
+
function insertJsonProperty(text, scan, key, renderedValue, indent, newline) {
|
|
175
|
+
const entry = `${JSON.stringify(key)}: ${renderedValue}`;
|
|
176
|
+
if (scan.properties.length === 0) {
|
|
177
|
+
return `${text.slice(0, scan.open + 1)}${newline}${indent}${entry}${newline}${text.slice(scan.close)}`;
|
|
178
|
+
}
|
|
179
|
+
const last = scan.properties[scan.properties.length - 1];
|
|
180
|
+
return `${text.slice(0, last.valueEnd)},${newline}${indent}${entry}${text.slice(last.valueEnd)}`;
|
|
181
|
+
}
|
|
182
|
+
/* ------------------------------------------------------------------------------------
|
|
183
|
+
* Marker blocks
|
|
184
|
+
* ---------------------------------------------------------------------------------- */
|
|
185
|
+
/**
|
|
186
|
+
* Replace the block delimited by `start` … `end` in `existingText`, or append it.
|
|
187
|
+
*
|
|
188
|
+
* An absent or blank file becomes exactly `block`. A file that carries the block has
|
|
189
|
+
* it replaced in place (one newline after `end` is absorbed, so a block that ends in
|
|
190
|
+
* its own newline does not grow a blank line per re-run). A file without it gets the
|
|
191
|
+
* block appended after exactly one blank line, whatever trailing newlines it had.
|
|
192
|
+
*/
|
|
193
|
+
export function upsertMarkerBlock(existingText, block, start, end) {
|
|
194
|
+
if (existingText === undefined || existingText.trim() === '')
|
|
195
|
+
return block;
|
|
196
|
+
const startIndex = existingText.indexOf(start);
|
|
197
|
+
const endIndex = existingText.indexOf(end);
|
|
198
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
199
|
+
const before = existingText.slice(0, startIndex);
|
|
200
|
+
const after = existingText.slice(endIndex + end.length).replace(/^\n/, '');
|
|
201
|
+
return `${before}${block}${after}`;
|
|
202
|
+
}
|
|
203
|
+
return `${existingText.replace(/\n*$/, '\n\n')}${block}`;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Remove the block delimited by `start` … `end`. The text comes back unchanged when
|
|
207
|
+
* the block is not there; `undefined` when nothing (or only whitespace) remains — the
|
|
208
|
+
* file was entirely the block, and the caller decides whether to delete it.
|
|
209
|
+
*/
|
|
210
|
+
export function stripMarkerBlock(existingText, start, end) {
|
|
211
|
+
const startIndex = existingText.indexOf(start);
|
|
212
|
+
const endIndex = existingText.indexOf(end);
|
|
213
|
+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex)
|
|
214
|
+
return existingText;
|
|
215
|
+
const stripped = existingText.slice(0, startIndex).replace(/\n+$/, '\n') +
|
|
216
|
+
existingText.slice(endIndex + end.length).replace(/^\n+/, '');
|
|
217
|
+
return stripped.trim() === '' ? undefined : stripped;
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=json-edit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-edit.js","sourceRoot":"","sources":["../../src/text/json-edit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Byte-faithful edits to somebody else's text file.\n *\n * Two editors, one contract: **change the one thing you were asked to change and\n * leave every other byte alone.** An installer that reformats a settings file has\n * edited what it was never asked to — key order, indentation, string escapes, number\n * spellings and unknown keys are all somebody's choices, and `JSON.parse` →\n * `JSON.stringify` erases every one of them.\n *\n * - {@link editTopLevelProperty}: replace, insert or remove **one top-level property**\n * of a JSON object, in its source text. The value is rendered fresh; nothing\n * outside its bytes moves. Callers have already `JSON.parse`d the text and decided\n * what the new value is; this module holds only the tokenising.\n * - {@link upsertMarkerBlock} / {@link stripMarkerBlock}: the same idea over a\n * delimited block in a plain-text file (an instruction file, a Markdown snippet\n * between two marker lines).\n *\n * Neither knows what a harness or a hook is. `cli/hooks.ts` is the consumer today; the\n * module lives under `src/text/` rather than `cli/` because its whole interface is\n * strings in, strings out — it reads no argv, prints nothing and imports nothing from\n * the CLI, and the next byte-faithful editor (an instruction-file rewrite) wants a\n * sibling here, not a CLI import.\n */\n\n/** How a JSON file is laid out — what a rendered value must match to blend in. */\nexport interface JsonStyle {\n /** The indentation unit — the whitespace before a top-level key. */\n readonly indent: string;\n /** `'\\r\\n'` when the file uses it anywhere, `'\\n'` otherwise. */\n readonly newline: string;\n}\n\n/**\n * The layout an existing file uses: its first indented key's leading whitespace (two\n * spaces when nothing is indented) and its newline convention. Detected once, before\n * any edit, so a sequence of edits renders consistently even after an earlier one has\n * changed the first indented line.\n */\nexport function jsonStyle(text: string): JsonStyle {\n return {\n newline: text.includes('\\r\\n') ? '\\r\\n' : '\\n',\n indent: /\\n([ \\t]+)\"/.exec(text)?.[1] ?? ' ',\n };\n}\n\n/**\n * Replace, insert or remove one top-level property of the JSON object in `text`,\n * leaving every other byte verbatim.\n *\n * - `value` defined: the property's value is replaced in place when the key exists,\n * otherwise the property is appended after the last one (or into an empty object).\n * - `value === undefined`: the property is removed, with its separating comma and\n * whitespace; a key that is not there is a no-op and the text comes back unchanged.\n *\n * The rendered value is `JSON.stringify(value, null, indent)`, re-indented to sit at\n * top level. `style` defaults to {@link jsonStyle} of `text`; pass it explicitly when\n * making several edits to one file.\n *\n * Returns `undefined` when `text` is not a JSON object the scanner can walk — an\n * array, a scalar, or something that is not JSON at all. Callers should have\n * `JSON.parse`d first and refused; this is belt and braces, not a validator.\n */\nexport function editTopLevelProperty(\n text: string,\n key: string,\n value: unknown,\n style: JsonStyle = jsonStyle(text),\n): string | undefined {\n const scan = scanJsonTopLevel(text);\n if (scan === undefined) return undefined;\n const property = scan.properties.find((candidate) => candidate.key === key);\n if (value === undefined) {\n return property === undefined ? text : removeJsonProperty(text, scan, property);\n }\n const rendered = renderJsonValue(value, style.indent, style.newline);\n return property !== undefined\n ? `${text.slice(0, property.valueStart)}${rendered}${text.slice(property.valueEnd)}`\n : insertJsonProperty(text, scan, key, rendered, style.indent, style.newline);\n}\n\n/** One top-level property of a JSON object, located by offsets in its source text. */\ninterface JsonTopLevelProperty {\n readonly key: string;\n /** Offset of the key's opening quote. */\n readonly keyStart: number;\n /** Offset of the value's first byte. */\n readonly valueStart: number;\n /** Offset one past the value's last byte. */\n readonly valueEnd: number;\n}\n\ninterface JsonTopLevelScan {\n /** Offset of the root object's `{`. */\n readonly open: number;\n /** Offset of the root object's `}`. */\n readonly close: number;\n readonly properties: readonly JsonTopLevelProperty[];\n}\n\n/**\n * Locate the top-level properties of a JSON object *in its source text*, so one\n * property can be replaced, inserted or removed while every other byte of the file\n * rides through verbatim. `undefined` when the text is not an object.\n */\nfunction scanJsonTopLevel(text: string): JsonTopLevelScan | undefined {\n let i = skipJsonWhitespace(text, 0);\n if (text[i] !== '{') return undefined;\n const open = i;\n i = skipJsonWhitespace(text, i + 1);\n const properties: JsonTopLevelProperty[] = [];\n if (text[i] === '}') return { open, close: i, properties };\n for (;;) {\n if (text[i] !== '\"') return undefined;\n const keyStart = i;\n const keyEnd = skipJsonString(text, i);\n if (keyEnd === undefined) return undefined;\n const key = JSON.parse(text.slice(keyStart, keyEnd)) as string;\n i = skipJsonWhitespace(text, keyEnd);\n if (text[i] !== ':') return undefined;\n const valueStart = skipJsonWhitespace(text, i + 1);\n const valueEnd = skipJsonValue(text, valueStart);\n if (valueEnd === undefined) return undefined;\n properties.push({ key, keyStart, valueStart, valueEnd });\n i = skipJsonWhitespace(text, valueEnd);\n if (text[i] === ',') {\n i = skipJsonWhitespace(text, i + 1);\n continue;\n }\n if (text[i] === '}') return { open, close: i, properties };\n return undefined;\n }\n}\n\nfunction skipJsonWhitespace(text: string, from: number): number {\n let i = from;\n while (i < text.length && ' \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i;\n}\n\n/** `from` points at `\"`; returns the offset one past the closing quote. */\nfunction skipJsonString(text: string, from: number): number | undefined {\n let i = from + 1;\n while (i < text.length) {\n if (text[i] === '\\\\') i += 2;\n else if (text[i] === '\"') return i + 1;\n else i += 1;\n }\n return undefined;\n}\n\nfunction skipJsonValue(text: string, from: number): number | undefined {\n const first = text[from];\n if (first === '\"') return skipJsonString(text, from);\n if (first === '{' || first === '[') {\n let depth = 0;\n let i = from;\n while (i < text.length) {\n const ch = text[i]!;\n if (ch === '\"') {\n const end = skipJsonString(text, i);\n if (end === undefined) return undefined;\n i = end;\n continue;\n }\n if (ch === '{' || ch === '[') depth += 1;\n else if (ch === '}' || ch === ']') {\n depth -= 1;\n if (depth === 0) return i + 1;\n }\n i += 1;\n }\n return undefined;\n }\n // number / true / false / null\n let i = from;\n while (i < text.length && !',}] \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i > from ? i : undefined;\n}\n\n/** A JSON value indented for embedding at a top-level property position. */\nfunction renderJsonValue(value: unknown, indent: string, newline: string): string {\n return JSON.stringify(value, null, indent).split('\\n').join(`${newline}${indent}`);\n}\n\nfunction removeJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n property: JsonTopLevelProperty,\n): string {\n const index = scan.properties.indexOf(property);\n const next = scan.properties[index + 1];\n if (next !== undefined) {\n // Delete through the separating comma and whitespace, up to the next key.\n return text.slice(0, property.keyStart) + text.slice(next.keyStart);\n }\n const previous = scan.properties[index - 1];\n // Last (or only) property: delete the preceding comma (if any) with it.\n const from = previous !== undefined ? previous.valueEnd : scan.open + 1;\n return text.slice(0, from) + text.slice(property.valueEnd);\n}\n\nfunction insertJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n key: string,\n renderedValue: string,\n indent: string,\n newline: string,\n): string {\n const entry = `${JSON.stringify(key)}: ${renderedValue}`;\n if (scan.properties.length === 0) {\n return `${text.slice(0, scan.open + 1)}${newline}${indent}${entry}${newline}${text.slice(scan.close)}`;\n }\n const last = scan.properties[scan.properties.length - 1]!;\n return `${text.slice(0, last.valueEnd)},${newline}${indent}${entry}${text.slice(last.valueEnd)}`;\n}\n\n/* ------------------------------------------------------------------------------------\n * Marker blocks\n * ---------------------------------------------------------------------------------- */\n\n/**\n * Replace the block delimited by `start` … `end` in `existingText`, or append it.\n *\n * An absent or blank file becomes exactly `block`. A file that carries the block has\n * it replaced in place (one newline after `end` is absorbed, so a block that ends in\n * its own newline does not grow a blank line per re-run). A file without it gets the\n * block appended after exactly one blank line, whatever trailing newlines it had.\n */\nexport function upsertMarkerBlock(\n existingText: string | undefined,\n block: string,\n start: string,\n end: string,\n): string {\n if (existingText === undefined || existingText.trim() === '') return block;\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {\n const before = existingText.slice(0, startIndex);\n const after = existingText.slice(endIndex + end.length).replace(/^\\n/, '');\n return `${before}${block}${after}`;\n }\n return `${existingText.replace(/\\n*$/, '\\n\\n')}${block}`;\n}\n\n/**\n * Remove the block delimited by `start` … `end`. The text comes back unchanged when\n * the block is not there; `undefined` when nothing (or only whitespace) remains — the\n * file was entirely the block, and the caller decides whether to delete it.\n */\nexport function stripMarkerBlock(\n existingText: string,\n start: string,\n end: string,\n): string | undefined {\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) return existingText;\n const stripped =\n existingText.slice(0, startIndex).replace(/\\n+$/, '\\n') +\n existingText.slice(endIndex + end.length).replace(/^\\n+/, '');\n return stripped.trim() === '' ? undefined : stripped;\n}\n"]}
|
package/dist/types.d.ts
CHANGED
|
@@ -154,17 +154,6 @@ export interface SmeltResult {
|
|
|
154
154
|
/** Present only when the caller supplied a {@link Measure}. Never invented. */
|
|
155
155
|
readonly measured?: MeasuredSize;
|
|
156
156
|
}
|
|
157
|
-
/** Options for a single `smelt()` call. */
|
|
158
|
-
export interface SmeltOptions {
|
|
159
|
-
/** Soft ceiling for the emitted output, in UTF-8 bytes. */
|
|
160
|
-
readonly budgetBytes: number;
|
|
161
|
-
/** Filename or path, used to detect the language. Optional; detection falls back to `'unknown'`. */
|
|
162
|
-
readonly path?: string;
|
|
163
|
-
/** Override language detection entirely. */
|
|
164
|
-
readonly language?: DetectedLanguage;
|
|
165
|
-
/** What the caller was looking for. See {@link PlanInput.focus}. */
|
|
166
|
-
readonly focus?: readonly string[];
|
|
167
|
-
}
|
|
168
157
|
/**
|
|
169
158
|
* Reversibility, as a callable. Takes a {@link SmeltResult} and the store that holds its
|
|
170
159
|
* elided bytes, and returns the original text — byte for byte.
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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","sourcesContent":["/** Languages smelt can parse structurally. Everything else falls back to the lexical planner. */\nexport type LanguageId =\n | 'typescript'\n | 'tsx'\n | 'javascript'\n | 'rust'\n | 'python'\n | 'go'\n | 'java'\n | 'c'\n | 'cpp'\n | 'c_sharp'\n | 'ruby'\n | 'php'\n | 'kotlin'\n | 'swift'\n | 'bash';\n\n/** `'unknown'` is a first-class outcome, not a failure: it selects the lexical planner. */\nexport type DetectedLanguage = LanguageId | 'unknown';\n\n/** A half-open byte range `[start, end)` into the UTF-8 bytes of the input. */\nexport interface ByteRange {\n readonly start: number;\n readonly end: number;\n}\n\n/**\n * Why a range was elided, in two registers: a stable id for counters, and a sentence\n * a human can read in a diff. Law 2 — every elision is explainable — lives here:\n * if you cannot write the sentence, the rule does not ship.\n */\nexport interface ElisionReason {\n /** Stable machine id, e.g. `'sibling-collapse'`, `'line-window'`. */\n readonly rule: string;\n /** e.g. `'collapsed 3 sibling functions'`. Present tense, no trailing period. */\n readonly explanation: string;\n}\n\n/** One range a planner proposes to remove. Plans are pure data — inspectable and testable. */\nexport interface PlannedElision {\n readonly range: ByteRange;\n readonly reason: ElisionReason;\n}\n\n/**\n * The complete output of a planner: the whole decision, before anything is mutated.\n * A plan can be logged, diffed, snapshot-tested, and rejected without touching the text.\n */\nexport interface ElisionPlan {\n readonly planner: string;\n readonly language: DetectedLanguage;\n readonly elisions: readonly PlannedElision[];\n}\n\n/**\n * The seam through which a planner asks what a marker will cost, in UTF-8 bytes.\n *\n * Marker cost is `apply.ts`'s fact — the applier renders the marker, so only the\n * applier knows its price. Planners need that price for two decisions (profitability:\n * a marker that costs more than it removes grows the output; and budget prediction:\n * which ladder rung actually fits), and before this seam each planner rebuilt the\n * marker machinery privately to measure it — correct, but an inversion. Now\n * `markerPricing()` in `apply.ts` builds the one adapter from the exact builder\n * `applyPlan` will use, and planners ask it. They never guess, and they never render\n * a marker of their own.\n *\n * The price is exact, not an estimate: the cost of the marker this elision would\n * earn, comment leader and all, with a stand-in hash of the real hash's length —\n * marker cost depends on the hash's *length*, never its value.\n */\nexport interface MarkerPricing {\n /** The exact UTF-8 byte cost of the marker an elision with this reason and size would earn. */\n costBytes(reason: ElisionReason, elidedBytes: number): number;\n}\n\n/**\n * What the caller hands a planner.\n *\n * Constructed centrally: `createSmelter` (and through it, the CLI) builds the one\n * `PlanInput` per call, including its {@link MarkerPricing} — a caller invoking\n * `planLexical`/`planStructural` directly builds `pricing` with `markerPricing()`\n * from `apply.ts`. A JS caller who omits it gets {@link MissingMarkerPricingError}\n * at plan time, not a guessed cost.\n */\nexport interface PlanInput {\n readonly text: string;\n readonly language: DetectedLanguage;\n /** Soft ceiling for the emitted output, in UTF-8 bytes. Planners aim under it. */\n readonly budgetBytes: number;\n /**\n * What the caller was actually looking for — grep pattern, symbol name, error string.\n * Planners keep matching regions and collapse around them.\n */\n readonly focus?: readonly string[];\n /**\n * What a marker costs. Required: a planner that guesses marker cost can plan an\n * elision that grows the output. See {@link MarkerPricing}; built by\n * `markerPricing()` in `apply.ts` from the exact builder `applyPlan` will use.\n */\n readonly pricing: MarkerPricing;\n}\n\n/**\n * A planner decides *what* to remove. It never removes anything itself; `applyPlan`\n * does that. Keeping the decision and the mutation apart is what makes the decision\n * testable in isolation.\n */\nexport interface Planner {\n readonly id: string;\n plan(input: PlanInput): Promise<ElisionPlan>;\n}\n\n/** One elision that actually happened, with the receipt needed to undo it. */\nexport interface AppliedElision {\n /** Content hash of the removed bytes — the key `retrieve()` takes. */\n readonly hash: string;\n /** Where the removed bytes were in the *input*. */\n readonly range: ByteRange;\n /**\n * Where the marker sits in the *output*. Law 3 — every elision is reversible — needs\n * this: {@link Reconstructor} splices stored bytes back over these ranges. Without it,\n * \"reversible\" would mean parsing markers back out of the text, which is a guess.\n * This is a fact recorded at the moment of the cut.\n */\n readonly outputRange: ByteRange;\n /** Size of the removed content, in UTF-8 bytes. */\n readonly bytes: number;\n readonly reason: ElisionReason;\n /** The exact marker text substituted into the output. */\n readonly marker: string;\n}\n\n/**\n * A consumer-supplied counter, so a caller who bills in tokens can *see* tokens.\n *\n * Budgets in smelt's core are UTF-8 bytes, permanently — bytes are the only unit that\n * is computable locally for every model, and they mean the same thing in five years.\n * See `docs/ARCHITECTURE.md` § \"Decision 1\". This hook does not change that: it adds a\n * second, labelled number to the result. The plan is still made in bytes.\n *\n * Both `id` and `unit` are required, and that is a Law 4 decision rather than\n * bookkeeping: a token count is meaningless without naming the tokenizer that produced\n * it. Anthropic's own docs record that Claude 4.7 and later use a newer tokenizer where\n * the same text yields roughly 30% more tokens than on earlier models — so `1,204\n * tokens` is not a fact, and `1,204 tokens (claude-4.7/count_tokens)` is.\n *\n * **This hook does not relax Law 1.** smelt imports no transport and the guard proves\n * that about smelt's own modules; it cannot prove it about a function you hand in. A\n * `count()` that calls an API makes *your* process call an API, from a line in *your*\n * source — exactly the arrangement {@link RerankStage} already describes. `count` is\n * synchronous on purpose: local tokenizers are synchronous, and network clients are not.\n */\nexport interface Measure {\n /** Names the counter, e.g. `'tiktoken/o200k_base'` or `'claude-4.7/count_tokens'`. */\n readonly id: string;\n /** The unit `count()` returns, e.g. `'tokens'`. Printed next to the number. */\n readonly unit: string;\n /** Local, synchronous count over the whole string. */\n count(text: string): number;\n}\n\n/** A second size for a result, in someone else's unit, with the counter named. */\nexport interface MeasuredSize {\n /** {@link Measure.id} of the counter that produced these numbers. */\n readonly measure: string;\n /** {@link Measure.unit}. */\n readonly unit: string;\n readonly input: number;\n readonly output: number;\n}\n\n/** The result of smelting one blob of text. */\nexport interface SmeltResult {\n readonly text: string;\n readonly inputBytes: number;\n readonly outputBytes: number;\n readonly planner: string;\n readonly language: DetectedLanguage;\n readonly elisions: readonly AppliedElision[];\n /** Present only when the caller supplied a {@link Measure}. Never invented. */\n readonly measured?: MeasuredSize;\n}\n\n/** Options for a single `smelt()` call. */\nexport interface SmeltOptions {\n /** Soft ceiling for the emitted output, in UTF-8 bytes. */\n readonly budgetBytes: number;\n /** Filename or path, used to detect the language. Optional; detection falls back to `'unknown'`. */\n readonly path?: string;\n /** Override language detection entirely. */\n readonly language?: DetectedLanguage;\n /** What the caller was looking for. See {@link PlanInput.focus}. */\n readonly focus?: readonly string[];\n}\n\n/**\n * Reversibility, as a callable. Takes a {@link SmeltResult} and the store that holds its\n * elided bytes, and returns the original text — byte for byte.\n */\nexport type Reconstructor = (result: SmeltResult, store: ElisionStore) => string;\n\n// ---------------------------------------------------------------------------\n// The store, and the counters that make over-pruning visible\n// ---------------------------------------------------------------------------\n\n/**\n * The numbers that keep smelt honest about itself.\n *\n * Law 3 says elisions are reversible. That is cheap to satisfy and easy to abuse: a\n * compressor that cuts everything is \"reversible\" and useless. The *retrieve rate* is\n * the tell. If the model keeps calling `smelt_retrieve`, smelt cut material the task\n * needed, and the round trip cost more tokens than the elision saved.\n *\n * So: `expansionRate` is not telemetry. It is the metric a caller tunes budgets\n * against, and the only number smelt is willing to have an opinion about — because it\n * measures it locally, per session, on the caller's own traffic.\n */\nexport interface RetrieveStats {\n /** Distinct blobs put into the store. */\n readonly elisionsStored: number;\n /** Total bytes held by the store. */\n readonly bytesStored: number;\n /** Every `retrieve()` call, including repeats and misses. */\n readonly retrieveCalls: number;\n /** Distinct hashes successfully retrieved at least once. */\n readonly uniqueRetrieved: number;\n /** Calls for a hash the store does not hold. Non-zero means a bug, not over-pruning. */\n readonly misses: number;\n /**\n * `uniqueRetrieved / elisionsStored`, or `0` when nothing has been stored.\n *\n * Read it as: *what fraction of what smelt hid did the model have to ask for back?*\n * There is no universally right value, and smelt does not ship a threshold it has\n * not measured. Rising across a workload is the signal.\n */\n readonly expansionRate: number;\n /**\n * The one degenerate outcome smelt is willing to name: **every distinct blob it hid\n * was asked for again.**\n *\n * smelt ships no expansion-rate threshold, because a threshold is a policy claim it\n * has no basis for and the right rate depends on how aggressive a budget the caller\n * chose — and a library printing warnings into someone else's process is bad manners.\n * This is not a threshold. At `uniqueRetrieved === elisionsStored` the elision\n * achieved nothing and cost a round trip: an arithmetic fact, not a preference. What\n * to do about it is the caller's call.\n *\n * `false` for an empty store — nothing was hidden, so nothing was defeated.\n */\n readonly allElisionsRetrieved: boolean;\n}\n\n/**\n * Local, content-addressed storage for elided bytes. No network, no eviction in v1 —\n * evicting is how \"reversible\" quietly becomes \"reversible for a while\".\n */\nexport interface ElisionStore {\n /** Store content, returning its hash. Idempotent for identical content. */\n put(content: string): string;\n /** The stored content, or `undefined` if this store never held that hash. */\n peek(hash: string): string | undefined;\n /**\n * The stored content, *counted* as a retrieval. This is what the model's tool calls.\n *\n * @throws {UnknownHashError} when the hash is unknown.\n * @throws {StoreCorruptionError} — {@link DirectoryElisionStore} only — when the\n * bytes on disk no longer hash to their own name. Distinct from\n * `UnknownHashError` on purpose: \"we hold damaged bytes\" and \"it never existed\"\n * are different answers, and a caller that conflates them will report the wrong\n * one to its user.\n *\n * A {@link DirectoryElisionStore} whose journal cannot be written (a read-only\n * store directory, a full disk) still returns the bytes — verified bytes are never\n * withheld over a bookkeeping failure — and surfaces the lost count as a\n * `process.emitWarning` named `SmeltCounterWriteFailure` instead of throwing. Its\n * stats go quiet from that point; the retrieval itself succeeded.\n */\n retrieve(hash: string): string;\n /**\n * Whether this hash can be retrieved — **not** merely whether a slot bearing it\n * exists. `true` must mean the very next `retrieve(hash)` returns bytes, so a store\n * that verifies content on read verifies here too and raises its corruption error\n * rather than answering `true` for bytes it would then refuse. Uncounted: a check is\n * not the model asking for material back.\n */\n has(hash: string): boolean;\n /** A snapshot of the counters. See {@link RetrieveStats}. */\n stats(): RetrieveStats;\n}\n\n/**\n * The retrieval tool a consumer exposes to its model. Deliberately not an MCP or\n * provider-specific shape — smelt does not know which SDK you use. The consumer adapts\n * this into its own tool schema; the contract is `hash in, exact bytes out`.\n */\nexport interface RetrieveTool {\n /** `'smelt_retrieve'`. Stable — consumers hard-code it in prompts. */\n readonly name: string;\n /** Prose the consumer can put straight into a tool description. */\n readonly description: string;\n /**\n * JSON-Schema-shaped parameter description, for consumers that want one.\n *\n * Strict-mode shaped: `additionalProperties: false` and a `required` naming every\n * property, so a consumer registering this under OpenAI's structured-outputs strict\n * mode is not refused at registration. See {@link createRetrieveTool}.\n */\n readonly inputSchema: {\n readonly type: 'object';\n readonly properties: {\n readonly hash: { readonly type: 'string'; readonly description: string };\n };\n readonly required: readonly ['hash'];\n readonly additionalProperties: false;\n };\n /**\n * @throws {UnknownHashError} when the hash is unknown.\n * @throws {StoreCorruptionError} when the backing store holds damaged bytes for\n * that hash — see {@link ElisionStore.retrieve}, whose contract this forwards\n * verbatim. Surface either to the model as a tool error, never as empty text.\n */\n invoke(input: { readonly hash: string }): string;\n}\n\n// ---------------------------------------------------------------------------\n// Pluggable stages — interfaces in v1, nothing more\n// ---------------------------------------------------------------------------\n\n/** A candidate handed to a {@link RerankStage}: an opaque id plus the text to judge. */\nexport interface RerankCandidate {\n readonly id: string;\n readonly text: string;\n}\n\n/** A reranked candidate, most relevant first. `score` is the stage's own scale. */\nexport interface RerankedCandidate extends RerankCandidate {\n readonly score: number;\n}\n\n/**\n * Relevance reranking — a *seam*, not a feature.\n *\n * Hosted rerankers are good and smelt will never bundle one, because bundling would\n * break Law 1: the moment smelt ships a default reranker, `smelt()` can make a network\n * call that the caller did not ask for and cannot see. A consumer that wants one\n * implements this interface, wires its own key, and owns the fact that its context now\n * leaves the machine. That decision must be legible in the consumer's own source.\n */\nexport interface RerankStage {\n readonly id: string;\n /** May make network calls — that is the consumer's choice, made in the consumer's code. */\n rerank(\n candidates: readonly RerankCandidate[],\n query: string,\n ): Promise<readonly RerankedCandidate[]>;\n}\n\n/**\n * Learned distillation — rewriting content with a model instead of cutting it.\n *\n * Out of v1 for a reason beyond the network: a distilled paragraph cannot satisfy\n * Law 2. \"The model summarised this\" is not an explanation of what was removed, and\n * the removed material is no longer recoverable from the output. If this ever ships,\n * it ships as a stage that stores the original and explains itself in the same terms\n * every other rule does.\n */\nexport interface DistillStage {\n readonly id: string;\n distill(text: string, budgetBytes: number): Promise<string>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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","sourcesContent":["/** Languages smelt can parse structurally. Everything else falls back to the lexical planner. */\nexport type LanguageId =\n | 'typescript'\n | 'tsx'\n | 'javascript'\n | 'rust'\n | 'python'\n | 'go'\n | 'java'\n | 'c'\n | 'cpp'\n | 'c_sharp'\n | 'ruby'\n | 'php'\n | 'kotlin'\n | 'swift'\n | 'bash';\n\n/** `'unknown'` is a first-class outcome, not a failure: it selects the lexical planner. */\nexport type DetectedLanguage = LanguageId | 'unknown';\n\n/** A half-open byte range `[start, end)` into the UTF-8 bytes of the input. */\nexport interface ByteRange {\n readonly start: number;\n readonly end: number;\n}\n\n/**\n * Why a range was elided, in two registers: a stable id for counters, and a sentence\n * a human can read in a diff. Law 2 — every elision is explainable — lives here:\n * if you cannot write the sentence, the rule does not ship.\n */\nexport interface ElisionReason {\n /** Stable machine id, e.g. `'sibling-collapse'`, `'line-window'`. */\n readonly rule: string;\n /** e.g. `'collapsed 3 sibling functions'`. Present tense, no trailing period. */\n readonly explanation: string;\n}\n\n/** One range a planner proposes to remove. Plans are pure data — inspectable and testable. */\nexport interface PlannedElision {\n readonly range: ByteRange;\n readonly reason: ElisionReason;\n}\n\n/**\n * The complete output of a planner: the whole decision, before anything is mutated.\n * A plan can be logged, diffed, snapshot-tested, and rejected without touching the text.\n */\nexport interface ElisionPlan {\n readonly planner: string;\n readonly language: DetectedLanguage;\n readonly elisions: readonly PlannedElision[];\n}\n\n/**\n * The seam through which a planner asks what a marker will cost, in UTF-8 bytes.\n *\n * Marker cost is `apply.ts`'s fact — the applier renders the marker, so only the\n * applier knows its price. Planners need that price for two decisions (profitability:\n * a marker that costs more than it removes grows the output; and budget prediction:\n * which ladder rung actually fits), and before this seam each planner rebuilt the\n * marker machinery privately to measure it — correct, but an inversion. Now\n * `markerPricing()` in `apply.ts` builds the one adapter from the exact builder\n * `applyPlan` will use, and planners ask it. They never guess, and they never render\n * a marker of their own.\n *\n * The price is exact, not an estimate: the cost of the marker this elision would\n * earn, comment leader and all, with a stand-in hash of the real hash's length —\n * marker cost depends on the hash's *length*, never its value.\n */\nexport interface MarkerPricing {\n /** The exact UTF-8 byte cost of the marker an elision with this reason and size would earn. */\n costBytes(reason: ElisionReason, elidedBytes: number): number;\n}\n\n/**\n * What the caller hands a planner.\n *\n * Constructed centrally: `createSmelter` (and through it, the CLI) builds the one\n * `PlanInput` per call, including its {@link MarkerPricing} — a caller invoking\n * `planLexical`/`planStructural` directly builds `pricing` with `markerPricing()`\n * from `apply.ts`. A JS caller who omits it gets {@link MissingMarkerPricingError}\n * at plan time, not a guessed cost.\n */\nexport interface PlanInput {\n readonly text: string;\n readonly language: DetectedLanguage;\n /** Soft ceiling for the emitted output, in UTF-8 bytes. Planners aim under it. */\n readonly budgetBytes: number;\n /**\n * What the caller was actually looking for — grep pattern, symbol name, error string.\n * Planners keep matching regions and collapse around them.\n */\n readonly focus?: readonly string[];\n /**\n * What a marker costs. Required: a planner that guesses marker cost can plan an\n * elision that grows the output. See {@link MarkerPricing}; built by\n * `markerPricing()` in `apply.ts` from the exact builder `applyPlan` will use.\n */\n readonly pricing: MarkerPricing;\n}\n\n/**\n * A planner decides *what* to remove. It never removes anything itself; `applyPlan`\n * does that. Keeping the decision and the mutation apart is what makes the decision\n * testable in isolation.\n */\nexport interface Planner {\n readonly id: string;\n plan(input: PlanInput): Promise<ElisionPlan>;\n}\n\n/** One elision that actually happened, with the receipt needed to undo it. */\nexport interface AppliedElision {\n /** Content hash of the removed bytes — the key `retrieve()` takes. */\n readonly hash: string;\n /** Where the removed bytes were in the *input*. */\n readonly range: ByteRange;\n /**\n * Where the marker sits in the *output*. Law 3 — every elision is reversible — needs\n * this: {@link Reconstructor} splices stored bytes back over these ranges. Without it,\n * \"reversible\" would mean parsing markers back out of the text, which is a guess.\n * This is a fact recorded at the moment of the cut.\n */\n readonly outputRange: ByteRange;\n /** Size of the removed content, in UTF-8 bytes. */\n readonly bytes: number;\n readonly reason: ElisionReason;\n /** The exact marker text substituted into the output. */\n readonly marker: string;\n}\n\n/**\n * A consumer-supplied counter, so a caller who bills in tokens can *see* tokens.\n *\n * Budgets in smelt's core are UTF-8 bytes, permanently — bytes are the only unit that\n * is computable locally for every model, and they mean the same thing in five years.\n * See `docs/ARCHITECTURE.md` § \"Decision 1\". This hook does not change that: it adds a\n * second, labelled number to the result. The plan is still made in bytes.\n *\n * Both `id` and `unit` are required, and that is a Law 4 decision rather than\n * bookkeeping: a token count is meaningless without naming the tokenizer that produced\n * it. Anthropic's own docs record that Claude 4.7 and later use a newer tokenizer where\n * the same text yields roughly 30% more tokens than on earlier models — so `1,204\n * tokens` is not a fact, and `1,204 tokens (claude-4.7/count_tokens)` is.\n *\n * **This hook does not relax Law 1.** smelt imports no transport and the guard proves\n * that about smelt's own modules; it cannot prove it about a function you hand in. A\n * `count()` that calls an API makes *your* process call an API, from a line in *your*\n * source — exactly the arrangement {@link RerankStage} already describes. `count` is\n * synchronous on purpose: local tokenizers are synchronous, and network clients are not.\n */\nexport interface Measure {\n /** Names the counter, e.g. `'tiktoken/o200k_base'` or `'claude-4.7/count_tokens'`. */\n readonly id: string;\n /** The unit `count()` returns, e.g. `'tokens'`. Printed next to the number. */\n readonly unit: string;\n /** Local, synchronous count over the whole string. */\n count(text: string): number;\n}\n\n/** A second size for a result, in someone else's unit, with the counter named. */\nexport interface MeasuredSize {\n /** {@link Measure.id} of the counter that produced these numbers. */\n readonly measure: string;\n /** {@link Measure.unit}. */\n readonly unit: string;\n readonly input: number;\n readonly output: number;\n}\n\n/** The result of smelting one blob of text. */\nexport interface SmeltResult {\n readonly text: string;\n readonly inputBytes: number;\n readonly outputBytes: number;\n readonly planner: string;\n readonly language: DetectedLanguage;\n readonly elisions: readonly AppliedElision[];\n /** Present only when the caller supplied a {@link Measure}. Never invented. */\n readonly measured?: MeasuredSize;\n}\n\n/**\n * Reversibility, as a callable. Takes a {@link SmeltResult} and the store that holds its\n * elided bytes, and returns the original text — byte for byte.\n */\nexport type Reconstructor = (result: SmeltResult, store: ElisionStore) => string;\n\n// ---------------------------------------------------------------------------\n// The store, and the counters that make over-pruning visible\n// ---------------------------------------------------------------------------\n\n/**\n * The numbers that keep smelt honest about itself.\n *\n * Law 3 says elisions are reversible. That is cheap to satisfy and easy to abuse: a\n * compressor that cuts everything is \"reversible\" and useless. The *retrieve rate* is\n * the tell. If the model keeps calling `smelt_retrieve`, smelt cut material the task\n * needed, and the round trip cost more tokens than the elision saved.\n *\n * So: `expansionRate` is not telemetry. It is the metric a caller tunes budgets\n * against, and the only number smelt is willing to have an opinion about — because it\n * measures it locally, per session, on the caller's own traffic.\n */\nexport interface RetrieveStats {\n /** Distinct blobs put into the store. */\n readonly elisionsStored: number;\n /** Total bytes held by the store. */\n readonly bytesStored: number;\n /** Every `retrieve()` call, including repeats and misses. */\n readonly retrieveCalls: number;\n /** Distinct hashes successfully retrieved at least once. */\n readonly uniqueRetrieved: number;\n /** Calls for a hash the store does not hold. Non-zero means a bug, not over-pruning. */\n readonly misses: number;\n /**\n * `uniqueRetrieved / elisionsStored`, or `0` when nothing has been stored.\n *\n * Read it as: *what fraction of what smelt hid did the model have to ask for back?*\n * There is no universally right value, and smelt does not ship a threshold it has\n * not measured. Rising across a workload is the signal.\n */\n readonly expansionRate: number;\n /**\n * The one degenerate outcome smelt is willing to name: **every distinct blob it hid\n * was asked for again.**\n *\n * smelt ships no expansion-rate threshold, because a threshold is a policy claim it\n * has no basis for and the right rate depends on how aggressive a budget the caller\n * chose — and a library printing warnings into someone else's process is bad manners.\n * This is not a threshold. At `uniqueRetrieved === elisionsStored` the elision\n * achieved nothing and cost a round trip: an arithmetic fact, not a preference. What\n * to do about it is the caller's call.\n *\n * `false` for an empty store — nothing was hidden, so nothing was defeated.\n */\n readonly allElisionsRetrieved: boolean;\n}\n\n/**\n * Local, content-addressed storage for elided bytes. No network, no eviction in v1 —\n * evicting is how \"reversible\" quietly becomes \"reversible for a while\".\n */\nexport interface ElisionStore {\n /** Store content, returning its hash. Idempotent for identical content. */\n put(content: string): string;\n /** The stored content, or `undefined` if this store never held that hash. */\n peek(hash: string): string | undefined;\n /**\n * The stored content, *counted* as a retrieval. This is what the model's tool calls.\n *\n * @throws {UnknownHashError} when the hash is unknown.\n * @throws {StoreCorruptionError} — {@link DirectoryElisionStore} only — when the\n * bytes on disk no longer hash to their own name. Distinct from\n * `UnknownHashError` on purpose: \"we hold damaged bytes\" and \"it never existed\"\n * are different answers, and a caller that conflates them will report the wrong\n * one to its user.\n *\n * A {@link DirectoryElisionStore} whose journal cannot be written (a read-only\n * store directory, a full disk) still returns the bytes — verified bytes are never\n * withheld over a bookkeeping failure — and surfaces the lost count as a\n * `process.emitWarning` named `SmeltCounterWriteFailure` instead of throwing. Its\n * stats go quiet from that point; the retrieval itself succeeded.\n */\n retrieve(hash: string): string;\n /**\n * Whether this hash can be retrieved — **not** merely whether a slot bearing it\n * exists. `true` must mean the very next `retrieve(hash)` returns bytes, so a store\n * that verifies content on read verifies here too and raises its corruption error\n * rather than answering `true` for bytes it would then refuse. Uncounted: a check is\n * not the model asking for material back.\n */\n has(hash: string): boolean;\n /** A snapshot of the counters. See {@link RetrieveStats}. */\n stats(): RetrieveStats;\n}\n\n/**\n * The retrieval tool a consumer exposes to its model. Deliberately not an MCP or\n * provider-specific shape — smelt does not know which SDK you use. The consumer adapts\n * this into its own tool schema; the contract is `hash in, exact bytes out`.\n */\nexport interface RetrieveTool {\n /** `'smelt_retrieve'`. Stable — consumers hard-code it in prompts. */\n readonly name: string;\n /** Prose the consumer can put straight into a tool description. */\n readonly description: string;\n /**\n * JSON-Schema-shaped parameter description, for consumers that want one.\n *\n * Strict-mode shaped: `additionalProperties: false` and a `required` naming every\n * property, so a consumer registering this under OpenAI's structured-outputs strict\n * mode is not refused at registration. See {@link createRetrieveTool}.\n */\n readonly inputSchema: {\n readonly type: 'object';\n readonly properties: {\n readonly hash: { readonly type: 'string'; readonly description: string };\n };\n readonly required: readonly ['hash'];\n readonly additionalProperties: false;\n };\n /**\n * @throws {UnknownHashError} when the hash is unknown.\n * @throws {StoreCorruptionError} when the backing store holds damaged bytes for\n * that hash — see {@link ElisionStore.retrieve}, whose contract this forwards\n * verbatim. Surface either to the model as a tool error, never as empty text.\n */\n invoke(input: { readonly hash: string }): string;\n}\n\n// ---------------------------------------------------------------------------\n// Pluggable stages — interfaces in v1, nothing more\n// ---------------------------------------------------------------------------\n\n/** A candidate handed to a {@link RerankStage}: an opaque id plus the text to judge. */\nexport interface RerankCandidate {\n readonly id: string;\n readonly text: string;\n}\n\n/** A reranked candidate, most relevant first. `score` is the stage's own scale. */\nexport interface RerankedCandidate extends RerankCandidate {\n readonly score: number;\n}\n\n/**\n * Relevance reranking — a *seam*, not a feature.\n *\n * Hosted rerankers are good and smelt will never bundle one, because bundling would\n * break Law 1: the moment smelt ships a default reranker, `smelt()` can make a network\n * call that the caller did not ask for and cannot see. A consumer that wants one\n * implements this interface, wires its own key, and owns the fact that its context now\n * leaves the machine. That decision must be legible in the consumer's own source.\n */\nexport interface RerankStage {\n readonly id: string;\n /** May make network calls — that is the consumer's choice, made in the consumer's code. */\n rerank(\n candidates: readonly RerankCandidate[],\n query: string,\n ): Promise<readonly RerankedCandidate[]>;\n}\n\n/**\n * Learned distillation — rewriting content with a model instead of cutting it.\n *\n * Out of v1 for a reason beyond the network: a distilled paragraph cannot satisfy\n * Law 2. \"The model summarised this\" is not an explanation of what was removed, and\n * the removed material is no longer recoverable from the output. If this ever ships,\n * it ships as a stage that stores the original and explains itself in the same terms\n * every other rule does.\n */\nexport interface DistillStage {\n readonly id: string;\n distill(text: string, budgetBytes: number): Promise<string>;\n}\n"]}
|