@smeltjs/core 0.5.0 → 0.6.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.
Files changed (97) hide show
  1. package/dist/apply.d.ts.map +1 -1
  2. package/dist/apply.js +8 -2
  3. package/dist/apply.js.map +1 -1
  4. package/dist/cli/init.d.ts.map +1 -1
  5. package/dist/cli/init.js +4 -2
  6. package/dist/cli/init.js.map +1 -1
  7. package/dist/cli/report.d.ts +10 -1
  8. package/dist/cli/report.d.ts.map +1 -1
  9. package/dist/cli/report.js +16 -1
  10. package/dist/cli/report.js.map +1 -1
  11. package/dist/cli/subcommands/flags.d.ts +3 -0
  12. package/dist/cli/subcommands/flags.d.ts.map +1 -1
  13. package/dist/cli/subcommands/flags.js +16 -3
  14. package/dist/cli/subcommands/flags.js.map +1 -1
  15. package/dist/cli/subcommands/smelt.d.ts +3 -0
  16. package/dist/cli/subcommands/smelt.d.ts.map +1 -1
  17. package/dist/cli/subcommands/smelt.js +5 -1
  18. package/dist/cli/subcommands/smelt.js.map +1 -1
  19. package/dist/cli/subcommands/stats.d.ts +8 -3
  20. package/dist/cli/subcommands/stats.d.ts.map +1 -1
  21. package/dist/cli/subcommands/stats.js +15 -5
  22. package/dist/cli/subcommands/stats.js.map +1 -1
  23. package/dist/errors.d.ts +10 -0
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +10 -0
  26. package/dist/errors.js.map +1 -1
  27. package/dist/hooks/focus-terms.d.ts +57 -0
  28. package/dist/hooks/focus-terms.d.ts.map +1 -0
  29. package/dist/hooks/focus-terms.js +230 -0
  30. package/dist/hooks/focus-terms.js.map +1 -0
  31. package/dist/hooks/guard-core.d.ts +8 -16
  32. package/dist/hooks/guard-core.d.ts.map +1 -1
  33. package/dist/hooks/guard-core.js +22 -120
  34. package/dist/hooks/guard-core.js.map +1 -1
  35. package/dist/index.d.ts +4 -4
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +3 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/ops/index.d.ts +3 -3
  40. package/dist/ops/index.d.ts.map +1 -1
  41. package/dist/ops/index.js +2 -2
  42. package/dist/ops/index.js.map +1 -1
  43. package/dist/ops/verbs.d.ts +57 -1
  44. package/dist/ops/verbs.d.ts.map +1 -1
  45. package/dist/ops/verbs.js +49 -1
  46. package/dist/ops/verbs.js.map +1 -1
  47. package/dist/plan/auto.d.ts +9 -2
  48. package/dist/plan/auto.d.ts.map +1 -1
  49. package/dist/plan/auto.js +14 -2
  50. package/dist/plan/auto.js.map +1 -1
  51. package/dist/plan/diff.d.ts +45 -0
  52. package/dist/plan/diff.d.ts.map +1 -0
  53. package/dist/plan/diff.js +284 -0
  54. package/dist/plan/diff.js.map +1 -0
  55. package/dist/plan/json.d.ts +37 -0
  56. package/dist/plan/json.d.ts.map +1 -0
  57. package/dist/plan/json.js +181 -0
  58. package/dist/plan/json.js.map +1 -0
  59. package/dist/plan/kind.d.ts +28 -0
  60. package/dist/plan/kind.d.ts.map +1 -0
  61. package/dist/plan/kind.js +57 -0
  62. package/dist/plan/kind.js.map +1 -0
  63. package/dist/plan/offsets.d.ts +8 -0
  64. package/dist/plan/offsets.d.ts.map +1 -0
  65. package/dist/plan/offsets.js +19 -0
  66. package/dist/plan/offsets.js.map +1 -0
  67. package/dist/plan/planners.d.ts +15 -5
  68. package/dist/plan/planners.d.ts.map +1 -1
  69. package/dist/plan/planners.js +13 -5
  70. package/dist/plan/planners.js.map +1 -1
  71. package/dist/plan/structural.d.ts.map +1 -1
  72. package/dist/plan/structural.js +106 -14
  73. package/dist/plan/structural.js.map +1 -1
  74. package/dist/retrieve.d.ts +22 -1
  75. package/dist/retrieve.d.ts.map +1 -1
  76. package/dist/retrieve.js +59 -0
  77. package/dist/retrieve.js.map +1 -1
  78. package/dist/smelter.d.ts +4 -0
  79. package/dist/smelter.d.ts.map +1 -1
  80. package/dist/smelter.js +4 -0
  81. package/dist/smelter.js.map +1 -1
  82. package/dist/stats.d.ts +16 -1
  83. package/dist/stats.d.ts.map +1 -1
  84. package/dist/stats.js +29 -0
  85. package/dist/stats.js.map +1 -1
  86. package/dist/store-dir.d.ts +9 -2
  87. package/dist/store-dir.d.ts.map +1 -1
  88. package/dist/store-dir.js +48 -7
  89. package/dist/store-dir.js.map +1 -1
  90. package/dist/store.d.ts +4 -2
  91. package/dist/store.d.ts.map +1 -1
  92. package/dist/store.js +12 -2
  93. package/dist/store.js.map +1 -1
  94. package/dist/types.d.ts +116 -2
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/types.js.map +1 -1
  97. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS;aACpB,MAAM;iBAAI,IAAI,EAAE,QAAQ;;aACxB,KAAK;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aACjC,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,MAAM;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aAClC,KAAK;iBAAI,IAAI,EAAE,QAAQ;;IACvB;;;;;OAKG;aACH,OAAO;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aACnC,GAAG;iBAAI,IAAI,EAAE,SAAS;;aACtB,QAAQ;iBAAI,IAAI,EAAE,SAAS;;aAC3B,MAAM;iBAAI,IAAI,EAAE,SAAS;;aACzB,IAAI;iBAAI,IAAI,EAAE,SAAS;;aACvB,WAAW;iBAAI,IAAI,EAAE,SAAS;;aAC9B,IAAI;iBAAI,IAAI,EAAE,SAAS;iBAAE,KAAK,EAAE,GAAG;;aACnC,OAAO;iBAAI,IAAI,EAAE,SAAS;;CAClB,CAAC;AAEX,uFAAuF;AACvF,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,SAAS,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,YAAY,8BAA6D,CAAC;AAEvF,kEAAkE;AAClE,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAExE,oFAAoF;AACpF,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAEzC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAI3D;AAED,iGAAiG;AACjG,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACtD,OAAO,GACP,CAAC,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GACnC,SAAS,MAAM,EAAE,GACjB,MAAM,CAAC;AAEb;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,EAAE,CAAC,IAAI,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEzF,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,IAAI,IAAI,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAerF;AAKD;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CA2G1D,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOvE","sourcesContent":["import { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESS_IDS } from '../../harness/registry.ts';\nimport { budgetFault, budgetMalformed } from '../../ops/inputs.ts';\nimport type { BudgetFault } from '../../ops/inputs.ts';\nimport { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.ts';\nimport { STRUCTURAL_LANGUAGES } from '../../plan/structural.ts';\nimport { DEFAULT_REPO_IGNORE } from '../../repomap/map.ts';\nimport { CLI_NAME } from '../shell.ts';\n\n/**\n * Every flag the CLI accepts, once.\n *\n * The table is the companion the {@link Subcommand} registry needs: a verb declares\n * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether\n * a verb can claim a flag that does not exist — comes from here. It carries three\n * things that used to be written in three places:\n *\n * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a\n * literal inside `parseSmeltArgs`.\n * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is\n * spelled once and every verb's ownership list typechecks against it.\n * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`\n * prefix, which is *not* stored here: it is generated from the registry's\n * ownership (see `renderOptions` in `cli/usage.ts`), because \"which verb owns\n * this flag\" is a fact the registry already holds and prose that restates it is\n * prose that can go stale.\n *\n * Key order is meaningful: it is the order the OPTIONS block renders and the order a\n * refusal lists flags in, so keep it stable and append new flags where they read best.\n */\nexport const CLI_FLAGS = {\n budget: { type: 'string' },\n focus: { type: 'string', multiple: true },\n language: { type: 'string' },\n strategy: { type: 'string' },\n ignore: { type: 'string', multiple: true },\n cache: { type: 'string' },\n /**\n * Repeatable because `setup` wires several harnesses in one run. `hooks` takes one\n * per run and refuses a second id in its own parse — arity is a per-verb fact,\n * expressed where the verb validates, with the one generated refusal; the table\n * records how argv is read, not how many a verb accepts.\n */\n harness: { type: 'string', multiple: true },\n yes: { type: 'boolean' },\n 'no-mcp': { type: 'boolean' },\n strict: { type: 'boolean' },\n json: { type: 'boolean' },\n reconstruct: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean' },\n} as const;\n\n/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */\nexport type FlagName = keyof typeof CLI_FLAGS;\n\n/**\n * The flags answered *before* any verb, so no verb owns them and no verb may refuse\n * them: `smelt map --help` prints the help, exactly as it always has. Every other flag\n * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.\n */\nexport const GLOBAL_FLAGS = ['help', 'version'] as const satisfies readonly FlagName[];\n\n/** A flag a verb can own — everything but the two global ones. */\nexport type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;\n\n/** Every ownable flag, in table order: the list a refusal checks a verb against. */\nexport const VERB_FLAGS: readonly VerbFlag[] = Object.keys(CLI_FLAGS).filter(\n (name): name is VerbFlag => !(GLOBAL_FLAGS as readonly string[]).includes(name),\n);\n\n/**\n * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.\n *\n * Every refusal that names more than one flag spells the list this way: the\n * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`\n * refusal in `./smelt.ts`. It lives with the table because the *order* is the\n * table's — a refusal that listed flags in the order the user happened to type them\n * would read differently every time, and two refusals disagreeing about how to spell\n * the same pair is the kind of drift this file exists to remove.\n */\nexport function flagList(flags: readonly VerbFlag[]): string {\n const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);\n if (spelled.length <= 1) return spelled.join('');\n return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;\n}\n\n/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */\ntype FlagValue<F> = F extends { readonly type: 'boolean' }\n ? boolean\n : F extends { readonly multiple: true }\n ? readonly string[]\n : string;\n\n/**\n * The parsed flags, as every `Subcommand.parse` sees them: one optional field per\n * flag, typed by the table above. Absent means the user did not type it — which is\n * the only thing a refusal needs to know, and the only thing a verb may act on.\n */\nexport type FlagValues = { readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]> };\n\n/** How one flag appears in the OPTIONS block of `--help`. */\nexport interface FlagHelp {\n /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */\n readonly label: string;\n /**\n * The description, already wrapped to the OPTIONS column — a function because three\n * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +\n * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The\n * ownership prefix is not here; it is generated.\n */\n body(): readonly string[];\n}\n\n/**\n * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the\n * hand-typed version wrapped. `--strategy` and `--language` render their registries on\n * one line because they fit; the harness ids do not, and a list long enough to wrap is\n * exactly the list nobody keeps in sync by hand.\n */\nexport function optionList(items: readonly string[], width: number): readonly string[] {\n const lines: string[] = [];\n let line = '';\n items.forEach((item, index) => {\n const word = index === items.length - 1 ? `${item}.` : `${item},`;\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = candidate;\n }\n });\n lines.push(line);\n return lines;\n}\n\n/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */\nconst OPTION_BODY_WIDTH = 65;\n\n/**\n * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag\n * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text\n * cannot fall behind what the parser accepts.\n */\nexport const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>> = {\n budget: {\n label: '--budget <bytes>',\n body: () => [\n 'Required, unless smelt.config.json sets defaultBudgetBytes.',\n 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',\n 'ceiling, met by construction). No built-in default: a budget',\n `${CLI_NAME} invented would decide for you.`,\n ],\n },\n focus: {\n label: '--focus <term>',\n body: () => [\n 'What you were looking for. Repeatable. Matching regions and',\n 'their context survive; the runs between them collapse. For',\n 'map: symbols matching a term (by name or path) are promoted',\n 'to the front of the fill order, ranks unchanged.',\n ],\n },\n language: {\n label: '--language <id>',\n body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],\n },\n strategy: {\n label: '--strategy <id>',\n body: () => [\n `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,\n 'smelt.config.json says otherwise. structural parses',\n `${STRUCTURAL_LANGUAGES.join(', ')};`,\n 'any other language is refused, never approximated. auto picks',\n 'structural for those languages and lexical for the rest, and',\n 'the report names whichever one actually ran.',\n ],\n },\n ignore: {\n label: '--ignore <entry>',\n body: () => [\n 'Repeatable. Replaces the default ignore list',\n // Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default\n // by hand is help text that will one day describe a different default.\n `(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,\n 'an entry containing / is a root-relative prefix.',\n ],\n },\n cache: {\n label: '--cache <dir>',\n body: () => [\n 'Directory for the tags cache, keyed by content',\n 'hash. Only when given does the map write to disk at all.',\n ],\n },\n harness: {\n label: '--harness <id>',\n body: () => [\n 'Skip harness detection and target one id:',\n ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),\n 'Repeatable for setup; hooks takes one per run.',\n ],\n },\n yes: {\n label: '--yes',\n body: () => [\n \"Non-interactive setup: the recipe's defaults, printed\",\n 'loudly as they are applied. Existing files are never',\n 'overwritten — skipped with a note; hooks install edits',\n 'them, and it asks per file.',\n ],\n },\n 'no-mcp': {\n label: '--no-mcp',\n body: () => [\n 'Setup only: skip the MCP registration step — the',\n 'printed command and its note — for a hooks-only',\n 'setup.',\n ],\n },\n strict: {\n label: '--strict',\n body: () => [\n 'Turn any lint finding into exit 1, for CI. Findings are',\n 'advisory by default: the rules are heuristics about somebody',\n \"else's house style, and enforcing them uninvited would be\",\n `${CLI_NAME} deciding it. A budget you set yourself is different —`,\n 'exceeding agents.budgetBytes exits 1 with or without --strict.',\n ],\n },\n json: {\n label: '--json',\n body: () => [\n 'Print a JSON envelope on stdout instead of the text:',\n '{ format, result, elided } for a smelt run — `result` is',\n 'the SmeltResult verbatim, `elided` carries the bytes, so',\n 'the envelope can be reconstructed; feed it back with',\n '--reconstruct. For map: { format, map }, the RepoMap',\n 'structure verbatim. For agents lint: { format, report },',\n 'the measured levels and every finding with its rule id.',\n ],\n },\n reconstruct: {\n label: '--reconstruct',\n body: () => [\n 'Read a --json envelope and print the original text, byte for',\n 'byte. This is Law 3 you can run from a shell.',\n ],\n },\n help: { label: '-h, --help', body: () => ['This text.'] },\n version: { label: '--version', body: () => ['The package version.'] },\n};\n\n/**\n * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget\n * smelt invented would be smelt deciding how much of the caller's context to throw\n * away, silently, at a number nobody chose. A *missing* flag is not an error here,\n * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose\n * explicitly, and the verb's `resolve` errors only when neither exists. A malformed\n * value is always an error.\n *\n * It lives with the flag rather than with a verb because two verbs own `--budget`, and\n * the two of them agreeing on what \"4kb\" means is not something to leave to chance.\n *\n * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a\n * leading `-` are answered by a digits-only test before anything numeric happens. The\n * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also\n * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`\n * here and `\"budgetBytes\"` there.\n */\nexport function parseBudget(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw)) throw refuseBudget('not-an-integer', raw);\n const value = Number(raw);\n const fault = budgetFault(value);\n if (fault !== undefined) throw refuseBudget(fault, raw);\n return value;\n}\n\n/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */\nfunction refuseBudget(fault: BudgetFault, raw: string): CliUsageError {\n return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);\n}\n"]}
1
+ {"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS;aACpB,MAAM;iBAAI,IAAI,EAAE,QAAQ;;aACxB,KAAK;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aACjC,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,QAAQ;iBAAI,IAAI,EAAE,QAAQ;;aAC1B,MAAM;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aAClC,KAAK;iBAAI,IAAI,EAAE,QAAQ;;IACvB;;;;;OAKG;aACH,OAAO;iBAAI,IAAI,EAAE,QAAQ;iBAAE,QAAQ;;aACnC,GAAG;iBAAI,IAAI,EAAE,SAAS;;aACtB,QAAQ;iBAAI,IAAI,EAAE,SAAS;;aAC3B,MAAM;iBAAI,IAAI,EAAE,SAAS;;aACzB,IAAI;iBAAI,IAAI,EAAE,SAAS;;aACvB,WAAW;iBAAI,IAAI,EAAE,SAAS;;aAC9B,IAAI;iBAAI,IAAI,EAAE,SAAS;iBAAE,KAAK,EAAE,GAAG;;aACnC,OAAO;iBAAI,IAAI,EAAE,SAAS;;CAClB,CAAC;AAEX,uFAAuF;AACvF,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,SAAS,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,YAAY,8BAA6D,CAAC;AAEvF,kEAAkE;AAClE,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAExE,oFAAoF;AACpF,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAEzC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAI3D;AAED,iGAAiG;AACjG,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACtD,OAAO,GACP,CAAC,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GACnC,SAAS,MAAM,EAAE,GACjB,MAAM,CAAC;AAEb;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,EAAE,CAAC,IAAI,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEzF,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,IAAI,IAAI,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAerF;AAKD;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAuH1D,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOvE","sourcesContent":["import { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESS_IDS } from '../../harness/registry.ts';\nimport { budgetFault, budgetMalformed } from '../../ops/inputs.ts';\nimport type { BudgetFault } from '../../ops/inputs.ts';\nimport { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.ts';\nimport { STRUCTURAL_LANGUAGES } from '../../plan/structural.ts';\nimport { DEFAULT_REPO_IGNORE } from '../../repomap/map.ts';\nimport { CLI_NAME } from '../shell.ts';\n\n/**\n * Every flag the CLI accepts, once.\n *\n * The table is the companion the {@link Subcommand} registry needs: a verb declares\n * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether\n * a verb can claim a flag that does not exist — comes from here. It carries three\n * things that used to be written in three places:\n *\n * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a\n * literal inside `parseSmeltArgs`.\n * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is\n * spelled once and every verb's ownership list typechecks against it.\n * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`\n * prefix, which is *not* stored here: it is generated from the registry's\n * ownership (see `renderOptions` in `cli/usage.ts`), because \"which verb owns\n * this flag\" is a fact the registry already holds and prose that restates it is\n * prose that can go stale.\n *\n * Key order is meaningful: it is the order the OPTIONS block renders and the order a\n * refusal lists flags in, so keep it stable and append new flags where they read best.\n */\nexport const CLI_FLAGS = {\n budget: { type: 'string' },\n focus: { type: 'string', multiple: true },\n producer: { type: 'string' },\n language: { type: 'string' },\n strategy: { type: 'string' },\n ignore: { type: 'string', multiple: true },\n cache: { type: 'string' },\n /**\n * Repeatable because `setup` wires several harnesses in one run. `hooks` takes one\n * per run and refuses a second id in its own parse — arity is a per-verb fact,\n * expressed where the verb validates, with the one generated refusal; the table\n * records how argv is read, not how many a verb accepts.\n */\n harness: { type: 'string', multiple: true },\n yes: { type: 'boolean' },\n 'no-mcp': { type: 'boolean' },\n strict: { type: 'boolean' },\n json: { type: 'boolean' },\n reconstruct: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean' },\n} as const;\n\n/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */\nexport type FlagName = keyof typeof CLI_FLAGS;\n\n/**\n * The flags answered *before* any verb, so no verb owns them and no verb may refuse\n * them: `smelt map --help` prints the help, exactly as it always has. Every other flag\n * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.\n */\nexport const GLOBAL_FLAGS = ['help', 'version'] as const satisfies readonly FlagName[];\n\n/** A flag a verb can own — everything but the two global ones. */\nexport type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;\n\n/** Every ownable flag, in table order: the list a refusal checks a verb against. */\nexport const VERB_FLAGS: readonly VerbFlag[] = Object.keys(CLI_FLAGS).filter(\n (name): name is VerbFlag => !(GLOBAL_FLAGS as readonly string[]).includes(name),\n);\n\n/**\n * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.\n *\n * Every refusal that names more than one flag spells the list this way: the\n * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`\n * refusal in `./smelt.ts`. It lives with the table because the *order* is the\n * table's — a refusal that listed flags in the order the user happened to type them\n * would read differently every time, and two refusals disagreeing about how to spell\n * the same pair is the kind of drift this file exists to remove.\n */\nexport function flagList(flags: readonly VerbFlag[]): string {\n const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);\n if (spelled.length <= 1) return spelled.join('');\n return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;\n}\n\n/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */\ntype FlagValue<F> = F extends { readonly type: 'boolean' }\n ? boolean\n : F extends { readonly multiple: true }\n ? readonly string[]\n : string;\n\n/**\n * The parsed flags, as every `Subcommand.parse` sees them: one optional field per\n * flag, typed by the table above. Absent means the user did not type it — which is\n * the only thing a refusal needs to know, and the only thing a verb may act on.\n */\nexport type FlagValues = { readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]> };\n\n/** How one flag appears in the OPTIONS block of `--help`. */\nexport interface FlagHelp {\n /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */\n readonly label: string;\n /**\n * The description, already wrapped to the OPTIONS column — a function because three\n * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +\n * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The\n * ownership prefix is not here; it is generated.\n */\n body(): readonly string[];\n}\n\n/**\n * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the\n * hand-typed version wrapped. `--strategy` and `--language` render their registries on\n * one line because they fit; the harness ids do not, and a list long enough to wrap is\n * exactly the list nobody keeps in sync by hand.\n */\nexport function optionList(items: readonly string[], width: number): readonly string[] {\n const lines: string[] = [];\n let line = '';\n items.forEach((item, index) => {\n const word = index === items.length - 1 ? `${item}.` : `${item},`;\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = candidate;\n }\n });\n lines.push(line);\n return lines;\n}\n\n/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */\nconst OPTION_BODY_WIDTH = 65;\n\n/**\n * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag\n * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text\n * cannot fall behind what the parser accepts.\n */\nexport const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>> = {\n budget: {\n label: '--budget <bytes>',\n body: () => [\n 'Required, unless smelt.config.json sets defaultBudgetBytes.',\n 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',\n 'ceiling, met by construction). No built-in default: a budget',\n `${CLI_NAME} invented would decide for you.`,\n ],\n },\n focus: {\n label: '--focus <term>',\n body: () => [\n 'What you were looking for. Repeatable. Matching regions and',\n 'their context survive; the runs between them collapse. For',\n 'map: symbols matching a term (by name or path) are promoted',\n 'to the front of the fill order, ranks unchanged.',\n ],\n },\n producer: {\n label: '--producer <cmd>',\n body: () => [\n 'The command whose output this is, e.g. \"grep -C 3 foo src\". When',\n 'no --focus is given, the focus is derived from it exactly as the',\n 'hooks guard derives it: a search pattern, only when the output',\n 'also holds non-matching lines (context flags). cat, diffs and',\n 'logs name no term; the head and tail are kept instead.',\n ],\n },\n language: {\n label: '--language <id>',\n body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],\n },\n strategy: {\n label: '--strategy <id>',\n body: () => [\n `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,\n 'smelt.config.json says otherwise. structural parses',\n `${STRUCTURAL_LANGUAGES.join(', ')};`,\n 'any other language is refused, never approximated. json cuts',\n 'members and elements; diff cuts files and hunks; each refuses',\n 'any other content. auto picks by content kind first (json,',\n 'diff), then structural for those languages and lexical for the',\n 'rest, and the report names whichever one actually ran.',\n ],\n },\n ignore: {\n label: '--ignore <entry>',\n body: () => [\n 'Repeatable. Replaces the default ignore list',\n // Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default\n // by hand is help text that will one day describe a different default.\n `(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,\n 'an entry containing / is a root-relative prefix.',\n ],\n },\n cache: {\n label: '--cache <dir>',\n body: () => [\n 'Directory for the tags cache, keyed by content',\n 'hash. Only when given does the map write to disk at all.',\n ],\n },\n harness: {\n label: '--harness <id>',\n body: () => [\n 'Skip harness detection and target one id:',\n ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),\n 'Repeatable for setup; hooks takes one per run.',\n ],\n },\n yes: {\n label: '--yes',\n body: () => [\n \"Non-interactive setup: the recipe's defaults, printed\",\n 'loudly as they are applied. Existing files are never',\n 'overwritten — skipped with a note; hooks install edits',\n 'them, and it asks per file.',\n ],\n },\n 'no-mcp': {\n label: '--no-mcp',\n body: () => [\n 'Setup only: skip the MCP registration step — the',\n 'printed command and its note — for a hooks-only',\n 'setup.',\n ],\n },\n strict: {\n label: '--strict',\n body: () => [\n 'Turn any lint finding into exit 1, for CI. Findings are',\n 'advisory by default: the rules are heuristics about somebody',\n \"else's house style, and enforcing them uninvited would be\",\n `${CLI_NAME} deciding it. A budget you set yourself is different —`,\n 'exceeding agents.budgetBytes exits 1 with or without --strict.',\n ],\n },\n json: {\n label: '--json',\n body: () => [\n 'Print a JSON envelope on stdout instead of the text:',\n '{ format, result, elided } for a smelt run — `result` is',\n 'the SmeltResult verbatim, `elided` carries the bytes, so',\n 'the envelope can be reconstructed; feed it back with',\n '--reconstruct. For map: { format, map }, the RepoMap',\n 'structure verbatim. For agents lint: { format, report },',\n 'the measured levels and every finding with its rule id.',\n ],\n },\n reconstruct: {\n label: '--reconstruct',\n body: () => [\n 'Read a --json envelope and print the original text, byte for',\n 'byte. This is Law 3 you can run from a shell.',\n ],\n },\n help: { label: '-h, --help', body: () => ['This text.'] },\n version: { label: '--version', body: () => ['The package version.'] },\n};\n\n/**\n * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget\n * smelt invented would be smelt deciding how much of the caller's context to throw\n * away, silently, at a number nobody chose. A *missing* flag is not an error here,\n * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose\n * explicitly, and the verb's `resolve` errors only when neither exists. A malformed\n * value is always an error.\n *\n * It lives with the flag rather than with a verb because two verbs own `--budget`, and\n * the two of them agreeing on what \"4kb\" means is not something to leave to chance.\n *\n * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a\n * leading `-` are answered by a digits-only test before anything numeric happens. The\n * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also\n * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`\n * here and `\"budgetBytes\"` there.\n */\nexport function parseBudget(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw)) throw refuseBudget('not-an-integer', raw);\n const value = Number(raw);\n const fault = budgetFault(value);\n if (fault !== undefined) throw refuseBudget(fault, raw);\n return value;\n}\n\n/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */\nfunction refuseBudget(fault: BudgetFault, raw: string): CliUsageError {\n return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);\n}\n"]}
@@ -30,6 +30,7 @@ import { CLI_NAME } from '../shell.js';
30
30
  export const CLI_FLAGS = {
31
31
  budget: { type: 'string' },
32
32
  focus: { type: 'string', multiple: true },
33
+ producer: { type: 'string' },
33
34
  language: { type: 'string' },
34
35
  strategy: { type: 'string' },
35
36
  ignore: { type: 'string', multiple: true },
@@ -122,6 +123,16 @@ export const FLAG_HELP = {
122
123
  'to the front of the fill order, ranks unchanged.',
123
124
  ],
124
125
  },
126
+ producer: {
127
+ label: '--producer <cmd>',
128
+ body: () => [
129
+ 'The command whose output this is, e.g. "grep -C 3 foo src". When',
130
+ 'no --focus is given, the focus is derived from it exactly as the',
131
+ 'hooks guard derives it: a search pattern, only when the output',
132
+ 'also holds non-matching lines (context flags). cat, diffs and',
133
+ 'logs name no term; the head and tail are kept instead.',
134
+ ],
135
+ },
125
136
  language: {
126
137
  label: '--language <id>',
127
138
  body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],
@@ -132,9 +143,11 @@ export const FLAG_HELP = {
132
143
  `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,
133
144
  'smelt.config.json says otherwise. structural parses',
134
145
  `${STRUCTURAL_LANGUAGES.join(', ')};`,
135
- 'any other language is refused, never approximated. auto picks',
136
- 'structural for those languages and lexical for the rest, and',
137
- 'the report names whichever one actually ran.',
146
+ 'any other language is refused, never approximated. json cuts',
147
+ 'members and elements; diff cuts files and hunks; each refuses',
148
+ 'any other content. auto picks by content kind first (json,',
149
+ 'diff), then structural for those languages and lexical for the',
150
+ 'rest, and the report names whichever one actually ran.',
138
151
  ],
139
152
  },
140
153
  ignore: {
@@ -1 +1 @@
1
- {"version":3,"file":"flags.js","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"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","sourcesContent":["import { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESS_IDS } from '../../harness/registry.ts';\nimport { budgetFault, budgetMalformed } from '../../ops/inputs.ts';\nimport type { BudgetFault } from '../../ops/inputs.ts';\nimport { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.ts';\nimport { STRUCTURAL_LANGUAGES } from '../../plan/structural.ts';\nimport { DEFAULT_REPO_IGNORE } from '../../repomap/map.ts';\nimport { CLI_NAME } from '../shell.ts';\n\n/**\n * Every flag the CLI accepts, once.\n *\n * The table is the companion the {@link Subcommand} registry needs: a verb declares\n * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether\n * a verb can claim a flag that does not exist — comes from here. It carries three\n * things that used to be written in three places:\n *\n * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a\n * literal inside `parseSmeltArgs`.\n * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is\n * spelled once and every verb's ownership list typechecks against it.\n * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`\n * prefix, which is *not* stored here: it is generated from the registry's\n * ownership (see `renderOptions` in `cli/usage.ts`), because \"which verb owns\n * this flag\" is a fact the registry already holds and prose that restates it is\n * prose that can go stale.\n *\n * Key order is meaningful: it is the order the OPTIONS block renders and the order a\n * refusal lists flags in, so keep it stable and append new flags where they read best.\n */\nexport const CLI_FLAGS = {\n budget: { type: 'string' },\n focus: { type: 'string', multiple: true },\n language: { type: 'string' },\n strategy: { type: 'string' },\n ignore: { type: 'string', multiple: true },\n cache: { type: 'string' },\n /**\n * Repeatable because `setup` wires several harnesses in one run. `hooks` takes one\n * per run and refuses a second id in its own parse — arity is a per-verb fact,\n * expressed where the verb validates, with the one generated refusal; the table\n * records how argv is read, not how many a verb accepts.\n */\n harness: { type: 'string', multiple: true },\n yes: { type: 'boolean' },\n 'no-mcp': { type: 'boolean' },\n strict: { type: 'boolean' },\n json: { type: 'boolean' },\n reconstruct: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean' },\n} as const;\n\n/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */\nexport type FlagName = keyof typeof CLI_FLAGS;\n\n/**\n * The flags answered *before* any verb, so no verb owns them and no verb may refuse\n * them: `smelt map --help` prints the help, exactly as it always has. Every other flag\n * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.\n */\nexport const GLOBAL_FLAGS = ['help', 'version'] as const satisfies readonly FlagName[];\n\n/** A flag a verb can own — everything but the two global ones. */\nexport type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;\n\n/** Every ownable flag, in table order: the list a refusal checks a verb against. */\nexport const VERB_FLAGS: readonly VerbFlag[] = Object.keys(CLI_FLAGS).filter(\n (name): name is VerbFlag => !(GLOBAL_FLAGS as readonly string[]).includes(name),\n);\n\n/**\n * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.\n *\n * Every refusal that names more than one flag spells the list this way: the\n * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`\n * refusal in `./smelt.ts`. It lives with the table because the *order* is the\n * table's — a refusal that listed flags in the order the user happened to type them\n * would read differently every time, and two refusals disagreeing about how to spell\n * the same pair is the kind of drift this file exists to remove.\n */\nexport function flagList(flags: readonly VerbFlag[]): string {\n const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);\n if (spelled.length <= 1) return spelled.join('');\n return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;\n}\n\n/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */\ntype FlagValue<F> = F extends { readonly type: 'boolean' }\n ? boolean\n : F extends { readonly multiple: true }\n ? readonly string[]\n : string;\n\n/**\n * The parsed flags, as every `Subcommand.parse` sees them: one optional field per\n * flag, typed by the table above. Absent means the user did not type it — which is\n * the only thing a refusal needs to know, and the only thing a verb may act on.\n */\nexport type FlagValues = { readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]> };\n\n/** How one flag appears in the OPTIONS block of `--help`. */\nexport interface FlagHelp {\n /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */\n readonly label: string;\n /**\n * The description, already wrapped to the OPTIONS column — a function because three\n * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +\n * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The\n * ownership prefix is not here; it is generated.\n */\n body(): readonly string[];\n}\n\n/**\n * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the\n * hand-typed version wrapped. `--strategy` and `--language` render their registries on\n * one line because they fit; the harness ids do not, and a list long enough to wrap is\n * exactly the list nobody keeps in sync by hand.\n */\nexport function optionList(items: readonly string[], width: number): readonly string[] {\n const lines: string[] = [];\n let line = '';\n items.forEach((item, index) => {\n const word = index === items.length - 1 ? `${item}.` : `${item},`;\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = candidate;\n }\n });\n lines.push(line);\n return lines;\n}\n\n/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */\nconst OPTION_BODY_WIDTH = 65;\n\n/**\n * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag\n * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text\n * cannot fall behind what the parser accepts.\n */\nexport const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>> = {\n budget: {\n label: '--budget <bytes>',\n body: () => [\n 'Required, unless smelt.config.json sets defaultBudgetBytes.',\n 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',\n 'ceiling, met by construction). No built-in default: a budget',\n `${CLI_NAME} invented would decide for you.`,\n ],\n },\n focus: {\n label: '--focus <term>',\n body: () => [\n 'What you were looking for. Repeatable. Matching regions and',\n 'their context survive; the runs between them collapse. For',\n 'map: symbols matching a term (by name or path) are promoted',\n 'to the front of the fill order, ranks unchanged.',\n ],\n },\n language: {\n label: '--language <id>',\n body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],\n },\n strategy: {\n label: '--strategy <id>',\n body: () => [\n `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,\n 'smelt.config.json says otherwise. structural parses',\n `${STRUCTURAL_LANGUAGES.join(', ')};`,\n 'any other language is refused, never approximated. auto picks',\n 'structural for those languages and lexical for the rest, and',\n 'the report names whichever one actually ran.',\n ],\n },\n ignore: {\n label: '--ignore <entry>',\n body: () => [\n 'Repeatable. Replaces the default ignore list',\n // Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default\n // by hand is help text that will one day describe a different default.\n `(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,\n 'an entry containing / is a root-relative prefix.',\n ],\n },\n cache: {\n label: '--cache <dir>',\n body: () => [\n 'Directory for the tags cache, keyed by content',\n 'hash. Only when given does the map write to disk at all.',\n ],\n },\n harness: {\n label: '--harness <id>',\n body: () => [\n 'Skip harness detection and target one id:',\n ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),\n 'Repeatable for setup; hooks takes one per run.',\n ],\n },\n yes: {\n label: '--yes',\n body: () => [\n \"Non-interactive setup: the recipe's defaults, printed\",\n 'loudly as they are applied. Existing files are never',\n 'overwritten — skipped with a note; hooks install edits',\n 'them, and it asks per file.',\n ],\n },\n 'no-mcp': {\n label: '--no-mcp',\n body: () => [\n 'Setup only: skip the MCP registration step — the',\n 'printed command and its note — for a hooks-only',\n 'setup.',\n ],\n },\n strict: {\n label: '--strict',\n body: () => [\n 'Turn any lint finding into exit 1, for CI. Findings are',\n 'advisory by default: the rules are heuristics about somebody',\n \"else's house style, and enforcing them uninvited would be\",\n `${CLI_NAME} deciding it. A budget you set yourself is different —`,\n 'exceeding agents.budgetBytes exits 1 with or without --strict.',\n ],\n },\n json: {\n label: '--json',\n body: () => [\n 'Print a JSON envelope on stdout instead of the text:',\n '{ format, result, elided } for a smelt run — `result` is',\n 'the SmeltResult verbatim, `elided` carries the bytes, so',\n 'the envelope can be reconstructed; feed it back with',\n '--reconstruct. For map: { format, map }, the RepoMap',\n 'structure verbatim. For agents lint: { format, report },',\n 'the measured levels and every finding with its rule id.',\n ],\n },\n reconstruct: {\n label: '--reconstruct',\n body: () => [\n 'Read a --json envelope and print the original text, byte for',\n 'byte. This is Law 3 you can run from a shell.',\n ],\n },\n help: { label: '-h, --help', body: () => ['This text.'] },\n version: { label: '--version', body: () => ['The package version.'] },\n};\n\n/**\n * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget\n * smelt invented would be smelt deciding how much of the caller's context to throw\n * away, silently, at a number nobody chose. A *missing* flag is not an error here,\n * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose\n * explicitly, and the verb's `resolve` errors only when neither exists. A malformed\n * value is always an error.\n *\n * It lives with the flag rather than with a verb because two verbs own `--budget`, and\n * the two of them agreeing on what \"4kb\" means is not something to leave to chance.\n *\n * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a\n * leading `-` are answered by a digits-only test before anything numeric happens. The\n * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also\n * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`\n * here and `\"budgetBytes\"` there.\n */\nexport function parseBudget(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw)) throw refuseBudget('not-an-integer', raw);\n const value = Number(raw);\n const fault = budgetFault(value);\n if (fault !== undefined) throw refuseBudget(fault, raw);\n return value;\n}\n\n/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */\nfunction refuseBudget(fault: BudgetFault, raw: string): CliUsageError {\n return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);\n}\n"]}
1
+ {"version":3,"file":"flags.js","sourceRoot":"","sources":["../../../src/cli/subcommands/flags.ts"],"names":[],"mappings":"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","sourcesContent":["import { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError } from '../../errors.ts';\nimport { HARNESS_IDS } from '../../harness/registry.ts';\nimport { budgetFault, budgetMalformed } from '../../ops/inputs.ts';\nimport type { BudgetFault } from '../../ops/inputs.ts';\nimport { STRATEGIES, DEFAULT_STRATEGY } from '../../plan/planners.ts';\nimport { STRUCTURAL_LANGUAGES } from '../../plan/structural.ts';\nimport { DEFAULT_REPO_IGNORE } from '../../repomap/map.ts';\nimport { CLI_NAME } from '../shell.ts';\n\n/**\n * Every flag the CLI accepts, once.\n *\n * The table is the companion the {@link Subcommand} registry needs: a verb declares\n * the flags it owns as `FlagName[]`, so the type of that list — and therefore whether\n * a verb can claim a flag that does not exist — comes from here. It carries three\n * things that used to be written in three places:\n *\n * 1. **How `node:util.parseArgs` reads the flag.** The option table used to be a\n * literal inside `parseSmeltArgs`.\n * 2. **The name.** `FlagName` is `keyof typeof CLI_FLAGS`, so an eleventh flag is\n * spelled once and every verb's ownership list typechecks against it.\n * 3. **Its OPTIONS entry in `--help`.** Including the `map only.` / `hooks only.`\n * prefix, which is *not* stored here: it is generated from the registry's\n * ownership (see `renderOptions` in `cli/usage.ts`), because \"which verb owns\n * this flag\" is a fact the registry already holds and prose that restates it is\n * prose that can go stale.\n *\n * Key order is meaningful: it is the order the OPTIONS block renders and the order a\n * refusal lists flags in, so keep it stable and append new flags where they read best.\n */\nexport const CLI_FLAGS = {\n budget: { type: 'string' },\n focus: { type: 'string', multiple: true },\n producer: { type: 'string' },\n language: { type: 'string' },\n strategy: { type: 'string' },\n ignore: { type: 'string', multiple: true },\n cache: { type: 'string' },\n /**\n * Repeatable because `setup` wires several harnesses in one run. `hooks` takes one\n * per run and refuses a second id in its own parse — arity is a per-verb fact,\n * expressed where the verb validates, with the one generated refusal; the table\n * records how argv is read, not how many a verb accepts.\n */\n harness: { type: 'string', multiple: true },\n yes: { type: 'boolean' },\n 'no-mcp': { type: 'boolean' },\n strict: { type: 'boolean' },\n json: { type: 'boolean' },\n reconstruct: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean' },\n} as const;\n\n/** Every flag name, as a type. A verb cannot claim a flag that is not in the table. */\nexport type FlagName = keyof typeof CLI_FLAGS;\n\n/**\n * The flags answered *before* any verb, so no verb owns them and no verb may refuse\n * them: `smelt map --help` prints the help, exactly as it always has. Every other flag\n * belongs to at least one verb — `test/guards/subcommand-registry.test.ts` pins that.\n */\nexport const GLOBAL_FLAGS = ['help', 'version'] as const satisfies readonly FlagName[];\n\n/** A flag a verb can own — everything but the two global ones. */\nexport type VerbFlag = Exclude<FlagName, (typeof GLOBAL_FLAGS)[number]>;\n\n/** Every ownable flag, in table order: the list a refusal checks a verb against. */\nexport const VERB_FLAGS: readonly VerbFlag[] = Object.keys(CLI_FLAGS).filter(\n (name): name is VerbFlag => !(GLOBAL_FLAGS as readonly string[]).includes(name),\n);\n\n/**\n * `--a`, `--a and --b`, `--a, --b and --c` — in flag-table order, always.\n *\n * Every refusal that names more than one flag spells the list this way: the\n * ownership refusal in `./registry.ts`, and the default verb's `--reconstruct`\n * refusal in `./smelt.ts`. It lives with the table because the *order* is the\n * table's — a refusal that listed flags in the order the user happened to type them\n * would read differently every time, and two refusals disagreeing about how to spell\n * the same pair is the kind of drift this file exists to remove.\n */\nexport function flagList(flags: readonly VerbFlag[]): string {\n const spelled = VERB_FLAGS.filter((flag) => flags.includes(flag)).map((flag) => `--${flag}`);\n if (spelled.length <= 1) return spelled.join('');\n return `${spelled.slice(0, -1).join(', ')} and ${spelled.at(-1) ?? ''}`;\n}\n\n/** What one flag's parsed value looks like, derived from how `parseArgs` was told to read it. */\ntype FlagValue<F> = F extends { readonly type: 'boolean' }\n ? boolean\n : F extends { readonly multiple: true }\n ? readonly string[]\n : string;\n\n/**\n * The parsed flags, as every `Subcommand.parse` sees them: one optional field per\n * flag, typed by the table above. Absent means the user did not type it — which is\n * the only thing a refusal needs to know, and the only thing a verb may act on.\n */\nexport type FlagValues = { readonly [K in FlagName]?: FlagValue<(typeof CLI_FLAGS)[K]> };\n\n/** How one flag appears in the OPTIONS block of `--help`. */\nexport interface FlagHelp {\n /** The left column, e.g. `--budget <bytes>` or `-h, --help`. */\n readonly label: string;\n /**\n * The description, already wrapped to the OPTIONS column — a function because three\n * entries render a registry (`SUPPORTED_LANGUAGES`, `STRATEGIES` +\n * `STRUCTURAL_LANGUAGES`, `HARNESS_IDS`) rather than a hand-typed list. The\n * ownership prefix is not here; it is generated.\n */\n body(): readonly string[];\n}\n\n/**\n * A comma-separated list under an OPTIONS entry's hanging indent, wrapped where the\n * hand-typed version wrapped. `--strategy` and `--language` render their registries on\n * one line because they fit; the harness ids do not, and a list long enough to wrap is\n * exactly the list nobody keeps in sync by hand.\n */\nexport function optionList(items: readonly string[], width: number): readonly string[] {\n const lines: string[] = [];\n let line = '';\n items.forEach((item, index) => {\n const word = index === items.length - 1 ? `${item}.` : `${item},`;\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > width) {\n lines.push(line);\n line = word;\n } else {\n line = candidate;\n }\n });\n lines.push(line);\n return lines;\n}\n\n/** The width an OPTIONS description wraps within, once its 23-column indent is removed. */\nconst OPTION_BODY_WIDTH = 65;\n\n/**\n * The OPTIONS entry for every flag. `Record<FlagName, FlagHelp>` on purpose: a flag\n * added to {@link CLI_FLAGS} without a help entry is a compile error, so the help text\n * cannot fall behind what the parser accepts.\n */\nexport const FLAG_HELP: Readonly<Record<FlagName, FlagHelp>> = {\n budget: {\n label: '--budget <bytes>',\n body: () => [\n 'Required, unless smelt.config.json sets defaultBudgetBytes.',\n 'Soft ceiling for the output, in UTF-8 bytes (for map: a hard',\n 'ceiling, met by construction). No built-in default: a budget',\n `${CLI_NAME} invented would decide for you.`,\n ],\n },\n focus: {\n label: '--focus <term>',\n body: () => [\n 'What you were looking for. Repeatable. Matching regions and',\n 'their context survive; the runs between them collapse. For',\n 'map: symbols matching a term (by name or path) are promoted',\n 'to the front of the fill order, ranks unchanged.',\n ],\n },\n producer: {\n label: '--producer <cmd>',\n body: () => [\n 'The command whose output this is, e.g. \"grep -C 3 foo src\". When',\n 'no --focus is given, the focus is derived from it exactly as the',\n 'hooks guard derives it: a search pattern, only when the output',\n 'also holds non-matching lines (context flags). cat, diffs and',\n 'logs name no term; the head and tail are kept instead.',\n ],\n },\n language: {\n label: '--language <id>',\n body: () => [`Override detection. One of: ${[...SUPPORTED_LANGUAGES, 'unknown'].join(', ')}.`],\n },\n strategy: {\n label: '--strategy <id>',\n body: () => [\n `${STRATEGIES.join(', ')}. Defaults to ${DEFAULT_STRATEGY}, unless`,\n 'smelt.config.json says otherwise. structural parses',\n `${STRUCTURAL_LANGUAGES.join(', ')};`,\n 'any other language is refused, never approximated. json cuts',\n 'members and elements; diff cuts files and hunks; each refuses',\n 'any other content. auto picks by content kind first (json,',\n 'diff), then structural for those languages and lexical for the',\n 'rest, and the report names whichever one actually ran.',\n ],\n },\n ignore: {\n label: '--ignore <entry>',\n body: () => [\n 'Repeatable. Replaces the default ignore list',\n // Read off DEFAULT_REPO_IGNORE, never re-typed: help text that lists a default\n // by hand is help text that will one day describe a different default.\n `(${DEFAULT_REPO_IGNORE.join(', ')}): a bare name matches any path segment,`,\n 'an entry containing / is a root-relative prefix.',\n ],\n },\n cache: {\n label: '--cache <dir>',\n body: () => [\n 'Directory for the tags cache, keyed by content',\n 'hash. Only when given does the map write to disk at all.',\n ],\n },\n harness: {\n label: '--harness <id>',\n body: () => [\n 'Skip harness detection and target one id:',\n ...optionList(HARNESS_IDS, OPTION_BODY_WIDTH),\n 'Repeatable for setup; hooks takes one per run.',\n ],\n },\n yes: {\n label: '--yes',\n body: () => [\n \"Non-interactive setup: the recipe's defaults, printed\",\n 'loudly as they are applied. Existing files are never',\n 'overwritten — skipped with a note; hooks install edits',\n 'them, and it asks per file.',\n ],\n },\n 'no-mcp': {\n label: '--no-mcp',\n body: () => [\n 'Setup only: skip the MCP registration step — the',\n 'printed command and its note — for a hooks-only',\n 'setup.',\n ],\n },\n strict: {\n label: '--strict',\n body: () => [\n 'Turn any lint finding into exit 1, for CI. Findings are',\n 'advisory by default: the rules are heuristics about somebody',\n \"else's house style, and enforcing them uninvited would be\",\n `${CLI_NAME} deciding it. A budget you set yourself is different —`,\n 'exceeding agents.budgetBytes exits 1 with or without --strict.',\n ],\n },\n json: {\n label: '--json',\n body: () => [\n 'Print a JSON envelope on stdout instead of the text:',\n '{ format, result, elided } for a smelt run — `result` is',\n 'the SmeltResult verbatim, `elided` carries the bytes, so',\n 'the envelope can be reconstructed; feed it back with',\n '--reconstruct. For map: { format, map }, the RepoMap',\n 'structure verbatim. For agents lint: { format, report },',\n 'the measured levels and every finding with its rule id.',\n ],\n },\n reconstruct: {\n label: '--reconstruct',\n body: () => [\n 'Read a --json envelope and print the original text, byte for',\n 'byte. This is Law 3 you can run from a shell.',\n ],\n },\n help: { label: '-h, --help', body: () => ['This text.'] },\n version: { label: '--version', body: () => ['The package version.'] },\n};\n\n/**\n * `--budget` has no built-in default, for the same reason `smelt()` has none: a budget\n * smelt invented would be smelt deciding how much of the caller's context to throw\n * away, silently, at a number nobody chose. A *missing* flag is not an error here,\n * though — `smelt.config.json` may carry a `defaultBudgetBytes` the user chose\n * explicitly, and the verb's `resolve` errors only when neither exists. A malformed\n * value is always an error.\n *\n * It lives with the flag rather than with a verb because two verbs own `--budget`, and\n * the two of them agreeing on what \"4kb\" means is not something to leave to chance.\n *\n * The *lexing* is the CLI's own and stays here — argv carries strings, so `4kb` and a\n * leading `-` are answered by a digits-only test before anything numeric happens. The\n * *rule* and *the sentence that refuses it* come from `ops/inputs.ts`, which is also\n * where the `smelt_file` tool gets them: two surfaces, one law, spelled `--budget`\n * here and `\"budgetBytes\"` there.\n */\nexport function parseBudget(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw)) throw refuseBudget('not-an-integer', raw);\n const value = Number(raw);\n const fault = budgetFault(value);\n if (fault !== undefined) throw refuseBudget(fault, raw);\n return value;\n}\n\n/** The malformed-budget refusal, in the CLI's currency: prefixed, and exit 2. */\nfunction refuseBudget(fault: BudgetFault, raw: string): CliUsageError {\n return new CliUsageError(`${CLI_NAME}: ${budgetMalformed(fault, '--budget', raw)}`);\n}\n"]}
@@ -22,6 +22,8 @@ export interface SmeltInvocation {
22
22
  */
23
23
  readonly budgetBytes?: number;
24
24
  readonly focus: readonly string[];
25
+ /** The command whose output the blob is, for focus derivation. See `--producer`. */
26
+ readonly producer?: string;
25
27
  readonly language?: DetectedLanguage;
26
28
  /** `undefined` means the flag was not given — the config default may apply. */
27
29
  readonly strategy?: Strategy;
@@ -51,6 +53,7 @@ export interface ResolvedRun {
51
53
  /** Path to read. `undefined` means stdin. Flags only; the config has no say. */
52
54
  readonly file?: string;
53
55
  readonly focus: readonly string[];
56
+ readonly producer?: string;
54
57
  readonly language?: DetectedLanguage;
55
58
  readonly json: boolean;
56
59
  }
@@ -1 +1 @@
1
- {"version":3,"file":"smelt.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/smelt.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,KAAK,EAAE,gBAAgB,EAAgB,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElF,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAOlE,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;GAQG;AAEH,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACrE,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACvD;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D;;;;GAIG;AACH,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,oBAAoB,CA8D1E,CAAC;AAqDF;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,YAAY,GAAG,SAAS,GAC/B,WAAW,CA6Bb","sourcesContent":["import { reconstruct } from '../../apply.ts';\nimport { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError, SmeltError } from '../../errors.ts';\nimport { budgetRequired, openStore, readBlob, resolveStrategy } from '../../ops/inputs.ts';\nimport { smeltBlob } from '../../ops/verbs.ts';\nimport { isStrategy, STRATEGIES } from '../../plan/planners.ts';\nimport type { Strategy } from '../../plan/planners.ts';\nimport { MemoryElisionStore } from '../../store.ts';\nimport type { DetectedLanguage, ElisionStore, SmeltResult } from '../../types.ts';\nimport { CONFIG_FILE_NAME, configuredStore } from '../config.ts';\nimport type { ConfiguredStore, LoadedConfig } from '../config.ts';\nimport { formatReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport { flagList, parseBudget, VERB_FLAGS } from './flags.ts';\nimport type { FlagValues, VerbFlag } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * The default verb: `smelt <file>` and `smelt < input`, plus the `--reconstruct` round\n * trip that reads one of its own `--json` envelopes back.\n *\n * Two jobs, one verb, which is why USAGE lists it twice: `--reconstruct` reads a file\n * this command wrote and puts every byte back, so it takes no budget, no focus and no\n * strategy — but it is the same front door, and giving it a subcommand word would\n * break every script that types `smelt --reconstruct`.\n */\n\n/** What the CLI was asked to do. Pure data, so the parse is testable on its own. */\nexport interface SmeltInvocation {\n readonly mode: 'smelt' | 'reconstruct' | 'help' | 'version' | 'init';\n /** Path to read. `undefined` means stdin. */\n readonly file?: string;\n /**\n * UTF-8 bytes. `undefined` in `'smelt'` mode means the flag was not given; the\n * runner then consults `smelt.config.json` and errors if that has no default either.\n */\n readonly budgetBytes?: number;\n readonly focus: readonly string[];\n readonly language?: DetectedLanguage;\n /** `undefined` means the flag was not given — the config default may apply. */\n readonly strategy?: Strategy;\n readonly json: boolean;\n}\n\n/**\n * Everything one smelt run needs, fully merged — with a receipt for where each\n * merged value came from.\n *\n * This is the verb's single merge of flags + config + built-ins. Precedence lives\n * here and nowhere else: `resolveRun` is the only code that may look at a flag and a\n * config default side by side, so a precedence question is always answered by one\n * function instead of by reading two files. `runSmelt` executes this object\n * straight-line, without a `??` of its own.\n */\nexport interface ResolvedRun {\n readonly budgetBytes: number;\n /** Where the budget came from. A missing budget never gets here — it throws. */\n readonly budgetSource: 'flag' | 'config';\n readonly strategy: Strategy;\n readonly strategySource: 'flag' | 'config' | 'builtin';\n /**\n * The store decision, with `path` already resolved against the config file's\n * directory. `'memory'` is the built-in default — a fresh in-memory store per run.\n */\n readonly store: ConfiguredStore;\n /** Path to read. `undefined` means stdin. Flags only; the config has no say. */\n readonly file?: string;\n readonly focus: readonly string[];\n readonly language?: DetectedLanguage;\n readonly json: boolean;\n}\n\n/**\n * The default verb's two resolved shapes.\n *\n * `--reconstruct` merges nothing: there is no budget to default, no strategy to pick\n * and no store to choose, because the envelope carries its own bytes. Saying that in\n * the type is cheaper than a {@link ResolvedRun} whose every field would be a lie for\n * half of this verb's runs.\n */\nexport type ResolvedSmeltCommand =\n | { readonly kind: 'smelt'; readonly run: ResolvedRun }\n | { readonly kind: 'reconstruct'; readonly file?: string };\n\n/**\n * The `--json` envelope format, versioned like the marker for the same reason: this is\n * a surface other programs parse, so a change to it has to be identifiable rather than\n * silent.\n */\nexport const CLI_JSON_FORMAT = 'smelt-cli/v1';\n\n/**\n * What `--json` prints, and what `--reconstruct` reads back.\n *\n * `result` is the {@link SmeltResult} **verbatim** — nothing renamed, nothing dropped —\n * so it can be diffed in a test. `elided` is the other half, and it is here because a\n * result on its own is *not* reconstructible: Law 3 is a property of the result plus\n * the store that holds its bytes, and a file claiming to prove the round trip while\n * carrying only half of it would prove nothing.\n */\nexport interface CliJsonEnvelope {\n readonly format: string;\n readonly result: SmeltResult;\n /** hash → the exact elided bytes. Keys match `result.elisions[].hash`. */\n readonly elided: Readonly<Record<string, string>>;\n}\n\nexport const smeltCommand: Subcommand<SmeltInvocation, ResolvedSmeltCommand> = {\n name: 'smelt',\n flags: ['budget', 'focus', 'language', 'strategy', 'json', 'reconstruct'],\n refusal:\n `A single-blob run reads one file or stdin; there is no tree to walk, ` +\n `nothing to cache, and no harness to install into.`,\n usage: {\n synopsis: [\n '<file> --budget <bytes> [--focus <term>]...',\n '--budget <bytes> [--focus <term>]... < input',\n ],\n occasional: ['--reconstruct <result.json>', '--reconstruct < result.json'],\n },\n\n parse(values: FlagValues, positionals: readonly string[]): SmeltInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: expected at most one file, got ${String(positionals.length)} ` +\n `(${positionals.join(', ')}). smelt reads one blob at a time.`,\n );\n }\n const file = positionals[0];\n\n if (values.reconstruct === true) {\n refuseReconstructFlags(values);\n return {\n mode: 'reconstruct',\n ...(file === undefined ? {} : { file }),\n focus: [],\n json: false,\n };\n }\n\n const budgetBytes = parseBudget(values.budget);\n const chosenStrategy = parseStrategy(values.strategy);\n return {\n mode: 'smelt',\n ...(file === undefined ? {} : { file }),\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n focus: values.focus ?? [],\n ...(values.language === undefined ? {} : { language: parseLanguage(values.language) }),\n ...(chosenStrategy === undefined ? {} : { strategy: chosenStrategy }),\n json: values.json === true,\n };\n },\n\n resolve(invocation: SmeltInvocation, config: ConfigSource): ResolvedSmeltCommand {\n if (invocation.mode === 'reconstruct') {\n return {\n kind: 'reconstruct',\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n };\n }\n return { kind: 'smelt', run: resolveRun(invocation, config()) };\n },\n\n run(resolved: ResolvedSmeltCommand, io: CliIo): number | Promise<number> {\n if (resolved.kind === 'reconstruct') {\n return runReconstruct(readInput(resolved.file, io), io);\n }\n return runSmelt(resolved.run, io);\n },\n};\n\n/**\n * Every flag this verb owns that `--reconstruct` cannot honour, with the reason each\n * one makes no sense — the second job's flag ownership, which the registry cannot\n * express because both jobs are the same verb.\n *\n * `refuseForeignFlags` refuses a flag no verb owns *here*; it cannot refuse `--focus`\n * on a single-blob run, because a single-blob run is exactly where `--focus` belongs.\n * So the ones the round trip ignores are named here instead, and named exhaustively:\n * every flag on `smeltCommand.flags` except `--reconstruct` itself has an entry, which\n * `test/guards/subcommand-registry.test.ts` crosses. A flag added to this verb and\n * forgotten here would be silently ignored by half of the verb's runs — the failure\n * this table exists to make impossible.\n *\n * Key order is the flag table's order, so a refusal lists flags the way every other\n * refusal does.\n */\nconst RECONSTRUCT_REFUSALS = {\n budget: `Reconstruction puts every byte back; there is nothing to fit.`,\n focus:\n `Focus decides what survives a cut, and the cut has already been made — ` +\n `the envelope names every elision it took.`,\n language:\n `Nothing is detected or parsed on the way back: the envelope carries the ` +\n `bytes and the ranges the cut recorded.`,\n strategy: `No planner runs on the way back — the elisions come from the envelope.`,\n json:\n `--reconstruct reads a --json envelope and prints the original text; ` +\n `there is no second envelope to write.`,\n} as const satisfies Partial<Record<VerbFlag, string>>;\n\n/**\n * Refuse every flag `--reconstruct` would otherwise ignore, in one message shaped like\n * the ownership refusals: the flags named, then why not here. A {@link CliUsageError},\n * so it exits 2 exactly as every other refusal does.\n *\n * @throws {CliUsageError} naming each offending flag and what the round trip does instead.\n */\nfunction refuseReconstructFlags(values: FlagValues): void {\n const reasons: Partial<Record<VerbFlag, string>> = RECONSTRUCT_REFUSALS;\n const offending = VERB_FLAGS.filter(\n (flag) => reasons[flag] !== undefined && values[flag] !== undefined,\n );\n if (offending.length === 0) return;\n\n const verb = offending.length === 1 ? 'makes' : 'make';\n const why = offending.map((flag) => reasons[flag] ?? '').join(' ');\n throw new CliUsageError(\n `${CLI_NAME}: ${flagList(offending)} ${verb} no sense with --reconstruct. ${why}`,\n );\n}\n\n/**\n * Merge one `'smelt'`-mode invocation with the loaded config (or `undefined` when no\n * `smelt.config.json` exists) and the built-in defaults.\n *\n * The precedence is strict and one-directional: an explicit flag always wins over the\n * config, and the config only fills what the flags left unsaid. Built-ins fill last,\n * and only where a built-in exists at all — the budget deliberately has none, so a run\n * with no budget from either source is refused here, in the one function that owns that\n * error.\n *\n * @throws {CliUsageError} when neither `--budget` nor the config names a budget.\n */\nexport function resolveRun(\n invocation: SmeltInvocation,\n config: LoadedConfig | undefined,\n): ResolvedRun {\n const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;\n if (budgetBytes === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: ` +\n budgetRequired({\n knob: '--budget',\n stake: 'your context to throw away',\n advice:\n `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +\n `(\\`${CLI_NAME} init\\` writes one).\\n` +\n ` ${CLI_NAME} src/server.ts --budget 4000 --focus handleRequest`,\n }),\n );\n }\n\n const strategy = resolveStrategy(invocation.strategy, config?.config.strategy);\n\n return {\n budgetBytes,\n budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',\n strategy: strategy.strategy,\n strategySource: strategy.source,\n store: configuredStore(config),\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n focus: invocation.focus,\n ...(invocation.language === undefined ? {} : { language: invocation.language }),\n json: invocation.json,\n };\n}\n\n/**\n * One smelt run: read the input, hand the resolved values to {@link smeltBlob}, render.\n *\n * All merging — flags versus `smelt.config.json` versus built-ins, including the\n * budget-required refusal — happens in {@link resolveRun}, which is the only place\n * precedence lives. This function reads the resolved object and never consults a flag\n * or a config field directly. The cut itself belongs to no front door: `smeltBlob` in\n * `ops/verbs.ts` builds the smelter and returns the values this function prints, and\n * the `smelt_file` tool calls the same op with its own arguments.\n */\nasync function runSmelt(run: ResolvedRun, io: CliIo): Promise<number> {\n const inputText = readInput(run.file, io);\n\n const outcome = await smeltBlob({\n text: inputText,\n source: run.file ?? '<stdin>',\n budgetBytes: run.budgetBytes,\n strategy: run.strategy,\n store: openStore(run.store),\n ...(run.file === undefined ? {} : { path: run.file }),\n ...(run.language === undefined ? {} : { language: run.language }),\n focus: run.focus,\n });\n\n if (run.json) {\n io.stdout(`${JSON.stringify(envelope(outcome.result, outcome.store), null, 2)}\\n`);\n } else {\n io.stdout(outcome.result.text);\n }\n io.stderr(formatReport(outcome));\n\n return outcome.result.outputBytes > run.budgetBytes ? EXIT.overBudget : EXIT.ok;\n}\n\n/**\n * Law 3, from a shell.\n *\n * This is deliberately not \"print the text and hope\": it rebuilds the store from the\n * envelope, checks every hash against the bytes it claims to key, and checks the\n * reconstructed length against the `inputBytes` the result recorded at the time of the\n * cut. A round trip that quietly returns almost-right text is the failure this whole\n * repository is arranged against.\n */\nfunction runReconstruct(text: string, io: CliIo): number {\n const { result, elided } = parseEnvelope(text);\n const store = new MemoryElisionStore();\n\n for (const [hash, content] of Object.entries(elided)) {\n const actual = store.put(content);\n if (actual !== hash) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is not self-consistent — it stores bytes under ` +\n `\"${hash}\" that actually hash to \"${actual}\". Refusing to reconstruct from it.`,\n );\n }\n }\n for (const elision of result.elisions) {\n if (!store.has(elision.hash)) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is missing the bytes for \"${elision.hash}\". A result ` +\n `without its elided bytes cannot be reconstructed; re-run with --json.`,\n );\n }\n }\n\n const original = reconstruct(result, store);\n const bytes = Buffer.byteLength(original, 'utf8');\n if (bytes !== result.inputBytes) {\n throw new SmeltError(\n `${CLI_NAME}: reconstruction produced ${String(bytes)} bytes but the result ` +\n `recorded ${String(result.inputBytes)}. The round trip did not close.`,\n );\n }\n\n io.stdout(original);\n io.stderr(\n `${CLI_NAME} reconstructed ${String(result.inputBytes)} B from ` +\n `${String(result.elisions.length)} elisions — byte for byte\\n`,\n );\n return EXIT.ok;\n}\n\n/** `peek`, not `retrieve`: writing a file is not the model asking for anything back. */\nfunction envelope(result: SmeltResult, store: ElisionStore): CliJsonEnvelope {\n const elided: Record<string, string> = {};\n for (const elision of result.elisions) {\n const content = store.peek(elision.hash);\n if (content === undefined) {\n throw new SmeltError(\n `${CLI_NAME}: the store does not hold \"${elision.hash}\", which its own result ` +\n `says it elided. Refusing to write an envelope that cannot round-trip.`,\n );\n }\n elided[elision.hash] = content;\n }\n return { format: CLI_JSON_FORMAT, result, elided };\n}\n\nfunction parseEnvelope(text: string): CliJsonEnvelope {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (cause) {\n throw new CliUsageError(\n `${CLI_NAME}: --reconstruct expected a --json envelope, and this is not JSON: ` +\n `${cause instanceof Error ? cause.message : String(cause)}`,\n );\n }\n\n if (typeof value !== 'object' || value === null) {\n throw new CliUsageError(`${CLI_NAME}: --reconstruct expected a JSON object.`);\n }\n const fields = value as Record<string, unknown>;\n\n if (fields['format'] !== CLI_JSON_FORMAT) {\n throw new CliUsageError(\n `${CLI_NAME}: this envelope says format \"${String(fields['format'])}\"; this build ` +\n `reads \"${CLI_JSON_FORMAT}\". Formats are versioned so a mismatch is visible ` +\n `instead of being half-understood.`,\n );\n }\n\n const result = fields['result'];\n if (typeof result !== 'object' || result === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`result\\` object.`);\n }\n const resultFields = result as Record<string, unknown>;\n if (\n typeof resultFields['text'] !== 'string' ||\n typeof resultFields['inputBytes'] !== 'number' ||\n !Array.isArray(resultFields['elisions'])\n ) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope's \\`result\\` is missing text, inputBytes or elisions.`,\n );\n }\n\n const elided = fields['elided'];\n if (typeof elided !== 'object' || elided === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`elided\\` map.`);\n }\n for (const [hash, content] of Object.entries(elided)) {\n if (typeof content !== 'string') {\n throw new CliUsageError(`${CLI_NAME}: envelope's \\`elided[\"${hash}\"]\\` is not a string.`);\n }\n }\n\n return {\n format: CLI_JSON_FORMAT,\n result: result as unknown as SmeltResult,\n elided: elided as Record<string, string>,\n };\n}\n\n/**\n * The blob to work on: stdin when no file was named, otherwise the file — read\n * through the ops law, so \"cannot read X\" says the same thing here and in the\n * `smelt_file` tool. Only the stdin leg is the CLI's own; a tool has no stdin.\n */\nfunction readInput(file: string | undefined, io: CliIo): string {\n if (file === undefined) return io.stdin();\n const read = readBlob(file, file);\n if (!read.ok) throw new CliUsageError(`${CLI_NAME}: ${read.refusal}`);\n return read.value;\n}\n\nfunction parseLanguage(raw: string): DetectedLanguage {\n const known: readonly string[] = [...SUPPORTED_LANGUAGES, 'unknown'];\n if (!known.includes(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --language \"${raw}\". Known: ${known.join(', ')}.`,\n );\n }\n return raw as DetectedLanguage;\n}\n\n/** Membership in the {@link PLANNERS} registry is the whole validation. */\nfunction parseStrategy(raw: string | undefined): Strategy | undefined {\n if (raw === undefined) return undefined;\n if (!isStrategy(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --strategy \"${raw}\". Known: ${STRATEGIES.join(', ')}.`,\n );\n }\n return raw;\n}\n"]}
1
+ {"version":3,"file":"smelt.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/smelt.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,KAAK,EAAE,gBAAgB,EAAgB,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElF,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAOlE,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;GAQG;AAEH,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACrE,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACvD;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D;;;;GAIG;AACH,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,oBAAoB,CA+D1E,CAAC;AAsDF;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,YAAY,GAAG,SAAS,GAC/B,WAAW,CA8Bb","sourcesContent":["import { reconstruct } from '../../apply.ts';\nimport { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError, SmeltError } from '../../errors.ts';\nimport { budgetRequired, openStore, readBlob, resolveStrategy } from '../../ops/inputs.ts';\nimport { smeltBlob } from '../../ops/verbs.ts';\nimport { isStrategy, STRATEGIES } from '../../plan/planners.ts';\nimport type { Strategy } from '../../plan/planners.ts';\nimport { MemoryElisionStore } from '../../store.ts';\nimport type { DetectedLanguage, ElisionStore, SmeltResult } from '../../types.ts';\nimport { CONFIG_FILE_NAME, configuredStore } from '../config.ts';\nimport type { ConfiguredStore, LoadedConfig } from '../config.ts';\nimport { formatReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport { flagList, parseBudget, VERB_FLAGS } from './flags.ts';\nimport type { FlagValues, VerbFlag } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * The default verb: `smelt <file>` and `smelt < input`, plus the `--reconstruct` round\n * trip that reads one of its own `--json` envelopes back.\n *\n * Two jobs, one verb, which is why USAGE lists it twice: `--reconstruct` reads a file\n * this command wrote and puts every byte back, so it takes no budget, no focus and no\n * strategy — but it is the same front door, and giving it a subcommand word would\n * break every script that types `smelt --reconstruct`.\n */\n\n/** What the CLI was asked to do. Pure data, so the parse is testable on its own. */\nexport interface SmeltInvocation {\n readonly mode: 'smelt' | 'reconstruct' | 'help' | 'version' | 'init';\n /** Path to read. `undefined` means stdin. */\n readonly file?: string;\n /**\n * UTF-8 bytes. `undefined` in `'smelt'` mode means the flag was not given; the\n * runner then consults `smelt.config.json` and errors if that has no default either.\n */\n readonly budgetBytes?: number;\n readonly focus: readonly string[];\n /** The command whose output the blob is, for focus derivation. See `--producer`. */\n readonly producer?: string;\n readonly language?: DetectedLanguage;\n /** `undefined` means the flag was not given — the config default may apply. */\n readonly strategy?: Strategy;\n readonly json: boolean;\n}\n\n/**\n * Everything one smelt run needs, fully merged — with a receipt for where each\n * merged value came from.\n *\n * This is the verb's single merge of flags + config + built-ins. Precedence lives\n * here and nowhere else: `resolveRun` is the only code that may look at a flag and a\n * config default side by side, so a precedence question is always answered by one\n * function instead of by reading two files. `runSmelt` executes this object\n * straight-line, without a `??` of its own.\n */\nexport interface ResolvedRun {\n readonly budgetBytes: number;\n /** Where the budget came from. A missing budget never gets here — it throws. */\n readonly budgetSource: 'flag' | 'config';\n readonly strategy: Strategy;\n readonly strategySource: 'flag' | 'config' | 'builtin';\n /**\n * The store decision, with `path` already resolved against the config file's\n * directory. `'memory'` is the built-in default — a fresh in-memory store per run.\n */\n readonly store: ConfiguredStore;\n /** Path to read. `undefined` means stdin. Flags only; the config has no say. */\n readonly file?: string;\n readonly focus: readonly string[];\n readonly producer?: string;\n readonly language?: DetectedLanguage;\n readonly json: boolean;\n}\n\n/**\n * The default verb's two resolved shapes.\n *\n * `--reconstruct` merges nothing: there is no budget to default, no strategy to pick\n * and no store to choose, because the envelope carries its own bytes. Saying that in\n * the type is cheaper than a {@link ResolvedRun} whose every field would be a lie for\n * half of this verb's runs.\n */\nexport type ResolvedSmeltCommand =\n | { readonly kind: 'smelt'; readonly run: ResolvedRun }\n | { readonly kind: 'reconstruct'; readonly file?: string };\n\n/**\n * The `--json` envelope format, versioned like the marker for the same reason: this is\n * a surface other programs parse, so a change to it has to be identifiable rather than\n * silent.\n */\nexport const CLI_JSON_FORMAT = 'smelt-cli/v1';\n\n/**\n * What `--json` prints, and what `--reconstruct` reads back.\n *\n * `result` is the {@link SmeltResult} **verbatim** — nothing renamed, nothing dropped —\n * so it can be diffed in a test. `elided` is the other half, and it is here because a\n * result on its own is *not* reconstructible: Law 3 is a property of the result plus\n * the store that holds its bytes, and a file claiming to prove the round trip while\n * carrying only half of it would prove nothing.\n */\nexport interface CliJsonEnvelope {\n readonly format: string;\n readonly result: SmeltResult;\n /** hash → the exact elided bytes. Keys match `result.elisions[].hash`. */\n readonly elided: Readonly<Record<string, string>>;\n}\n\nexport const smeltCommand: Subcommand<SmeltInvocation, ResolvedSmeltCommand> = {\n name: 'smelt',\n flags: ['budget', 'focus', 'producer', 'language', 'strategy', 'json', 'reconstruct'],\n refusal:\n `A single-blob run reads one file or stdin; there is no tree to walk, ` +\n `nothing to cache, and no harness to install into.`,\n usage: {\n synopsis: [\n '<file> --budget <bytes> [--focus <term>]...',\n '--budget <bytes> [--focus <term>]... < input',\n ],\n occasional: ['--reconstruct <result.json>', '--reconstruct < result.json'],\n },\n\n parse(values: FlagValues, positionals: readonly string[]): SmeltInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: expected at most one file, got ${String(positionals.length)} ` +\n `(${positionals.join(', ')}). smelt reads one blob at a time.`,\n );\n }\n const file = positionals[0];\n\n if (values.reconstruct === true) {\n refuseReconstructFlags(values);\n return {\n mode: 'reconstruct',\n ...(file === undefined ? {} : { file }),\n focus: [],\n json: false,\n };\n }\n\n const budgetBytes = parseBudget(values.budget);\n const chosenStrategy = parseStrategy(values.strategy);\n return {\n mode: 'smelt',\n ...(file === undefined ? {} : { file }),\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n focus: values.focus ?? [],\n ...(values.producer === undefined ? {} : { producer: values.producer }),\n ...(values.language === undefined ? {} : { language: parseLanguage(values.language) }),\n ...(chosenStrategy === undefined ? {} : { strategy: chosenStrategy }),\n json: values.json === true,\n };\n },\n\n resolve(invocation: SmeltInvocation, config: ConfigSource): ResolvedSmeltCommand {\n if (invocation.mode === 'reconstruct') {\n return {\n kind: 'reconstruct',\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n };\n }\n return { kind: 'smelt', run: resolveRun(invocation, config()) };\n },\n\n run(resolved: ResolvedSmeltCommand, io: CliIo): number | Promise<number> {\n if (resolved.kind === 'reconstruct') {\n return runReconstruct(readInput(resolved.file, io), io);\n }\n return runSmelt(resolved.run, io);\n },\n};\n\n/**\n * Every flag this verb owns that `--reconstruct` cannot honour, with the reason each\n * one makes no sense — the second job's flag ownership, which the registry cannot\n * express because both jobs are the same verb.\n *\n * `refuseForeignFlags` refuses a flag no verb owns *here*; it cannot refuse `--focus`\n * on a single-blob run, because a single-blob run is exactly where `--focus` belongs.\n * So the ones the round trip ignores are named here instead, and named exhaustively:\n * every flag on `smeltCommand.flags` except `--reconstruct` itself has an entry, which\n * `test/guards/subcommand-registry.test.ts` crosses. A flag added to this verb and\n * forgotten here would be silently ignored by half of the verb's runs — the failure\n * this table exists to make impossible.\n *\n * Key order is the flag table's order, so a refusal lists flags the way every other\n * refusal does.\n */\nconst RECONSTRUCT_REFUSALS = {\n budget: `Reconstruction puts every byte back; there is nothing to fit.`,\n focus:\n `Focus decides what survives a cut, and the cut has already been made — ` +\n `the envelope names every elision it took.`,\n producer: `A producer only derives a focus, and there is no cut left to focus.`,\n language:\n `Nothing is detected or parsed on the way back: the envelope carries the ` +\n `bytes and the ranges the cut recorded.`,\n strategy: `No planner runs on the way back — the elisions come from the envelope.`,\n json:\n `--reconstruct reads a --json envelope and prints the original text; ` +\n `there is no second envelope to write.`,\n} as const satisfies Partial<Record<VerbFlag, string>>;\n\n/**\n * Refuse every flag `--reconstruct` would otherwise ignore, in one message shaped like\n * the ownership refusals: the flags named, then why not here. A {@link CliUsageError},\n * so it exits 2 exactly as every other refusal does.\n *\n * @throws {CliUsageError} naming each offending flag and what the round trip does instead.\n */\nfunction refuseReconstructFlags(values: FlagValues): void {\n const reasons: Partial<Record<VerbFlag, string>> = RECONSTRUCT_REFUSALS;\n const offending = VERB_FLAGS.filter(\n (flag) => reasons[flag] !== undefined && values[flag] !== undefined,\n );\n if (offending.length === 0) return;\n\n const verb = offending.length === 1 ? 'makes' : 'make';\n const why = offending.map((flag) => reasons[flag] ?? '').join(' ');\n throw new CliUsageError(\n `${CLI_NAME}: ${flagList(offending)} ${verb} no sense with --reconstruct. ${why}`,\n );\n}\n\n/**\n * Merge one `'smelt'`-mode invocation with the loaded config (or `undefined` when no\n * `smelt.config.json` exists) and the built-in defaults.\n *\n * The precedence is strict and one-directional: an explicit flag always wins over the\n * config, and the config only fills what the flags left unsaid. Built-ins fill last,\n * and only where a built-in exists at all — the budget deliberately has none, so a run\n * with no budget from either source is refused here, in the one function that owns that\n * error.\n *\n * @throws {CliUsageError} when neither `--budget` nor the config names a budget.\n */\nexport function resolveRun(\n invocation: SmeltInvocation,\n config: LoadedConfig | undefined,\n): ResolvedRun {\n const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;\n if (budgetBytes === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: ` +\n budgetRequired({\n knob: '--budget',\n stake: 'your context to throw away',\n advice:\n `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +\n `(\\`${CLI_NAME} init\\` writes one).\\n` +\n ` ${CLI_NAME} src/server.ts --budget 4000 --focus handleRequest`,\n }),\n );\n }\n\n const strategy = resolveStrategy(invocation.strategy, config?.config.strategy);\n\n return {\n budgetBytes,\n budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',\n strategy: strategy.strategy,\n strategySource: strategy.source,\n store: configuredStore(config),\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n focus: invocation.focus,\n ...(invocation.producer === undefined ? {} : { producer: invocation.producer }),\n ...(invocation.language === undefined ? {} : { language: invocation.language }),\n json: invocation.json,\n };\n}\n\n/**\n * One smelt run: read the input, hand the resolved values to {@link smeltBlob}, render.\n *\n * All merging — flags versus `smelt.config.json` versus built-ins, including the\n * budget-required refusal — happens in {@link resolveRun}, which is the only place\n * precedence lives. This function reads the resolved object and never consults a flag\n * or a config field directly. The cut itself belongs to no front door: `smeltBlob` in\n * `ops/verbs.ts` builds the smelter and returns the values this function prints, and\n * the `smelt_file` tool calls the same op with its own arguments.\n */\nasync function runSmelt(run: ResolvedRun, io: CliIo): Promise<number> {\n const inputText = readInput(run.file, io);\n\n const outcome = await smeltBlob({\n text: inputText,\n source: run.file ?? '<stdin>',\n budgetBytes: run.budgetBytes,\n strategy: run.strategy,\n store: openStore(run.store),\n ...(run.file === undefined ? {} : { path: run.file }),\n ...(run.language === undefined ? {} : { language: run.language }),\n focus: run.focus,\n ...(run.producer === undefined ? {} : { producer: run.producer }),\n });\n\n if (run.json) {\n io.stdout(`${JSON.stringify(envelope(outcome.result, outcome.store), null, 2)}\\n`);\n } else {\n io.stdout(outcome.result.text);\n }\n io.stderr(formatReport(outcome));\n\n return outcome.result.outputBytes > run.budgetBytes ? EXIT.overBudget : EXIT.ok;\n}\n\n/**\n * Law 3, from a shell.\n *\n * This is deliberately not \"print the text and hope\": it rebuilds the store from the\n * envelope, checks every hash against the bytes it claims to key, and checks the\n * reconstructed length against the `inputBytes` the result recorded at the time of the\n * cut. A round trip that quietly returns almost-right text is the failure this whole\n * repository is arranged against.\n */\nfunction runReconstruct(text: string, io: CliIo): number {\n const { result, elided } = parseEnvelope(text);\n const store = new MemoryElisionStore();\n\n for (const [hash, content] of Object.entries(elided)) {\n const actual = store.put(content);\n if (actual !== hash) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is not self-consistent — it stores bytes under ` +\n `\"${hash}\" that actually hash to \"${actual}\". Refusing to reconstruct from it.`,\n );\n }\n }\n for (const elision of result.elisions) {\n if (!store.has(elision.hash)) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is missing the bytes for \"${elision.hash}\". A result ` +\n `without its elided bytes cannot be reconstructed; re-run with --json.`,\n );\n }\n }\n\n const original = reconstruct(result, store);\n const bytes = Buffer.byteLength(original, 'utf8');\n if (bytes !== result.inputBytes) {\n throw new SmeltError(\n `${CLI_NAME}: reconstruction produced ${String(bytes)} bytes but the result ` +\n `recorded ${String(result.inputBytes)}. The round trip did not close.`,\n );\n }\n\n io.stdout(original);\n io.stderr(\n `${CLI_NAME} reconstructed ${String(result.inputBytes)} B from ` +\n `${String(result.elisions.length)} elisions — byte for byte\\n`,\n );\n return EXIT.ok;\n}\n\n/** `peek`, not `retrieve`: writing a file is not the model asking for anything back. */\nfunction envelope(result: SmeltResult, store: ElisionStore): CliJsonEnvelope {\n const elided: Record<string, string> = {};\n for (const elision of result.elisions) {\n const content = store.peek(elision.hash);\n if (content === undefined) {\n throw new SmeltError(\n `${CLI_NAME}: the store does not hold \"${elision.hash}\", which its own result ` +\n `says it elided. Refusing to write an envelope that cannot round-trip.`,\n );\n }\n elided[elision.hash] = content;\n }\n return { format: CLI_JSON_FORMAT, result, elided };\n}\n\nfunction parseEnvelope(text: string): CliJsonEnvelope {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (cause) {\n throw new CliUsageError(\n `${CLI_NAME}: --reconstruct expected a --json envelope, and this is not JSON: ` +\n `${cause instanceof Error ? cause.message : String(cause)}`,\n );\n }\n\n if (typeof value !== 'object' || value === null) {\n throw new CliUsageError(`${CLI_NAME}: --reconstruct expected a JSON object.`);\n }\n const fields = value as Record<string, unknown>;\n\n if (fields['format'] !== CLI_JSON_FORMAT) {\n throw new CliUsageError(\n `${CLI_NAME}: this envelope says format \"${String(fields['format'])}\"; this build ` +\n `reads \"${CLI_JSON_FORMAT}\". Formats are versioned so a mismatch is visible ` +\n `instead of being half-understood.`,\n );\n }\n\n const result = fields['result'];\n if (typeof result !== 'object' || result === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`result\\` object.`);\n }\n const resultFields = result as Record<string, unknown>;\n if (\n typeof resultFields['text'] !== 'string' ||\n typeof resultFields['inputBytes'] !== 'number' ||\n !Array.isArray(resultFields['elisions'])\n ) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope's \\`result\\` is missing text, inputBytes or elisions.`,\n );\n }\n\n const elided = fields['elided'];\n if (typeof elided !== 'object' || elided === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`elided\\` map.`);\n }\n for (const [hash, content] of Object.entries(elided)) {\n if (typeof content !== 'string') {\n throw new CliUsageError(`${CLI_NAME}: envelope's \\`elided[\"${hash}\"]\\` is not a string.`);\n }\n }\n\n return {\n format: CLI_JSON_FORMAT,\n result: result as unknown as SmeltResult,\n elided: elided as Record<string, string>,\n };\n}\n\n/**\n * The blob to work on: stdin when no file was named, otherwise the file — read\n * through the ops law, so \"cannot read X\" says the same thing here and in the\n * `smelt_file` tool. Only the stdin leg is the CLI's own; a tool has no stdin.\n */\nfunction readInput(file: string | undefined, io: CliIo): string {\n if (file === undefined) return io.stdin();\n const read = readBlob(file, file);\n if (!read.ok) throw new CliUsageError(`${CLI_NAME}: ${read.refusal}`);\n return read.value;\n}\n\nfunction parseLanguage(raw: string): DetectedLanguage {\n const known: readonly string[] = [...SUPPORTED_LANGUAGES, 'unknown'];\n if (!known.includes(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --language \"${raw}\". Known: ${known.join(', ')}.`,\n );\n }\n return raw as DetectedLanguage;\n}\n\n/** Membership in the {@link PLANNERS} registry is the whole validation. */\nfunction parseStrategy(raw: string | undefined): Strategy | undefined {\n if (raw === undefined) return undefined;\n if (!isStrategy(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --strategy \"${raw}\". Known: ${STRATEGIES.join(', ')}.`,\n );\n }\n return raw;\n}\n"]}
@@ -17,7 +17,7 @@ import { flagList, parseBudget, VERB_FLAGS } from './flags.js';
17
17
  export const CLI_JSON_FORMAT = 'smelt-cli/v1';
18
18
  export const smeltCommand = {
19
19
  name: 'smelt',
20
- flags: ['budget', 'focus', 'language', 'strategy', 'json', 'reconstruct'],
20
+ flags: ['budget', 'focus', 'producer', 'language', 'strategy', 'json', 'reconstruct'],
21
21
  refusal: `A single-blob run reads one file or stdin; there is no tree to walk, ` +
22
22
  `nothing to cache, and no harness to install into.`,
23
23
  usage: {
@@ -49,6 +49,7 @@ export const smeltCommand = {
49
49
  ...(file === undefined ? {} : { file }),
50
50
  ...(budgetBytes === undefined ? {} : { budgetBytes }),
51
51
  focus: values.focus ?? [],
52
+ ...(values.producer === undefined ? {} : { producer: values.producer }),
52
53
  ...(values.language === undefined ? {} : { language: parseLanguage(values.language) }),
53
54
  ...(chosenStrategy === undefined ? {} : { strategy: chosenStrategy }),
54
55
  json: values.json === true,
@@ -90,6 +91,7 @@ const RECONSTRUCT_REFUSALS = {
90
91
  budget: `Reconstruction puts every byte back; there is nothing to fit.`,
91
92
  focus: `Focus decides what survives a cut, and the cut has already been made — ` +
92
93
  `the envelope names every elision it took.`,
94
+ producer: `A producer only derives a focus, and there is no cut left to focus.`,
93
95
  language: `Nothing is detected or parsed on the way back: the envelope carries the ` +
94
96
  `bytes and the ranges the cut recorded.`,
95
97
  strategy: `No planner runs on the way back — the elisions come from the envelope.`,
@@ -145,6 +147,7 @@ export function resolveRun(invocation, config) {
145
147
  store: configuredStore(config),
146
148
  ...(invocation.file === undefined ? {} : { file: invocation.file }),
147
149
  focus: invocation.focus,
150
+ ...(invocation.producer === undefined ? {} : { producer: invocation.producer }),
148
151
  ...(invocation.language === undefined ? {} : { language: invocation.language }),
149
152
  json: invocation.json,
150
153
  };
@@ -170,6 +173,7 @@ async function runSmelt(run, io) {
170
173
  ...(run.file === undefined ? {} : { path: run.file }),
171
174
  ...(run.language === undefined ? {} : { language: run.language }),
172
175
  focus: run.focus,
176
+ ...(run.producer === undefined ? {} : { producer: run.producer }),
173
177
  });
174
178
  if (run.json) {
175
179
  io.stdout(`${JSON.stringify(envelope(outcome.result, outcome.store), null, 2)}\n`);
@@ -1 +1 @@
1
- {"version":3,"file":"smelt.js","sourceRoot":"","sources":["../../../src/cli/subcommands/smelt.ts"],"names":[],"mappings":"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","sourcesContent":["import { reconstruct } from '../../apply.ts';\nimport { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError, SmeltError } from '../../errors.ts';\nimport { budgetRequired, openStore, readBlob, resolveStrategy } from '../../ops/inputs.ts';\nimport { smeltBlob } from '../../ops/verbs.ts';\nimport { isStrategy, STRATEGIES } from '../../plan/planners.ts';\nimport type { Strategy } from '../../plan/planners.ts';\nimport { MemoryElisionStore } from '../../store.ts';\nimport type { DetectedLanguage, ElisionStore, SmeltResult } from '../../types.ts';\nimport { CONFIG_FILE_NAME, configuredStore } from '../config.ts';\nimport type { ConfiguredStore, LoadedConfig } from '../config.ts';\nimport { formatReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport { flagList, parseBudget, VERB_FLAGS } from './flags.ts';\nimport type { FlagValues, VerbFlag } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * The default verb: `smelt <file>` and `smelt < input`, plus the `--reconstruct` round\n * trip that reads one of its own `--json` envelopes back.\n *\n * Two jobs, one verb, which is why USAGE lists it twice: `--reconstruct` reads a file\n * this command wrote and puts every byte back, so it takes no budget, no focus and no\n * strategy — but it is the same front door, and giving it a subcommand word would\n * break every script that types `smelt --reconstruct`.\n */\n\n/** What the CLI was asked to do. Pure data, so the parse is testable on its own. */\nexport interface SmeltInvocation {\n readonly mode: 'smelt' | 'reconstruct' | 'help' | 'version' | 'init';\n /** Path to read. `undefined` means stdin. */\n readonly file?: string;\n /**\n * UTF-8 bytes. `undefined` in `'smelt'` mode means the flag was not given; the\n * runner then consults `smelt.config.json` and errors if that has no default either.\n */\n readonly budgetBytes?: number;\n readonly focus: readonly string[];\n readonly language?: DetectedLanguage;\n /** `undefined` means the flag was not given — the config default may apply. */\n readonly strategy?: Strategy;\n readonly json: boolean;\n}\n\n/**\n * Everything one smelt run needs, fully merged — with a receipt for where each\n * merged value came from.\n *\n * This is the verb's single merge of flags + config + built-ins. Precedence lives\n * here and nowhere else: `resolveRun` is the only code that may look at a flag and a\n * config default side by side, so a precedence question is always answered by one\n * function instead of by reading two files. `runSmelt` executes this object\n * straight-line, without a `??` of its own.\n */\nexport interface ResolvedRun {\n readonly budgetBytes: number;\n /** Where the budget came from. A missing budget never gets here — it throws. */\n readonly budgetSource: 'flag' | 'config';\n readonly strategy: Strategy;\n readonly strategySource: 'flag' | 'config' | 'builtin';\n /**\n * The store decision, with `path` already resolved against the config file's\n * directory. `'memory'` is the built-in default — a fresh in-memory store per run.\n */\n readonly store: ConfiguredStore;\n /** Path to read. `undefined` means stdin. Flags only; the config has no say. */\n readonly file?: string;\n readonly focus: readonly string[];\n readonly language?: DetectedLanguage;\n readonly json: boolean;\n}\n\n/**\n * The default verb's two resolved shapes.\n *\n * `--reconstruct` merges nothing: there is no budget to default, no strategy to pick\n * and no store to choose, because the envelope carries its own bytes. Saying that in\n * the type is cheaper than a {@link ResolvedRun} whose every field would be a lie for\n * half of this verb's runs.\n */\nexport type ResolvedSmeltCommand =\n | { readonly kind: 'smelt'; readonly run: ResolvedRun }\n | { readonly kind: 'reconstruct'; readonly file?: string };\n\n/**\n * The `--json` envelope format, versioned like the marker for the same reason: this is\n * a surface other programs parse, so a change to it has to be identifiable rather than\n * silent.\n */\nexport const CLI_JSON_FORMAT = 'smelt-cli/v1';\n\n/**\n * What `--json` prints, and what `--reconstruct` reads back.\n *\n * `result` is the {@link SmeltResult} **verbatim** — nothing renamed, nothing dropped —\n * so it can be diffed in a test. `elided` is the other half, and it is here because a\n * result on its own is *not* reconstructible: Law 3 is a property of the result plus\n * the store that holds its bytes, and a file claiming to prove the round trip while\n * carrying only half of it would prove nothing.\n */\nexport interface CliJsonEnvelope {\n readonly format: string;\n readonly result: SmeltResult;\n /** hash → the exact elided bytes. Keys match `result.elisions[].hash`. */\n readonly elided: Readonly<Record<string, string>>;\n}\n\nexport const smeltCommand: Subcommand<SmeltInvocation, ResolvedSmeltCommand> = {\n name: 'smelt',\n flags: ['budget', 'focus', 'language', 'strategy', 'json', 'reconstruct'],\n refusal:\n `A single-blob run reads one file or stdin; there is no tree to walk, ` +\n `nothing to cache, and no harness to install into.`,\n usage: {\n synopsis: [\n '<file> --budget <bytes> [--focus <term>]...',\n '--budget <bytes> [--focus <term>]... < input',\n ],\n occasional: ['--reconstruct <result.json>', '--reconstruct < result.json'],\n },\n\n parse(values: FlagValues, positionals: readonly string[]): SmeltInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: expected at most one file, got ${String(positionals.length)} ` +\n `(${positionals.join(', ')}). smelt reads one blob at a time.`,\n );\n }\n const file = positionals[0];\n\n if (values.reconstruct === true) {\n refuseReconstructFlags(values);\n return {\n mode: 'reconstruct',\n ...(file === undefined ? {} : { file }),\n focus: [],\n json: false,\n };\n }\n\n const budgetBytes = parseBudget(values.budget);\n const chosenStrategy = parseStrategy(values.strategy);\n return {\n mode: 'smelt',\n ...(file === undefined ? {} : { file }),\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n focus: values.focus ?? [],\n ...(values.language === undefined ? {} : { language: parseLanguage(values.language) }),\n ...(chosenStrategy === undefined ? {} : { strategy: chosenStrategy }),\n json: values.json === true,\n };\n },\n\n resolve(invocation: SmeltInvocation, config: ConfigSource): ResolvedSmeltCommand {\n if (invocation.mode === 'reconstruct') {\n return {\n kind: 'reconstruct',\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n };\n }\n return { kind: 'smelt', run: resolveRun(invocation, config()) };\n },\n\n run(resolved: ResolvedSmeltCommand, io: CliIo): number | Promise<number> {\n if (resolved.kind === 'reconstruct') {\n return runReconstruct(readInput(resolved.file, io), io);\n }\n return runSmelt(resolved.run, io);\n },\n};\n\n/**\n * Every flag this verb owns that `--reconstruct` cannot honour, with the reason each\n * one makes no sense — the second job's flag ownership, which the registry cannot\n * express because both jobs are the same verb.\n *\n * `refuseForeignFlags` refuses a flag no verb owns *here*; it cannot refuse `--focus`\n * on a single-blob run, because a single-blob run is exactly where `--focus` belongs.\n * So the ones the round trip ignores are named here instead, and named exhaustively:\n * every flag on `smeltCommand.flags` except `--reconstruct` itself has an entry, which\n * `test/guards/subcommand-registry.test.ts` crosses. A flag added to this verb and\n * forgotten here would be silently ignored by half of the verb's runs — the failure\n * this table exists to make impossible.\n *\n * Key order is the flag table's order, so a refusal lists flags the way every other\n * refusal does.\n */\nconst RECONSTRUCT_REFUSALS = {\n budget: `Reconstruction puts every byte back; there is nothing to fit.`,\n focus:\n `Focus decides what survives a cut, and the cut has already been made — ` +\n `the envelope names every elision it took.`,\n language:\n `Nothing is detected or parsed on the way back: the envelope carries the ` +\n `bytes and the ranges the cut recorded.`,\n strategy: `No planner runs on the way back — the elisions come from the envelope.`,\n json:\n `--reconstruct reads a --json envelope and prints the original text; ` +\n `there is no second envelope to write.`,\n} as const satisfies Partial<Record<VerbFlag, string>>;\n\n/**\n * Refuse every flag `--reconstruct` would otherwise ignore, in one message shaped like\n * the ownership refusals: the flags named, then why not here. A {@link CliUsageError},\n * so it exits 2 exactly as every other refusal does.\n *\n * @throws {CliUsageError} naming each offending flag and what the round trip does instead.\n */\nfunction refuseReconstructFlags(values: FlagValues): void {\n const reasons: Partial<Record<VerbFlag, string>> = RECONSTRUCT_REFUSALS;\n const offending = VERB_FLAGS.filter(\n (flag) => reasons[flag] !== undefined && values[flag] !== undefined,\n );\n if (offending.length === 0) return;\n\n const verb = offending.length === 1 ? 'makes' : 'make';\n const why = offending.map((flag) => reasons[flag] ?? '').join(' ');\n throw new CliUsageError(\n `${CLI_NAME}: ${flagList(offending)} ${verb} no sense with --reconstruct. ${why}`,\n );\n}\n\n/**\n * Merge one `'smelt'`-mode invocation with the loaded config (or `undefined` when no\n * `smelt.config.json` exists) and the built-in defaults.\n *\n * The precedence is strict and one-directional: an explicit flag always wins over the\n * config, and the config only fills what the flags left unsaid. Built-ins fill last,\n * and only where a built-in exists at all — the budget deliberately has none, so a run\n * with no budget from either source is refused here, in the one function that owns that\n * error.\n *\n * @throws {CliUsageError} when neither `--budget` nor the config names a budget.\n */\nexport function resolveRun(\n invocation: SmeltInvocation,\n config: LoadedConfig | undefined,\n): ResolvedRun {\n const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;\n if (budgetBytes === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: ` +\n budgetRequired({\n knob: '--budget',\n stake: 'your context to throw away',\n advice:\n `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +\n `(\\`${CLI_NAME} init\\` writes one).\\n` +\n ` ${CLI_NAME} src/server.ts --budget 4000 --focus handleRequest`,\n }),\n );\n }\n\n const strategy = resolveStrategy(invocation.strategy, config?.config.strategy);\n\n return {\n budgetBytes,\n budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',\n strategy: strategy.strategy,\n strategySource: strategy.source,\n store: configuredStore(config),\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n focus: invocation.focus,\n ...(invocation.language === undefined ? {} : { language: invocation.language }),\n json: invocation.json,\n };\n}\n\n/**\n * One smelt run: read the input, hand the resolved values to {@link smeltBlob}, render.\n *\n * All merging — flags versus `smelt.config.json` versus built-ins, including the\n * budget-required refusal — happens in {@link resolveRun}, which is the only place\n * precedence lives. This function reads the resolved object and never consults a flag\n * or a config field directly. The cut itself belongs to no front door: `smeltBlob` in\n * `ops/verbs.ts` builds the smelter and returns the values this function prints, and\n * the `smelt_file` tool calls the same op with its own arguments.\n */\nasync function runSmelt(run: ResolvedRun, io: CliIo): Promise<number> {\n const inputText = readInput(run.file, io);\n\n const outcome = await smeltBlob({\n text: inputText,\n source: run.file ?? '<stdin>',\n budgetBytes: run.budgetBytes,\n strategy: run.strategy,\n store: openStore(run.store),\n ...(run.file === undefined ? {} : { path: run.file }),\n ...(run.language === undefined ? {} : { language: run.language }),\n focus: run.focus,\n });\n\n if (run.json) {\n io.stdout(`${JSON.stringify(envelope(outcome.result, outcome.store), null, 2)}\\n`);\n } else {\n io.stdout(outcome.result.text);\n }\n io.stderr(formatReport(outcome));\n\n return outcome.result.outputBytes > run.budgetBytes ? EXIT.overBudget : EXIT.ok;\n}\n\n/**\n * Law 3, from a shell.\n *\n * This is deliberately not \"print the text and hope\": it rebuilds the store from the\n * envelope, checks every hash against the bytes it claims to key, and checks the\n * reconstructed length against the `inputBytes` the result recorded at the time of the\n * cut. A round trip that quietly returns almost-right text is the failure this whole\n * repository is arranged against.\n */\nfunction runReconstruct(text: string, io: CliIo): number {\n const { result, elided } = parseEnvelope(text);\n const store = new MemoryElisionStore();\n\n for (const [hash, content] of Object.entries(elided)) {\n const actual = store.put(content);\n if (actual !== hash) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is not self-consistent — it stores bytes under ` +\n `\"${hash}\" that actually hash to \"${actual}\". Refusing to reconstruct from it.`,\n );\n }\n }\n for (const elision of result.elisions) {\n if (!store.has(elision.hash)) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is missing the bytes for \"${elision.hash}\". A result ` +\n `without its elided bytes cannot be reconstructed; re-run with --json.`,\n );\n }\n }\n\n const original = reconstruct(result, store);\n const bytes = Buffer.byteLength(original, 'utf8');\n if (bytes !== result.inputBytes) {\n throw new SmeltError(\n `${CLI_NAME}: reconstruction produced ${String(bytes)} bytes but the result ` +\n `recorded ${String(result.inputBytes)}. The round trip did not close.`,\n );\n }\n\n io.stdout(original);\n io.stderr(\n `${CLI_NAME} reconstructed ${String(result.inputBytes)} B from ` +\n `${String(result.elisions.length)} elisions — byte for byte\\n`,\n );\n return EXIT.ok;\n}\n\n/** `peek`, not `retrieve`: writing a file is not the model asking for anything back. */\nfunction envelope(result: SmeltResult, store: ElisionStore): CliJsonEnvelope {\n const elided: Record<string, string> = {};\n for (const elision of result.elisions) {\n const content = store.peek(elision.hash);\n if (content === undefined) {\n throw new SmeltError(\n `${CLI_NAME}: the store does not hold \"${elision.hash}\", which its own result ` +\n `says it elided. Refusing to write an envelope that cannot round-trip.`,\n );\n }\n elided[elision.hash] = content;\n }\n return { format: CLI_JSON_FORMAT, result, elided };\n}\n\nfunction parseEnvelope(text: string): CliJsonEnvelope {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (cause) {\n throw new CliUsageError(\n `${CLI_NAME}: --reconstruct expected a --json envelope, and this is not JSON: ` +\n `${cause instanceof Error ? cause.message : String(cause)}`,\n );\n }\n\n if (typeof value !== 'object' || value === null) {\n throw new CliUsageError(`${CLI_NAME}: --reconstruct expected a JSON object.`);\n }\n const fields = value as Record<string, unknown>;\n\n if (fields['format'] !== CLI_JSON_FORMAT) {\n throw new CliUsageError(\n `${CLI_NAME}: this envelope says format \"${String(fields['format'])}\"; this build ` +\n `reads \"${CLI_JSON_FORMAT}\". Formats are versioned so a mismatch is visible ` +\n `instead of being half-understood.`,\n );\n }\n\n const result = fields['result'];\n if (typeof result !== 'object' || result === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`result\\` object.`);\n }\n const resultFields = result as Record<string, unknown>;\n if (\n typeof resultFields['text'] !== 'string' ||\n typeof resultFields['inputBytes'] !== 'number' ||\n !Array.isArray(resultFields['elisions'])\n ) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope's \\`result\\` is missing text, inputBytes or elisions.`,\n );\n }\n\n const elided = fields['elided'];\n if (typeof elided !== 'object' || elided === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`elided\\` map.`);\n }\n for (const [hash, content] of Object.entries(elided)) {\n if (typeof content !== 'string') {\n throw new CliUsageError(`${CLI_NAME}: envelope's \\`elided[\"${hash}\"]\\` is not a string.`);\n }\n }\n\n return {\n format: CLI_JSON_FORMAT,\n result: result as unknown as SmeltResult,\n elided: elided as Record<string, string>,\n };\n}\n\n/**\n * The blob to work on: stdin when no file was named, otherwise the file — read\n * through the ops law, so \"cannot read X\" says the same thing here and in the\n * `smelt_file` tool. Only the stdin leg is the CLI's own; a tool has no stdin.\n */\nfunction readInput(file: string | undefined, io: CliIo): string {\n if (file === undefined) return io.stdin();\n const read = readBlob(file, file);\n if (!read.ok) throw new CliUsageError(`${CLI_NAME}: ${read.refusal}`);\n return read.value;\n}\n\nfunction parseLanguage(raw: string): DetectedLanguage {\n const known: readonly string[] = [...SUPPORTED_LANGUAGES, 'unknown'];\n if (!known.includes(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --language \"${raw}\". Known: ${known.join(', ')}.`,\n );\n }\n return raw as DetectedLanguage;\n}\n\n/** Membership in the {@link PLANNERS} registry is the whole validation. */\nfunction parseStrategy(raw: string | undefined): Strategy | undefined {\n if (raw === undefined) return undefined;\n if (!isStrategy(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --strategy \"${raw}\". Known: ${STRATEGIES.join(', ')}.`,\n );\n }\n return raw;\n}\n"]}
1
+ {"version":3,"file":"smelt.js","sourceRoot":"","sources":["../../../src/cli/subcommands/smelt.ts"],"names":[],"mappings":"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","sourcesContent":["import { reconstruct } from '../../apply.ts';\nimport { SUPPORTED_LANGUAGES } from '../../detect.ts';\nimport { CliUsageError, SmeltError } from '../../errors.ts';\nimport { budgetRequired, openStore, readBlob, resolveStrategy } from '../../ops/inputs.ts';\nimport { smeltBlob } from '../../ops/verbs.ts';\nimport { isStrategy, STRATEGIES } from '../../plan/planners.ts';\nimport type { Strategy } from '../../plan/planners.ts';\nimport { MemoryElisionStore } from '../../store.ts';\nimport type { DetectedLanguage, ElisionStore, SmeltResult } from '../../types.ts';\nimport { CONFIG_FILE_NAME, configuredStore } from '../config.ts';\nimport type { ConfiguredStore, LoadedConfig } from '../config.ts';\nimport { formatReport } from '../report.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport { flagList, parseBudget, VERB_FLAGS } from './flags.ts';\nimport type { FlagValues, VerbFlag } from './flags.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * The default verb: `smelt <file>` and `smelt < input`, plus the `--reconstruct` round\n * trip that reads one of its own `--json` envelopes back.\n *\n * Two jobs, one verb, which is why USAGE lists it twice: `--reconstruct` reads a file\n * this command wrote and puts every byte back, so it takes no budget, no focus and no\n * strategy — but it is the same front door, and giving it a subcommand word would\n * break every script that types `smelt --reconstruct`.\n */\n\n/** What the CLI was asked to do. Pure data, so the parse is testable on its own. */\nexport interface SmeltInvocation {\n readonly mode: 'smelt' | 'reconstruct' | 'help' | 'version' | 'init';\n /** Path to read. `undefined` means stdin. */\n readonly file?: string;\n /**\n * UTF-8 bytes. `undefined` in `'smelt'` mode means the flag was not given; the\n * runner then consults `smelt.config.json` and errors if that has no default either.\n */\n readonly budgetBytes?: number;\n readonly focus: readonly string[];\n /** The command whose output the blob is, for focus derivation. See `--producer`. */\n readonly producer?: string;\n readonly language?: DetectedLanguage;\n /** `undefined` means the flag was not given — the config default may apply. */\n readonly strategy?: Strategy;\n readonly json: boolean;\n}\n\n/**\n * Everything one smelt run needs, fully merged — with a receipt for where each\n * merged value came from.\n *\n * This is the verb's single merge of flags + config + built-ins. Precedence lives\n * here and nowhere else: `resolveRun` is the only code that may look at a flag and a\n * config default side by side, so a precedence question is always answered by one\n * function instead of by reading two files. `runSmelt` executes this object\n * straight-line, without a `??` of its own.\n */\nexport interface ResolvedRun {\n readonly budgetBytes: number;\n /** Where the budget came from. A missing budget never gets here — it throws. */\n readonly budgetSource: 'flag' | 'config';\n readonly strategy: Strategy;\n readonly strategySource: 'flag' | 'config' | 'builtin';\n /**\n * The store decision, with `path` already resolved against the config file's\n * directory. `'memory'` is the built-in default — a fresh in-memory store per run.\n */\n readonly store: ConfiguredStore;\n /** Path to read. `undefined` means stdin. Flags only; the config has no say. */\n readonly file?: string;\n readonly focus: readonly string[];\n readonly producer?: string;\n readonly language?: DetectedLanguage;\n readonly json: boolean;\n}\n\n/**\n * The default verb's two resolved shapes.\n *\n * `--reconstruct` merges nothing: there is no budget to default, no strategy to pick\n * and no store to choose, because the envelope carries its own bytes. Saying that in\n * the type is cheaper than a {@link ResolvedRun} whose every field would be a lie for\n * half of this verb's runs.\n */\nexport type ResolvedSmeltCommand =\n | { readonly kind: 'smelt'; readonly run: ResolvedRun }\n | { readonly kind: 'reconstruct'; readonly file?: string };\n\n/**\n * The `--json` envelope format, versioned like the marker for the same reason: this is\n * a surface other programs parse, so a change to it has to be identifiable rather than\n * silent.\n */\nexport const CLI_JSON_FORMAT = 'smelt-cli/v1';\n\n/**\n * What `--json` prints, and what `--reconstruct` reads back.\n *\n * `result` is the {@link SmeltResult} **verbatim** — nothing renamed, nothing dropped —\n * so it can be diffed in a test. `elided` is the other half, and it is here because a\n * result on its own is *not* reconstructible: Law 3 is a property of the result plus\n * the store that holds its bytes, and a file claiming to prove the round trip while\n * carrying only half of it would prove nothing.\n */\nexport interface CliJsonEnvelope {\n readonly format: string;\n readonly result: SmeltResult;\n /** hash → the exact elided bytes. Keys match `result.elisions[].hash`. */\n readonly elided: Readonly<Record<string, string>>;\n}\n\nexport const smeltCommand: Subcommand<SmeltInvocation, ResolvedSmeltCommand> = {\n name: 'smelt',\n flags: ['budget', 'focus', 'producer', 'language', 'strategy', 'json', 'reconstruct'],\n refusal:\n `A single-blob run reads one file or stdin; there is no tree to walk, ` +\n `nothing to cache, and no harness to install into.`,\n usage: {\n synopsis: [\n '<file> --budget <bytes> [--focus <term>]...',\n '--budget <bytes> [--focus <term>]... < input',\n ],\n occasional: ['--reconstruct <result.json>', '--reconstruct < result.json'],\n },\n\n parse(values: FlagValues, positionals: readonly string[]): SmeltInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: expected at most one file, got ${String(positionals.length)} ` +\n `(${positionals.join(', ')}). smelt reads one blob at a time.`,\n );\n }\n const file = positionals[0];\n\n if (values.reconstruct === true) {\n refuseReconstructFlags(values);\n return {\n mode: 'reconstruct',\n ...(file === undefined ? {} : { file }),\n focus: [],\n json: false,\n };\n }\n\n const budgetBytes = parseBudget(values.budget);\n const chosenStrategy = parseStrategy(values.strategy);\n return {\n mode: 'smelt',\n ...(file === undefined ? {} : { file }),\n ...(budgetBytes === undefined ? {} : { budgetBytes }),\n focus: values.focus ?? [],\n ...(values.producer === undefined ? {} : { producer: values.producer }),\n ...(values.language === undefined ? {} : { language: parseLanguage(values.language) }),\n ...(chosenStrategy === undefined ? {} : { strategy: chosenStrategy }),\n json: values.json === true,\n };\n },\n\n resolve(invocation: SmeltInvocation, config: ConfigSource): ResolvedSmeltCommand {\n if (invocation.mode === 'reconstruct') {\n return {\n kind: 'reconstruct',\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n };\n }\n return { kind: 'smelt', run: resolveRun(invocation, config()) };\n },\n\n run(resolved: ResolvedSmeltCommand, io: CliIo): number | Promise<number> {\n if (resolved.kind === 'reconstruct') {\n return runReconstruct(readInput(resolved.file, io), io);\n }\n return runSmelt(resolved.run, io);\n },\n};\n\n/**\n * Every flag this verb owns that `--reconstruct` cannot honour, with the reason each\n * one makes no sense — the second job's flag ownership, which the registry cannot\n * express because both jobs are the same verb.\n *\n * `refuseForeignFlags` refuses a flag no verb owns *here*; it cannot refuse `--focus`\n * on a single-blob run, because a single-blob run is exactly where `--focus` belongs.\n * So the ones the round trip ignores are named here instead, and named exhaustively:\n * every flag on `smeltCommand.flags` except `--reconstruct` itself has an entry, which\n * `test/guards/subcommand-registry.test.ts` crosses. A flag added to this verb and\n * forgotten here would be silently ignored by half of the verb's runs — the failure\n * this table exists to make impossible.\n *\n * Key order is the flag table's order, so a refusal lists flags the way every other\n * refusal does.\n */\nconst RECONSTRUCT_REFUSALS = {\n budget: `Reconstruction puts every byte back; there is nothing to fit.`,\n focus:\n `Focus decides what survives a cut, and the cut has already been made — ` +\n `the envelope names every elision it took.`,\n producer: `A producer only derives a focus, and there is no cut left to focus.`,\n language:\n `Nothing is detected or parsed on the way back: the envelope carries the ` +\n `bytes and the ranges the cut recorded.`,\n strategy: `No planner runs on the way back — the elisions come from the envelope.`,\n json:\n `--reconstruct reads a --json envelope and prints the original text; ` +\n `there is no second envelope to write.`,\n} as const satisfies Partial<Record<VerbFlag, string>>;\n\n/**\n * Refuse every flag `--reconstruct` would otherwise ignore, in one message shaped like\n * the ownership refusals: the flags named, then why not here. A {@link CliUsageError},\n * so it exits 2 exactly as every other refusal does.\n *\n * @throws {CliUsageError} naming each offending flag and what the round trip does instead.\n */\nfunction refuseReconstructFlags(values: FlagValues): void {\n const reasons: Partial<Record<VerbFlag, string>> = RECONSTRUCT_REFUSALS;\n const offending = VERB_FLAGS.filter(\n (flag) => reasons[flag] !== undefined && values[flag] !== undefined,\n );\n if (offending.length === 0) return;\n\n const verb = offending.length === 1 ? 'makes' : 'make';\n const why = offending.map((flag) => reasons[flag] ?? '').join(' ');\n throw new CliUsageError(\n `${CLI_NAME}: ${flagList(offending)} ${verb} no sense with --reconstruct. ${why}`,\n );\n}\n\n/**\n * Merge one `'smelt'`-mode invocation with the loaded config (or `undefined` when no\n * `smelt.config.json` exists) and the built-in defaults.\n *\n * The precedence is strict and one-directional: an explicit flag always wins over the\n * config, and the config only fills what the flags left unsaid. Built-ins fill last,\n * and only where a built-in exists at all — the budget deliberately has none, so a run\n * with no budget from either source is refused here, in the one function that owns that\n * error.\n *\n * @throws {CliUsageError} when neither `--budget` nor the config names a budget.\n */\nexport function resolveRun(\n invocation: SmeltInvocation,\n config: LoadedConfig | undefined,\n): ResolvedRun {\n const budgetBytes = invocation.budgetBytes ?? config?.config.defaultBudgetBytes;\n if (budgetBytes === undefined) {\n throw new CliUsageError(\n `${CLI_NAME}: ` +\n budgetRequired({\n knob: '--budget',\n stake: 'your context to throw away',\n advice:\n `Pass --budget, or set defaultBudgetBytes in ${CONFIG_FILE_NAME} ` +\n `(\\`${CLI_NAME} init\\` writes one).\\n` +\n ` ${CLI_NAME} src/server.ts --budget 4000 --focus handleRequest`,\n }),\n );\n }\n\n const strategy = resolveStrategy(invocation.strategy, config?.config.strategy);\n\n return {\n budgetBytes,\n budgetSource: invocation.budgetBytes !== undefined ? 'flag' : 'config',\n strategy: strategy.strategy,\n strategySource: strategy.source,\n store: configuredStore(config),\n ...(invocation.file === undefined ? {} : { file: invocation.file }),\n focus: invocation.focus,\n ...(invocation.producer === undefined ? {} : { producer: invocation.producer }),\n ...(invocation.language === undefined ? {} : { language: invocation.language }),\n json: invocation.json,\n };\n}\n\n/**\n * One smelt run: read the input, hand the resolved values to {@link smeltBlob}, render.\n *\n * All merging — flags versus `smelt.config.json` versus built-ins, including the\n * budget-required refusal — happens in {@link resolveRun}, which is the only place\n * precedence lives. This function reads the resolved object and never consults a flag\n * or a config field directly. The cut itself belongs to no front door: `smeltBlob` in\n * `ops/verbs.ts` builds the smelter and returns the values this function prints, and\n * the `smelt_file` tool calls the same op with its own arguments.\n */\nasync function runSmelt(run: ResolvedRun, io: CliIo): Promise<number> {\n const inputText = readInput(run.file, io);\n\n const outcome = await smeltBlob({\n text: inputText,\n source: run.file ?? '<stdin>',\n budgetBytes: run.budgetBytes,\n strategy: run.strategy,\n store: openStore(run.store),\n ...(run.file === undefined ? {} : { path: run.file }),\n ...(run.language === undefined ? {} : { language: run.language }),\n focus: run.focus,\n ...(run.producer === undefined ? {} : { producer: run.producer }),\n });\n\n if (run.json) {\n io.stdout(`${JSON.stringify(envelope(outcome.result, outcome.store), null, 2)}\\n`);\n } else {\n io.stdout(outcome.result.text);\n }\n io.stderr(formatReport(outcome));\n\n return outcome.result.outputBytes > run.budgetBytes ? EXIT.overBudget : EXIT.ok;\n}\n\n/**\n * Law 3, from a shell.\n *\n * This is deliberately not \"print the text and hope\": it rebuilds the store from the\n * envelope, checks every hash against the bytes it claims to key, and checks the\n * reconstructed length against the `inputBytes` the result recorded at the time of the\n * cut. A round trip that quietly returns almost-right text is the failure this whole\n * repository is arranged against.\n */\nfunction runReconstruct(text: string, io: CliIo): number {\n const { result, elided } = parseEnvelope(text);\n const store = new MemoryElisionStore();\n\n for (const [hash, content] of Object.entries(elided)) {\n const actual = store.put(content);\n if (actual !== hash) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is not self-consistent — it stores bytes under ` +\n `\"${hash}\" that actually hash to \"${actual}\". Refusing to reconstruct from it.`,\n );\n }\n }\n for (const elision of result.elisions) {\n if (!store.has(elision.hash)) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope is missing the bytes for \"${elision.hash}\". A result ` +\n `without its elided bytes cannot be reconstructed; re-run with --json.`,\n );\n }\n }\n\n const original = reconstruct(result, store);\n const bytes = Buffer.byteLength(original, 'utf8');\n if (bytes !== result.inputBytes) {\n throw new SmeltError(\n `${CLI_NAME}: reconstruction produced ${String(bytes)} bytes but the result ` +\n `recorded ${String(result.inputBytes)}. The round trip did not close.`,\n );\n }\n\n io.stdout(original);\n io.stderr(\n `${CLI_NAME} reconstructed ${String(result.inputBytes)} B from ` +\n `${String(result.elisions.length)} elisions — byte for byte\\n`,\n );\n return EXIT.ok;\n}\n\n/** `peek`, not `retrieve`: writing a file is not the model asking for anything back. */\nfunction envelope(result: SmeltResult, store: ElisionStore): CliJsonEnvelope {\n const elided: Record<string, string> = {};\n for (const elision of result.elisions) {\n const content = store.peek(elision.hash);\n if (content === undefined) {\n throw new SmeltError(\n `${CLI_NAME}: the store does not hold \"${elision.hash}\", which its own result ` +\n `says it elided. Refusing to write an envelope that cannot round-trip.`,\n );\n }\n elided[elision.hash] = content;\n }\n return { format: CLI_JSON_FORMAT, result, elided };\n}\n\nfunction parseEnvelope(text: string): CliJsonEnvelope {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (cause) {\n throw new CliUsageError(\n `${CLI_NAME}: --reconstruct expected a --json envelope, and this is not JSON: ` +\n `${cause instanceof Error ? cause.message : String(cause)}`,\n );\n }\n\n if (typeof value !== 'object' || value === null) {\n throw new CliUsageError(`${CLI_NAME}: --reconstruct expected a JSON object.`);\n }\n const fields = value as Record<string, unknown>;\n\n if (fields['format'] !== CLI_JSON_FORMAT) {\n throw new CliUsageError(\n `${CLI_NAME}: this envelope says format \"${String(fields['format'])}\"; this build ` +\n `reads \"${CLI_JSON_FORMAT}\". Formats are versioned so a mismatch is visible ` +\n `instead of being half-understood.`,\n );\n }\n\n const result = fields['result'];\n if (typeof result !== 'object' || result === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`result\\` object.`);\n }\n const resultFields = result as Record<string, unknown>;\n if (\n typeof resultFields['text'] !== 'string' ||\n typeof resultFields['inputBytes'] !== 'number' ||\n !Array.isArray(resultFields['elisions'])\n ) {\n throw new CliUsageError(\n `${CLI_NAME}: envelope's \\`result\\` is missing text, inputBytes or elisions.`,\n );\n }\n\n const elided = fields['elided'];\n if (typeof elided !== 'object' || elided === null) {\n throw new CliUsageError(`${CLI_NAME}: envelope has no \\`elided\\` map.`);\n }\n for (const [hash, content] of Object.entries(elided)) {\n if (typeof content !== 'string') {\n throw new CliUsageError(`${CLI_NAME}: envelope's \\`elided[\"${hash}\"]\\` is not a string.`);\n }\n }\n\n return {\n format: CLI_JSON_FORMAT,\n result: result as unknown as SmeltResult,\n elided: elided as Record<string, string>,\n };\n}\n\n/**\n * The blob to work on: stdin when no file was named, otherwise the file — read\n * through the ops law, so \"cannot read X\" says the same thing here and in the\n * `smelt_file` tool. Only the stdin leg is the CLI's own; a tool has no stdin.\n */\nfunction readInput(file: string | undefined, io: CliIo): string {\n if (file === undefined) return io.stdin();\n const read = readBlob(file, file);\n if (!read.ok) throw new CliUsageError(`${CLI_NAME}: ${read.refusal}`);\n return read.value;\n}\n\nfunction parseLanguage(raw: string): DetectedLanguage {\n const known: readonly string[] = [...SUPPORTED_LANGUAGES, 'unknown'];\n if (!known.includes(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --language \"${raw}\". Known: ${known.join(', ')}.`,\n );\n }\n return raw as DetectedLanguage;\n}\n\n/** Membership in the {@link PLANNERS} registry is the whole validation. */\nfunction parseStrategy(raw: string | undefined): Strategy | undefined {\n if (raw === undefined) return undefined;\n if (!isStrategy(raw)) {\n throw new CliUsageError(\n `${CLI_NAME}: unknown --strategy \"${raw}\". Known: ${STRATEGIES.join(', ')}.`,\n );\n }\n return raw;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { RetrieveStats } from '../../types.ts';
1
+ import type { RetrieveStats, RuleLedgerEntry } from '../../types.ts';
2
2
  import type { ResolvedStoreRun } from './retrieve.ts';
3
3
  import type { Subcommand } from './subcommand.ts';
4
4
  /**
@@ -32,12 +32,17 @@ export interface ResolvedStatsRun {
32
32
  * `smelt map` has one: the two envelopes carry different structures and must move
33
33
  * independently.
34
34
  */
35
- export declare const CLI_STATS_JSON_FORMAT = "smelt-stats-cli/v1";
36
- /** What `smelt stats --json` prints: the {@link RetrieveStats} verbatim, versioned. */
35
+ export declare const CLI_STATS_JSON_FORMAT = "smelt-stats-cli/v2";
36
+ /**
37
+ * What `smelt stats --json` prints: the {@link RetrieveStats} verbatim, and the
38
+ * ledger beside them, versioned. v2 added `ledger`; v1 carried `stats` alone.
39
+ */
37
40
  export interface CliStatsJsonEnvelope {
38
41
  readonly format: string;
39
42
  /** The {@link RetrieveStats} exactly as the store's `stats()` returned them. */
40
43
  readonly stats: RetrieveStats;
44
+ /** The store's per-rule ledger, exactly as `ledger()` returned it. */
45
+ readonly ledger: readonly RuleLedgerEntry[];
41
46
  }
42
47
  export declare const statsCommand: Subcommand<StatsInvocation, ResolvedStatsRun>;
43
48
  //# sourceMappingURL=stats.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/stats.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAMpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;;;;;;;;GAeG;AAEH,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAE1D,uFAAuF;AACvF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;CAC/B;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,gBAAgB,CA2DtE,CAAC","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { openStore } from '../../ops/inputs.ts';\nimport { readCounters } from '../../ops/verbs.ts';\nimport type { RetrieveStats } from '../../types.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport { resolveStoreRun } from './retrieve.ts';\nimport type { ResolvedStoreRun } from './retrieve.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt stats` — the store's counters, without touching them.\n *\n * Reading stats does NOT count as a retrieval: `stats()` folds the journal and scans\n * the blobs, journaling nothing, so watching the expansion rate can never move it —\n * an observer that inflated its own metric would make the honest signal dishonest.\n *\n * The plain form is one `name value` per line, greppable and stable; `--json` emits\n * the {@link RetrieveStats} verbatim in its own versioned envelope\n * ({@link CLI_STATS_JSON_FORMAT}), like every other machine-read surface here.\n *\n * It resolves through `retrieve`'s {@link resolveStoreRun} because the two verbs\n * share one merge — the store leg, and the same refusal when that store cannot\n * outlive a run. Sharing the function is the point; sharing a struct with a hash\n * field `stats` would never read is not.\n */\n\n/** `smelt stats` — the store's counters, read without touching them. */\nexport interface StatsInvocation {\n readonly mode: 'stats';\n readonly json: boolean;\n}\n\n/** What `stats` runs on: the shared store leg, plus how to print it. */\nexport interface ResolvedStatsRun {\n readonly store: ResolvedStoreRun;\n readonly json: boolean;\n}\n\n/**\n * The `smelt stats --json` envelope format. Its own version line for the same reason\n * `smelt map` has one: the two envelopes carry different structures and must move\n * independently.\n */\nexport const CLI_STATS_JSON_FORMAT = 'smelt-stats-cli/v1';\n\n/** What `smelt stats --json` prints: the {@link RetrieveStats} verbatim, versioned. */\nexport interface CliStatsJsonEnvelope {\n readonly format: string;\n /** The {@link RetrieveStats} exactly as the store's `stats()` returned them. */\n readonly stats: RetrieveStats;\n}\n\nexport const statsCommand: Subcommand<StatsInvocation, ResolvedStatsRun> = {\n name: 'stats',\n flags: ['json'],\n refusal: `stats reads counters; there is nothing to budget, focus or plan.`,\n usage: {\n synopsis: ['stats [--json]'],\n section: {\n heading: 'RETRIEVE & STATS',\n body:\n ` ${CLI_NAME} stats prints the same store's counters, one \\`name value\\` per line —\\n` +\n ` elisionsStored, bytesStored, retrieveCalls, uniqueRetrieved, expansionRate,\\n` +\n ` allElisionsRetrieved — and reading them is NOT counted as a retrieval. --json\\n` +\n ` emits the RetrieveStats verbatim in its own versioned envelope.\\n` +\n `\\n` +\n ` Both need somewhere for elisions to outlive the run that made them: a\\n` +\n ` smelt.config.json with a directory store (\\`${CLI_NAME} init\\` writes one). With a\\n` +\n ` memory store — or no config — every run's store dies with its process, so there\\n` +\n ` is nothing to retrieve across runs, and that is a usage error rather than a\\n` +\n ` quiet empty answer.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): StatsInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: stats takes no further arguments, got ` +\n `${positionals.slice(1).join(', ')}. It reports on the one configured store.`,\n );\n }\n return { mode: 'stats', json: values.json === true };\n },\n\n resolve(invocation: StatsInvocation, config: ConfigSource): ResolvedStatsRun {\n return { store: resolveStoreRun('stats', config()), json: invocation.json };\n },\n\n run(resolved: ResolvedStatsRun, io: CliIo): number {\n const store = openStore({ kind: 'directory', path: resolved.store.storePath });\n const stats = readCounters({ store });\n\n if (resolved.json) {\n const statsEnvelope: CliStatsJsonEnvelope = { format: CLI_STATS_JSON_FORMAT, stats };\n io.stdout(`${JSON.stringify(statsEnvelope, null, 2)}\\n`);\n return EXIT.ok;\n }\n\n io.stdout(\n [\n `elisionsStored ${String(stats.elisionsStored)}`,\n `bytesStored ${String(stats.bytesStored)}`,\n `retrieveCalls ${String(stats.retrieveCalls)}`,\n `uniqueRetrieved ${String(stats.uniqueRetrieved)}`,\n `expansionRate ${String(stats.expansionRate)}`,\n `allElisionsRetrieved ${String(stats.allElisionsRetrieved)}`,\n '',\n ].join('\\n'),\n );\n return EXIT.ok;\n },\n};\n"]}
1
+ {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/stats.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAMrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;;;;;;;;GAeG;AAEH,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;CAC7C;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,eAAe,EAAE,gBAAgB,CAqEtE,CAAC","sourcesContent":["import { CliUsageError } from '../../errors.ts';\nimport { openStore } from '../../ops/inputs.ts';\nimport { readCounters, readLedger } from '../../ops/verbs.ts';\nimport type { RetrieveStats, RuleLedgerEntry } from '../../types.ts';\nimport { CLI_NAME, EXIT } from '../shell.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues } from './flags.ts';\nimport { resolveStoreRun } from './retrieve.ts';\nimport type { ResolvedStoreRun } from './retrieve.ts';\nimport type { ConfigSource, Subcommand } from './subcommand.ts';\n\n/**\n * `smelt stats` — the store's counters, without touching them.\n *\n * Reading stats does NOT count as a retrieval: `stats()` folds the journal and scans\n * the blobs, journaling nothing, so watching the expansion rate can never move it —\n * an observer that inflated its own metric would make the honest signal dishonest.\n *\n * The plain form is one `name value` per line, greppable and stable; `--json` emits\n * the {@link RetrieveStats} verbatim in its own versioned envelope\n * ({@link CLI_STATS_JSON_FORMAT}), like every other machine-read surface here.\n *\n * It resolves through `retrieve`'s {@link resolveStoreRun} because the two verbs\n * share one merge — the store leg, and the same refusal when that store cannot\n * outlive a run. Sharing the function is the point; sharing a struct with a hash\n * field `stats` would never read is not.\n */\n\n/** `smelt stats` — the store's counters, read without touching them. */\nexport interface StatsInvocation {\n readonly mode: 'stats';\n readonly json: boolean;\n}\n\n/** What `stats` runs on: the shared store leg, plus how to print it. */\nexport interface ResolvedStatsRun {\n readonly store: ResolvedStoreRun;\n readonly json: boolean;\n}\n\n/**\n * The `smelt stats --json` envelope format. Its own version line for the same reason\n * `smelt map` has one: the two envelopes carry different structures and must move\n * independently.\n */\nexport const CLI_STATS_JSON_FORMAT = 'smelt-stats-cli/v2';\n\n/**\n * What `smelt stats --json` prints: the {@link RetrieveStats} verbatim, and the\n * ledger beside them, versioned. v2 added `ledger`; v1 carried `stats` alone.\n */\nexport interface CliStatsJsonEnvelope {\n readonly format: string;\n /** The {@link RetrieveStats} exactly as the store's `stats()` returned them. */\n readonly stats: RetrieveStats;\n /** The store's per-rule ledger, exactly as `ledger()` returned it. */\n readonly ledger: readonly RuleLedgerEntry[];\n}\n\nexport const statsCommand: Subcommand<StatsInvocation, ResolvedStatsRun> = {\n name: 'stats',\n flags: ['json'],\n refusal: `stats reads counters; there is nothing to budget, focus or plan.`,\n usage: {\n synopsis: ['stats [--json]'],\n section: {\n heading: 'RETRIEVE & STATS',\n body:\n ` ${CLI_NAME} stats prints the same store's counters, one \\`name value\\` per line —\\n` +\n ` elisionsStored, bytesStored, retrieveCalls, uniqueRetrieved, expansionRate,\\n` +\n ` allElisionsRetrieved — then the ledger, one rule.<id>.stored and\\n` +\n ` rule.<id>.retrieved per rule that cut anything: which rule's cuts get asked\\n` +\n ` for back. Reading them is NOT counted as a retrieval. --json emits the\\n` +\n ` RetrieveStats and the ledger verbatim in their own versioned envelope.\\n` +\n `\\n` +\n ` Both need somewhere for elisions to outlive the run that made them: a\\n` +\n ` smelt.config.json with a directory store (\\`${CLI_NAME} init\\` writes one). With a\\n` +\n ` memory store — or no config — every run's store dies with its process, so there\\n` +\n ` is nothing to retrieve across runs, and that is a usage error rather than a\\n` +\n ` quiet empty answer.`,\n },\n },\n\n parse(values: FlagValues, positionals: readonly string[]): StatsInvocation {\n if (positionals.length > 1) {\n throw new CliUsageError(\n `${CLI_NAME}: stats takes no further arguments, got ` +\n `${positionals.slice(1).join(', ')}. It reports on the one configured store.`,\n );\n }\n return { mode: 'stats', json: values.json === true };\n },\n\n resolve(invocation: StatsInvocation, config: ConfigSource): ResolvedStatsRun {\n return { store: resolveStoreRun('stats', config()), json: invocation.json };\n },\n\n run(resolved: ResolvedStatsRun, io: CliIo): number {\n const store = openStore({ kind: 'directory', path: resolved.store.storePath });\n const stats = readCounters({ store });\n // The directory store always keeps a ledger; the `?? []` is the type's escape\n // hatch for a custom store, never a case this verb reaches.\n const ledger = readLedger({ store }) ?? [];\n\n if (resolved.json) {\n const statsEnvelope: CliStatsJsonEnvelope = { format: CLI_STATS_JSON_FORMAT, stats, ledger };\n io.stdout(`${JSON.stringify(statsEnvelope, null, 2)}\\n`);\n return EXIT.ok;\n }\n\n io.stdout(\n [\n `elisionsStored ${String(stats.elisionsStored)}`,\n `bytesStored ${String(stats.bytesStored)}`,\n `retrieveCalls ${String(stats.retrieveCalls)}`,\n `uniqueRetrieved ${String(stats.uniqueRetrieved)}`,\n `expansionRate ${String(stats.expansionRate)}`,\n `allElisionsRetrieved ${String(stats.allElisionsRetrieved)}`,\n // The ledger, in the same `name value` shape: greppable, one fact per line.\n ...ledger.flatMap((entry) => [\n `rule.${entry.rule}.stored ${String(entry.stored)}`,\n `rule.${entry.rule}.retrieved ${String(entry.retrieved)}`,\n ]),\n '',\n ].join('\\n'),\n );\n return EXIT.ok;\n },\n};\n"]}