@smeltjs/core 0.4.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 (181) 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/bin.d.ts.map +1 -1
  5. package/dist/cli/bin.js +9 -4
  6. package/dist/cli/bin.js.map +1 -1
  7. package/dist/cli/doctor.d.ts +66 -0
  8. package/dist/cli/doctor.d.ts.map +1 -0
  9. package/dist/cli/doctor.js +137 -0
  10. package/dist/cli/doctor.js.map +1 -0
  11. package/dist/cli/hooks.d.ts +36 -2
  12. package/dist/cli/hooks.d.ts.map +1 -1
  13. package/dist/cli/hooks.js +108 -64
  14. package/dist/cli/hooks.js.map +1 -1
  15. package/dist/cli/init.d.ts.map +1 -1
  16. package/dist/cli/init.js +12 -16
  17. package/dist/cli/init.js.map +1 -1
  18. package/dist/cli/installed.d.ts +56 -0
  19. package/dist/cli/installed.d.ts.map +1 -0
  20. package/dist/cli/installed.js +105 -0
  21. package/dist/cli/installed.js.map +1 -0
  22. package/dist/cli/lava.d.ts +31 -0
  23. package/dist/cli/lava.d.ts.map +1 -0
  24. package/dist/cli/lava.js +82 -0
  25. package/dist/cli/lava.js.map +1 -0
  26. package/dist/cli/report.d.ts +10 -1
  27. package/dist/cli/report.d.ts.map +1 -1
  28. package/dist/cli/report.js +16 -1
  29. package/dist/cli/report.js.map +1 -1
  30. package/dist/cli/setup.d.ts +87 -0
  31. package/dist/cli/setup.d.ts.map +1 -0
  32. package/dist/cli/setup.js +513 -0
  33. package/dist/cli/setup.js.map +1 -0
  34. package/dist/cli/shell.d.ts +11 -4
  35. package/dist/cli/shell.d.ts.map +1 -1
  36. package/dist/cli/shell.js.map +1 -1
  37. package/dist/cli/subcommands/agents.d.ts.map +1 -1
  38. package/dist/cli/subcommands/agents.js +2 -1
  39. package/dist/cli/subcommands/agents.js.map +1 -1
  40. package/dist/cli/subcommands/doctor.d.ts +15 -0
  41. package/dist/cli/subcommands/doctor.d.ts.map +1 -0
  42. package/dist/cli/subcommands/doctor.js +43 -0
  43. package/dist/cli/subcommands/doctor.js.map +1 -0
  44. package/dist/cli/subcommands/flags.d.ts +16 -0
  45. package/dist/cli/subcommands/flags.d.ts.map +1 -1
  46. package/dist/cli/subcommands/flags.js +43 -4
  47. package/dist/cli/subcommands/flags.js.map +1 -1
  48. package/dist/cli/subcommands/hooks.d.ts.map +1 -1
  49. package/dist/cli/subcommands/hooks.js +10 -2
  50. package/dist/cli/subcommands/hooks.js.map +1 -1
  51. package/dist/cli/subcommands/init.d.ts.map +1 -1
  52. package/dist/cli/subcommands/init.js +2 -1
  53. package/dist/cli/subcommands/init.js.map +1 -1
  54. package/dist/cli/subcommands/registry.d.ts +3 -1
  55. package/dist/cli/subcommands/registry.d.ts.map +1 -1
  56. package/dist/cli/subcommands/registry.js +6 -0
  57. package/dist/cli/subcommands/registry.js.map +1 -1
  58. package/dist/cli/subcommands/setup.d.ts +23 -0
  59. package/dist/cli/subcommands/setup.d.ts.map +1 -0
  60. package/dist/cli/subcommands/setup.js +87 -0
  61. package/dist/cli/subcommands/setup.js.map +1 -0
  62. package/dist/cli/subcommands/smelt.d.ts +3 -0
  63. package/dist/cli/subcommands/smelt.d.ts.map +1 -1
  64. package/dist/cli/subcommands/smelt.js +5 -1
  65. package/dist/cli/subcommands/smelt.js.map +1 -1
  66. package/dist/cli/subcommands/stats.d.ts +8 -3
  67. package/dist/cli/subcommands/stats.d.ts.map +1 -1
  68. package/dist/cli/subcommands/stats.js +15 -5
  69. package/dist/cli/subcommands/stats.js.map +1 -1
  70. package/dist/cli/subcommands/subcommand.d.ts +1 -1
  71. package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
  72. package/dist/cli/subcommands/subcommand.js.map +1 -1
  73. package/dist/cli/wizard.d.ts +72 -0
  74. package/dist/cli/wizard.d.ts.map +1 -0
  75. package/dist/cli/wizard.js +86 -0
  76. package/dist/cli/wizard.js.map +1 -0
  77. package/dist/errors.d.ts +10 -0
  78. package/dist/errors.d.ts.map +1 -1
  79. package/dist/errors.js +10 -0
  80. package/dist/errors.js.map +1 -1
  81. package/dist/harness/claude-code.d.ts.map +1 -1
  82. package/dist/harness/claude-code.js +10 -0
  83. package/dist/harness/claude-code.js.map +1 -1
  84. package/dist/harness/codex.d.ts.map +1 -1
  85. package/dist/harness/codex.js +1 -0
  86. package/dist/harness/codex.js.map +1 -1
  87. package/dist/harness/grok.d.ts.map +1 -1
  88. package/dist/harness/grok.js +4 -1
  89. package/dist/harness/grok.js.map +1 -1
  90. package/dist/harness/kilocode.d.ts.map +1 -1
  91. package/dist/harness/kilocode.js +1 -1
  92. package/dist/harness/kilocode.js.map +1 -1
  93. package/dist/harness/opencode.d.ts.map +1 -1
  94. package/dist/harness/opencode.js +10 -0
  95. package/dist/harness/opencode.js.map +1 -1
  96. package/dist/harness/profile.d.ts +23 -2
  97. package/dist/harness/profile.d.ts.map +1 -1
  98. package/dist/harness/profile.js.map +1 -1
  99. package/dist/harness/snippet.d.ts +14 -1
  100. package/dist/harness/snippet.d.ts.map +1 -1
  101. package/dist/harness/snippet.js +18 -2
  102. package/dist/harness/snippet.js.map +1 -1
  103. package/dist/hooks/focus-terms.d.ts +57 -0
  104. package/dist/hooks/focus-terms.d.ts.map +1 -0
  105. package/dist/hooks/focus-terms.js +230 -0
  106. package/dist/hooks/focus-terms.js.map +1 -0
  107. package/dist/hooks/guard-core.d.ts +8 -16
  108. package/dist/hooks/guard-core.d.ts.map +1 -1
  109. package/dist/hooks/guard-core.js +22 -120
  110. package/dist/hooks/guard-core.js.map +1 -1
  111. package/dist/index.d.ts +16 -4
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +12 -3
  114. package/dist/index.js.map +1 -1
  115. package/dist/ops/index.d.ts +3 -3
  116. package/dist/ops/index.d.ts.map +1 -1
  117. package/dist/ops/index.js +2 -2
  118. package/dist/ops/index.js.map +1 -1
  119. package/dist/ops/verbs.d.ts +57 -1
  120. package/dist/ops/verbs.d.ts.map +1 -1
  121. package/dist/ops/verbs.js +49 -1
  122. package/dist/ops/verbs.js.map +1 -1
  123. package/dist/plan/auto.d.ts +9 -2
  124. package/dist/plan/auto.d.ts.map +1 -1
  125. package/dist/plan/auto.js +14 -2
  126. package/dist/plan/auto.js.map +1 -1
  127. package/dist/plan/diff.d.ts +45 -0
  128. package/dist/plan/diff.d.ts.map +1 -0
  129. package/dist/plan/diff.js +284 -0
  130. package/dist/plan/diff.js.map +1 -0
  131. package/dist/plan/json.d.ts +37 -0
  132. package/dist/plan/json.d.ts.map +1 -0
  133. package/dist/plan/json.js +181 -0
  134. package/dist/plan/json.js.map +1 -0
  135. package/dist/plan/kind.d.ts +28 -0
  136. package/dist/plan/kind.d.ts.map +1 -0
  137. package/dist/plan/kind.js +57 -0
  138. package/dist/plan/kind.js.map +1 -0
  139. package/dist/plan/offsets.d.ts +8 -0
  140. package/dist/plan/offsets.d.ts.map +1 -0
  141. package/dist/plan/offsets.js +19 -0
  142. package/dist/plan/offsets.js.map +1 -0
  143. package/dist/plan/planners.d.ts +15 -5
  144. package/dist/plan/planners.d.ts.map +1 -1
  145. package/dist/plan/planners.js +13 -5
  146. package/dist/plan/planners.js.map +1 -1
  147. package/dist/plan/structural.d.ts.map +1 -1
  148. package/dist/plan/structural.js +106 -14
  149. package/dist/plan/structural.js.map +1 -1
  150. package/dist/retrieve.d.ts +22 -1
  151. package/dist/retrieve.d.ts.map +1 -1
  152. package/dist/retrieve.js +59 -0
  153. package/dist/retrieve.js.map +1 -1
  154. package/dist/setup/recipe.d.ts +73 -0
  155. package/dist/setup/recipe.d.ts.map +1 -0
  156. package/dist/setup/recipe.js +76 -0
  157. package/dist/setup/recipe.js.map +1 -0
  158. package/dist/smelter.d.ts +4 -0
  159. package/dist/smelter.d.ts.map +1 -1
  160. package/dist/smelter.js +4 -0
  161. package/dist/smelter.js.map +1 -1
  162. package/dist/stats.d.ts +16 -1
  163. package/dist/stats.d.ts.map +1 -1
  164. package/dist/stats.js +29 -0
  165. package/dist/stats.js.map +1 -1
  166. package/dist/store-dir.d.ts +9 -2
  167. package/dist/store-dir.d.ts.map +1 -1
  168. package/dist/store-dir.js +48 -7
  169. package/dist/store-dir.js.map +1 -1
  170. package/dist/store.d.ts +4 -2
  171. package/dist/store.d.ts.map +1 -1
  172. package/dist/store.js +12 -2
  173. package/dist/store.js.map +1 -1
  174. package/dist/text/json-edit.d.ts +15 -1
  175. package/dist/text/json-edit.d.ts.map +1 -1
  176. package/dist/text/json-edit.js +76 -5
  177. package/dist/text/json-edit.js.map +1 -1
  178. package/dist/types.d.ts +116 -2
  179. package/dist/types.d.ts.map +1 -1
  180. package/dist/types.js.map +1 -1
  181. package/package.json +1 -1
@@ -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"]}
@@ -1,6 +1,6 @@
1
1
  import { CliUsageError } from '../../errors.js';
2
2
  import { openStore } from '../../ops/inputs.js';
3
- import { readCounters } from '../../ops/verbs.js';
3
+ import { readCounters, readLedger } from '../../ops/verbs.js';
4
4
  import { CLI_NAME, EXIT } from '../shell.js';
5
5
  import { resolveStoreRun } from './retrieve.js';
6
6
  /**
@@ -8,7 +8,7 @@ import { resolveStoreRun } from './retrieve.js';
8
8
  * `smelt map` has one: the two envelopes carry different structures and must move
9
9
  * independently.
10
10
  */
11
- export const CLI_STATS_JSON_FORMAT = 'smelt-stats-cli/v1';
11
+ export const CLI_STATS_JSON_FORMAT = 'smelt-stats-cli/v2';
12
12
  export const statsCommand = {
13
13
  name: 'stats',
14
14
  flags: ['json'],
@@ -19,8 +19,10 @@ export const statsCommand = {
19
19
  heading: 'RETRIEVE & STATS',
20
20
  body: ` ${CLI_NAME} stats prints the same store's counters, one \`name value\` per line —\n` +
21
21
  ` elisionsStored, bytesStored, retrieveCalls, uniqueRetrieved, expansionRate,\n` +
22
- ` allElisionsRetrieved — and reading them is NOT counted as a retrieval. --json\n` +
23
- ` emits the RetrieveStats verbatim in its own versioned envelope.\n` +
22
+ ` allElisionsRetrieved — then the ledger, one rule.<id>.stored and\n` +
23
+ ` rule.<id>.retrieved per rule that cut anything: which rule's cuts get asked\n` +
24
+ ` for back. Reading them is NOT counted as a retrieval. --json emits the\n` +
25
+ ` RetrieveStats and the ledger verbatim in their own versioned envelope.\n` +
24
26
  `\n` +
25
27
  ` Both need somewhere for elisions to outlive the run that made them: a\n` +
26
28
  ` smelt.config.json with a directory store (\`${CLI_NAME} init\` writes one). With a\n` +
@@ -42,8 +44,11 @@ export const statsCommand = {
42
44
  run(resolved, io) {
43
45
  const store = openStore({ kind: 'directory', path: resolved.store.storePath });
44
46
  const stats = readCounters({ store });
47
+ // The directory store always keeps a ledger; the `?? []` is the type's escape
48
+ // hatch for a custom store, never a case this verb reaches.
49
+ const ledger = readLedger({ store }) ?? [];
45
50
  if (resolved.json) {
46
- const statsEnvelope = { format: CLI_STATS_JSON_FORMAT, stats };
51
+ const statsEnvelope = { format: CLI_STATS_JSON_FORMAT, stats, ledger };
47
52
  io.stdout(`${JSON.stringify(statsEnvelope, null, 2)}\n`);
48
53
  return EXIT.ok;
49
54
  }
@@ -54,6 +59,11 @@ export const statsCommand = {
54
59
  `uniqueRetrieved ${String(stats.uniqueRetrieved)}`,
55
60
  `expansionRate ${String(stats.expansionRate)}`,
56
61
  `allElisionsRetrieved ${String(stats.allElisionsRetrieved)}`,
62
+ // The ledger, in the same `name value` shape: greppable, one fact per line.
63
+ ...ledger.flatMap((entry) => [
64
+ `rule.${entry.rule}.stored ${String(entry.stored)}`,
65
+ `rule.${entry.rule}.retrieved ${String(entry.retrieved)}`,
66
+ ]),
57
67
  '',
58
68
  ].join('\n'));
59
69
  return EXIT.ok;
@@ -1 +1 @@
1
- {"version":3,"file":"stats.js","sourceRoot":"","sources":["../../../src/cli/subcommands/stats.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAI7C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAiChD;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,oBAAoB,CAAC;AAS1D,MAAM,CAAC,MAAM,YAAY,GAAkD;IACzE,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,OAAO,EAAE,kEAAkE;IAC3E,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,gBAAgB,CAAC;QAC5B,OAAO,EAAE;YACP,OAAO,EAAE,kBAAkB;YAC3B,IAAI,EACF,KAAK,QAAQ,0EAA0E;gBACvF,iFAAiF;gBACjF,mFAAmF;gBACnF,qEAAqE;gBACrE,IAAI;gBACJ,2EAA2E;gBAC3E,iDAAiD,QAAQ,+BAA+B;gBACxF,qFAAqF;gBACrF,iFAAiF;gBACjF,uBAAuB;SAC1B;KACF;IAED,KAAK,CAAC,MAAkB,EAAE,WAA8B;QACtD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,0CAA0C;gBACnD,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,2CAA2C,CAChF,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;IACvD,CAAC;IAED,OAAO,CAAC,UAA2B,EAAE,MAAoB;QACvD,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;IAC9E,CAAC;IAED,GAAG,CAAC,QAA0B,EAAE,EAAS;QACvC,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;QAC/E,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAEtC,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;YAClB,MAAM,aAAa,GAAyB,EAAE,MAAM,EAAE,qBAAqB,EAAE,KAAK,EAAE,CAAC;YACrF,EAAE,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YACzD,OAAO,IAAI,CAAC,EAAE,CAAC;QACjB,CAAC;QAED,EAAE,CAAC,MAAM,CACP;YACE,kBAAkB,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE;YAChD,eAAe,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE;YAC1C,iBAAiB,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE;YAC9C,mBAAmB,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE;YAClD,iBAAiB,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE;YAC9C,wBAAwB,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE;YAC5D,EAAE;SACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;QACF,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;CACF,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.js","sourceRoot":"","sources":["../../../src/cli/subcommands/stats.ts"],"names":[],"mappings":"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","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"]}
@@ -9,7 +9,7 @@ import type { FlagValues, VerbFlag } from './flags.ts';
9
9
  * Order is meaningful: it is the order `--help` renders every derived list in, and the
10
10
  * order a refusal names owners in. Keep it stable and append new verbs at the end.
11
11
  */
12
- export type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents';
12
+ export type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents' | 'setup' | 'doctor';
13
13
  /** The verb a bare invocation selects — a file or stdin, with no subcommand word. */
14
14
  export declare const DEFAULT_VERB = "smelt";
15
15
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExF,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAAyB,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,YAAY,GAAG,SAAS,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC;;;;;OAKG;IACH,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7D;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,CAAC;IAChD,wEAAwE;IACxE,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
1
+ {"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GACd,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5F,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAAyB,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,YAAY,GAAG,SAAS,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC;;;;;OAKG;IACH,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7D;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,CAAC;IAChD,wEAAwE;IACxE,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb =\n 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents' | 'setup' | 'doctor';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"subcommand.js","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAeA,qFAAqF;AACrF,MAAM,CAAC,MAAM,YAAY,GAAG,OAAsB,CAAC","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb = 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
1
+ {"version":3,"file":"subcommand.js","sourceRoot":"","sources":["../../../src/cli/subcommands/subcommand.ts"],"names":[],"mappings":"AAgBA,qFAAqF;AACrF,MAAM,CAAC,MAAM,YAAY,GAAG,OAAsB,CAAC","sourcesContent":["import type { LoadedConfig } from '../config.ts';\nimport type { CliIo } from '../shell.ts';\n\nimport type { FlagValues, VerbFlag } from './flags.ts';\n\n/**\n * The verbs `smelt` answers to. `'smelt'` is the default verb — the bare\n * `smelt <file>` / `smelt < input` run — and is the only one with no word on the\n * command line, which is why a file literally named `map` needs `./map`.\n *\n * Order is meaningful: it is the order `--help` renders every derived list in, and the\n * order a refusal names owners in. Keep it stable and append new verbs at the end.\n */\nexport type Verb =\n 'smelt' | 'init' | 'map' | 'retrieve' | 'stats' | 'hooks' | 'agents' | 'setup' | 'doctor';\n\n/** The verb a bare invocation selects — a file or stdin, with no subcommand word. */\nexport const DEFAULT_VERB = 'smelt' satisfies Verb;\n\n/**\n * The nearest `smelt.config.json`, loaded on demand.\n *\n * A thunk rather than a value, because two verbs must **not** load it: `init` and\n * `hooks` read the config file themselves, with their own tolerance, and a wizard you\n * run to *fix* a malformed config cannot be a wizard that refuses to start because the\n * config is malformed. Loading eagerly in the dispatcher would quietly turn that into\n * a usage error.\n */\nexport type ConfigSource = () => LoadedConfig | undefined;\n\n/** What one verb contributes to `--help`. See `cli/usage.ts` for the rendering. */\nexport interface SubcommandUsage {\n /**\n * The command's everyday forms, rendered in the first block of USAGE in registry\n * order. Each entry is the text *after* the CLI name.\n */\n readonly synopsis: readonly string[];\n /**\n * Forms rendered in the second block, after every command's everyday forms: the\n * round trip (`--reconstruct`) and the one-time setup (`init`) — jobs you do\n * occasionally rather than the shape of a normal run. It keeps USAGE reading\n * top-to-bottom by how often you type a thing without anyone arranging the list.\n */\n readonly occasional?: readonly string[];\n /**\n * The verb's own section of the help — a heading and its body, already indented.\n * Two verbs may declare the same heading (`retrieve` and `stats` share RETRIEVE &\n * STATS, because the loop is one story); their bodies are joined under it in\n * registry order.\n */\n readonly section?: { readonly heading: string; readonly body: string };\n}\n\n/**\n * One subcommand: everything the CLI knows about one verb, in one place.\n *\n * This is the seam the CLI was missing. A verb used to be a *shape* restated in four\n * modules — an `*Invocation` interface and a `parse*Args` in `cli/args.ts`, a\n * `Resolved*Run` and a `resolve*` in `cli/resolve.ts`, a `case` and a `run*` in\n * `cli/run.ts`, plus a hand-written help block and a hand-written refusal for every\n * flag it did not want. The refusals were the compounding cost: because no verb owned\n * its flags, every verb refused every other verb's flags by hand, so an eleventh flag\n * edited five messages.\n *\n * Now a verb is one file exporting one of these, and `SUBCOMMANDS` in `./registry.ts`\n * is `Record<Verb, …>` — the {@link LANGUAGE_PROFILES} / {@link HARNESS_PROFILES}\n * pattern, so totality is a compile error and every rendered list (USAGE, the sections,\n * the `map only.` prefixes in OPTIONS) is a derived view.\n *\n * The type parameters are the verb's own two shapes, and they never merge:\n *\n * @typeParam I - what the verb's `parse` returns, e.g. `MapInvocation`. Pure data, so\n * the parse is testable on its own.\n * @typeParam R - what its `resolve` returns, e.g. `ResolvedMapRun`. The verb's single\n * merge of flags + config + built-ins, each value carrying its provenance. Deliberately\n * one type per verb: `map` has no store, no strategy and no stdin, and a shared struct\n * whose fields are lies for half its users is not a seam, it is a coincidence.\n */\nexport interface Subcommand<I, R> {\n /** The word on the command line — and the registry key. `'smelt'` has no word. */\n readonly name: Verb;\n /**\n * The flags this verb owns. **The whole point.** Every flag outside this list is\n * refused by one generated message (`refuseForeignFlags` in `./registry.ts`) rather\n * than by prose the next verb has to write again. A flag silently ignored would be a\n * setting the user believed was in force.\n */\n readonly flags: readonly VerbFlag[];\n /**\n * What this verb *is*, as one sentence — the tail of the generated refusal, and the\n * only part of it a verb writes. It answers \"why not here?\", which is the half of\n * the old hand-written messages worth keeping; the offending flag, this verb's name\n * and the verb the flag does belong to are all derived from the registry.\n */\n readonly refusal: string;\n /** What this verb contributes to `--help`. Rendered, never hand-arranged. */\n readonly usage: SubcommandUsage;\n /**\n * The verb's own validation, over the flags it owns and the positionals it was\n * given (index 0 is the verb word itself, except for the default verb).\n *\n * @throws {CliUsageError} on anything the user got wrong. Never guesses.\n */\n parse(values: FlagValues, positionals: readonly string[]): I;\n /**\n * The verb's single merge of flags + config + built-ins. Precedence for this verb\n * lives here and nowhere else, so a precedence question is answered by one function\n * instead of by reading two files.\n *\n * @throws {CliUsageError} when a required value has no source — the budget, for the\n * two verbs that need one.\n */\n resolve(invocation: I, config: ConfigSource): R;\n /** Execute the resolved run straight-line, and return the exit code. */\n run(resolved: R, io: CliIo): number | Promise<number>;\n}\n"]}
@@ -0,0 +1,72 @@
1
+ import type { AnswerStream } from './shell.ts';
2
+ /**
3
+ * The wizard kit — the stream machinery every interactive verb shares, extracted
4
+ * once because its third copy was the one that raced: `setup` re-typed the ask
5
+ * adapter without awaiting a step, and two prompts were on screen at once. A kit is
6
+ * not an abstraction here; it is the deletion of two copies of a thing that had
7
+ * already drifted (review II, KOT-255).
8
+ *
9
+ * What lives here, and nothing else:
10
+ *
11
+ * - {@link wizardAsk}: the ask adapter — `answerReader`, the prompt echo, the trim,
12
+ * and the EOF refusal, whose message is each verb's own (init, hooks and setup
13
+ * owe the reader different last sentences).
14
+ * - {@link walkSteps}: the step machine with real back-navigation — the loop
15
+ * `hooks` always had, `init` mirrored, and `setup` faked with "lands on the last
16
+ * question". A step is `(ask) => 'ok' | 'back'`; back at the first step is
17
+ * answered, not ignored.
18
+ * - {@link confirmLoop} / {@link confirmYesNo}: the confirm prompts, retry copy
19
+ * included, because two verbs spelling "yes to…" differently is drift.
20
+ * - {@link listPlannedFiles} / {@link writePlannedFile}: the plan listing and the
21
+ * one file-write mechanic (mkdir, write, chmod) every apply loop performs.
22
+ *
23
+ * Pure IO plumbing: no verb knowledge, no domain facts, no rendering opinions — the
24
+ * lava adapter stays outside, at the verb boundary, where one switch styles them all.
25
+ */
26
+ /** One question at a time, each answered by a line. What every wizard's `ask` is. */
27
+ export type Ask = (prompt: string) => Promise<string>;
28
+ /** One wizard step: `'ok'` advances, `'back'` returns to the previous step. */
29
+ export type Step = (ask: Ask) => Promise<'ok' | 'back'>;
30
+ /**
31
+ * The ask adapter over an injected stream. `eofMessage` is the refusal's tail — the
32
+ * one sentence that differs per verb, and the only one allowed to: setup's teaches
33
+ * the non-interactive flags, hooks' says what happens to already-confirmed writes,
34
+ * init's states the plain fact.
35
+ */
36
+ export declare function wizardAsk(input: AnswerStream, output: (text: string) => void, eofMessage: string): {
37
+ ask: Ask;
38
+ release: () => Promise<void>;
39
+ };
40
+ /**
41
+ * The step machine: steps in order, `back` moving one step back. The first step's
42
+ * back is answered where the user can read it — there is nothing before it, and
43
+ * pretending otherwise is how a wizard eats an answer. `startAt` is where a confirm's
44
+ * `back` lands: the last step, not the first.
45
+ */
46
+ export declare function walkSteps(steps: readonly Step[], ask: Ask, say: (text: string) => void, startAt?: number): Promise<void>;
47
+ /**
48
+ * `confirm (yes / no / back)> ` — the three-way confirm. `retryCopy` completes the
49
+ * "yes to …, no to …, back to …" sentence, which is verb knowledge.
50
+ */
51
+ export declare function confirmLoop(ask: Ask, retryCopy: string): Promise<'yes' | 'no' | 'back'>;
52
+ /** `confirm (yes / no)> ` — the two-way confirm, for flows with no step to return to. */
53
+ export declare function confirmYesNo(ask: Ask, retryCopy: string): Promise<'yes' | 'no'>;
54
+ /** One planned file, as the listing and the write mechanic both see it. */
55
+ export interface PlannedFileLike {
56
+ readonly name: string;
57
+ readonly path: string;
58
+ readonly content: string;
59
+ readonly exists: boolean;
60
+ readonly unchanged: boolean;
61
+ readonly mode?: number;
62
+ }
63
+ /** One skipped file, with the reason the plan refused it. */
64
+ export interface PlannedSkipLike {
65
+ readonly name: string;
66
+ readonly why: string;
67
+ }
68
+ /** The ` name…padEnd(32) (fate)` listing every confirm prints. */
69
+ export declare function listPlannedFiles(say: (text: string) => void, files: readonly PlannedFileLike[], skipped: readonly PlannedSkipLike[], fate: (file: PlannedFileLike) => string): void;
70
+ /** The one write mechanic: mkdir, write, chmod — in that order, everywhere. */
71
+ export declare function writePlannedFile(file: PlannedFileLike): void;
72
+ //# sourceMappingURL=wizard.d.ts.map