@smeltjs/core 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/apply.d.ts.map +1 -1
  2. package/dist/apply.js +8 -2
  3. package/dist/apply.js.map +1 -1
  4. package/dist/cli/init.d.ts.map +1 -1
  5. package/dist/cli/init.js +4 -2
  6. package/dist/cli/init.js.map +1 -1
  7. package/dist/cli/report.d.ts +10 -1
  8. package/dist/cli/report.d.ts.map +1 -1
  9. package/dist/cli/report.js +16 -1
  10. package/dist/cli/report.js.map +1 -1
  11. package/dist/cli/subcommands/flags.d.ts +3 -0
  12. package/dist/cli/subcommands/flags.d.ts.map +1 -1
  13. package/dist/cli/subcommands/flags.js +16 -3
  14. package/dist/cli/subcommands/flags.js.map +1 -1
  15. package/dist/cli/subcommands/smelt.d.ts +3 -0
  16. package/dist/cli/subcommands/smelt.d.ts.map +1 -1
  17. package/dist/cli/subcommands/smelt.js +5 -1
  18. package/dist/cli/subcommands/smelt.js.map +1 -1
  19. package/dist/cli/subcommands/stats.d.ts +8 -3
  20. package/dist/cli/subcommands/stats.d.ts.map +1 -1
  21. package/dist/cli/subcommands/stats.js +15 -5
  22. package/dist/cli/subcommands/stats.js.map +1 -1
  23. package/dist/errors.d.ts +10 -0
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +10 -0
  26. package/dist/errors.js.map +1 -1
  27. package/dist/hooks/focus-terms.d.ts +57 -0
  28. package/dist/hooks/focus-terms.d.ts.map +1 -0
  29. package/dist/hooks/focus-terms.js +230 -0
  30. package/dist/hooks/focus-terms.js.map +1 -0
  31. package/dist/hooks/guard-core.d.ts +8 -16
  32. package/dist/hooks/guard-core.d.ts.map +1 -1
  33. package/dist/hooks/guard-core.js +22 -120
  34. package/dist/hooks/guard-core.js.map +1 -1
  35. package/dist/index.d.ts +4 -4
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +3 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/ops/index.d.ts +3 -3
  40. package/dist/ops/index.d.ts.map +1 -1
  41. package/dist/ops/index.js +2 -2
  42. package/dist/ops/index.js.map +1 -1
  43. package/dist/ops/verbs.d.ts +57 -1
  44. package/dist/ops/verbs.d.ts.map +1 -1
  45. package/dist/ops/verbs.js +49 -1
  46. package/dist/ops/verbs.js.map +1 -1
  47. package/dist/plan/auto.d.ts +9 -2
  48. package/dist/plan/auto.d.ts.map +1 -1
  49. package/dist/plan/auto.js +14 -2
  50. package/dist/plan/auto.js.map +1 -1
  51. package/dist/plan/diff.d.ts +45 -0
  52. package/dist/plan/diff.d.ts.map +1 -0
  53. package/dist/plan/diff.js +284 -0
  54. package/dist/plan/diff.js.map +1 -0
  55. package/dist/plan/json.d.ts +37 -0
  56. package/dist/plan/json.d.ts.map +1 -0
  57. package/dist/plan/json.js +181 -0
  58. package/dist/plan/json.js.map +1 -0
  59. package/dist/plan/kind.d.ts +28 -0
  60. package/dist/plan/kind.d.ts.map +1 -0
  61. package/dist/plan/kind.js +57 -0
  62. package/dist/plan/kind.js.map +1 -0
  63. package/dist/plan/offsets.d.ts +8 -0
  64. package/dist/plan/offsets.d.ts.map +1 -0
  65. package/dist/plan/offsets.js +19 -0
  66. package/dist/plan/offsets.js.map +1 -0
  67. package/dist/plan/planners.d.ts +15 -5
  68. package/dist/plan/planners.d.ts.map +1 -1
  69. package/dist/plan/planners.js +13 -5
  70. package/dist/plan/planners.js.map +1 -1
  71. package/dist/plan/structural.d.ts.map +1 -1
  72. package/dist/plan/structural.js +106 -14
  73. package/dist/plan/structural.js.map +1 -1
  74. package/dist/retrieve.d.ts +22 -1
  75. package/dist/retrieve.d.ts.map +1 -1
  76. package/dist/retrieve.js +59 -0
  77. package/dist/retrieve.js.map +1 -1
  78. package/dist/smelter.d.ts +4 -0
  79. package/dist/smelter.d.ts.map +1 -1
  80. package/dist/smelter.js +4 -0
  81. package/dist/smelter.js.map +1 -1
  82. package/dist/stats.d.ts +16 -1
  83. package/dist/stats.d.ts.map +1 -1
  84. package/dist/stats.js +29 -0
  85. package/dist/stats.js.map +1 -1
  86. package/dist/store-dir.d.ts +9 -2
  87. package/dist/store-dir.d.ts.map +1 -1
  88. package/dist/store-dir.js +48 -7
  89. package/dist/store-dir.js.map +1 -1
  90. package/dist/store.d.ts +4 -2
  91. package/dist/store.d.ts.map +1 -1
  92. package/dist/store.js +12 -2
  93. package/dist/store.js.map +1 -1
  94. package/dist/types.d.ts +116 -2
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/types.js.map +1 -1
  97. package/package.json +1 -1
@@ -1,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"]}
package/dist/errors.d.ts CHANGED
@@ -105,4 +105,14 @@ export declare class RepoMapIoError extends SmeltError {
105
105
  readonly name = "RepoMapIoError";
106
106
  constructor(operation: string, path: string, cause: unknown);
107
107
  }
108
+ /**
109
+ * A content-kind planner (`json`, `diff`) was asked to plan text that is not that kind.
110
+ * It refuses rather than approximating, for the reason the structural planner refuses
111
+ * an unknown language: output labelled `json/v1` that is really line windows would be
112
+ * undetectable from outside. `auto` never raises this — it probes the kind first and
113
+ * routes only what the probe proved.
114
+ */
115
+ export declare class ContentKindError extends SmeltError {
116
+ readonly name = "ContentKindError";
117
+ }
108
118
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAkB,IAAI,EAAE,MAAM,CAAgB;CAC/C;AAED;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,UAAU;IACjD,SAAkB,IAAI,yBAAyB;IAE/C,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAKxC;CACF;AAED;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,UAAU;IAChD,SAAkB,IAAI,wBAAwB;IAE9C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACF;AAED,0FAA0F;AAC1F,qBAAa,uBAAwB,SAAQ,UAAU;IACrD,SAAkB,IAAI,6BAA6B;CACpD;AAED,mEAAmE;AACnE,qBAAa,qBAAsB,SAAQ,UAAU;IACnD,SAAkB,IAAI,2BAA2B;CAClD;AAED,uEAAuE;AACvE,qBAAa,uBAAwB,SAAQ,UAAU;IACrD,SAAkB,IAAI,6BAA6B;CACpD;AAED,uEAAuE;AACvE,qBAAa,gBAAiB,SAAQ,UAAU;IAC9C,SAAkB,IAAI,sBAAsB;IAE5C,YAAY,IAAI,EAAE,MAAM,EAIvB;CACF;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,UAAU;IACvD,SAAkB,IAAI,+BAA+B;IAErD,YAAY,SAAS,EAAE,MAAM,EAO5B;CACF;AAED;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,UAAU;IAC3C,SAAkB,IAAI,mBAAmB;CAC1C;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,UAAU;IAChD,SAAkB,IAAI,wBAAwB;IAE9C,YAAY,IAAI,EAAE,MAAM,EAKvB;CACF;AAED;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,UAAU;IAClD,SAAkB,IAAI,0BAA0B;IAEhD,YAAY,IAAI,EAAE,MAAM,EAMvB;CACF;AAED;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,UAAU;IAC9C,SAAkB,IAAI,sBAAsB;CAC7C;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,cAAe,SAAQ,UAAU;IAC5C,SAAkB,IAAI,oBAAoB;IAE1C,YAAY,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAM1D;CACF","sourcesContent":["/**\n * Every error smelt throws is one of these. Consumers can `instanceof SmeltError`\n * to tell \"the library said no\" apart from \"something else blew up\".\n */\nexport class SmeltError extends Error {\n override readonly name: string = 'SmeltError';\n}\n\n/**\n * A scaffold stub. It throws instead of returning a plausible-looking wrong answer,\n * because a context optimizer that quietly returns bad output is indistinguishable\n * from one that works. See CONTRIBUTING.md § \"Silence is the enemy\".\n */\nexport class NotImplementedError extends SmeltError {\n override readonly name = 'NotImplementedError';\n\n constructor(what: string, seeAlso: string) {\n super(\n `smelt: ${what} is not implemented yet. This is a scaffold stub — it throws rather ` +\n `than returning a plausible wrong answer. See ${seeAlso}.`,\n );\n }\n}\n\n/**\n * Thrown when something in the elision path tried to reach a non-local resource.\n * v1 has no network. See docs/ARCHITECTURE.md § \"Law 1 — zero network\".\n */\nexport class NetworkPolicyError extends SmeltError {\n override readonly name = 'NetworkPolicyError';\n\n constructor(message: string) {\n super(`smelt: ${message}`);\n }\n}\n\n/** A plan asked to elide two overlapping ranges. Applying it would corrupt the output. */\nexport class OverlappingElisionError extends SmeltError {\n override readonly name = 'OverlappingElisionError';\n}\n\n/** A plan referenced a byte range that is not inside the input. */\nexport class RangeOutOfBoundsError extends SmeltError {\n override readonly name = 'RangeOutOfBoundsError';\n}\n\n/** A grammar was requested that is not installed or not registered. */\nexport class GrammarUnavailableError extends SmeltError {\n override readonly name = 'GrammarUnavailableError';\n}\n\n/** `retrieve(hash)` was called with a hash the store does not hold. */\nexport class UnknownHashError extends SmeltError {\n override readonly name = 'UnknownHashError';\n\n constructor(hash: string) {\n super(\n `smelt: no stored content for hash \"${hash}\". It was never elided, or the store was reset.`,\n );\n }\n}\n\n/**\n * A planner was handed a `PlanInput` without `pricing`. The type makes `pricing`\n * required, so TypeScript callers cannot get here; a JS caller can, and the honest\n * answer is this error rather than a guessed marker cost — a planner pricing markers\n * itself is exactly the inversion the MarkerPricing seam removed.\n */\nexport class MissingMarkerPricingError extends SmeltError {\n override readonly name = 'MissingMarkerPricingError';\n\n constructor(plannerId: string) {\n super(\n `smelt: ${plannerId} was handed a PlanInput without \\`pricing\\`. A planner never ` +\n `guesses what a marker costs — the applier renders markers, so the applier ` +\n `prices them. Build one with markerPricing(language, marker) from apply.ts and ` +\n `put it on the input; createSmelter and the CLI construct it centrally.`,\n );\n }\n}\n\n/**\n * The CLI was invoked wrongly — a missing `--budget`, an unknown flag, a budget that\n * is not a number. Distinct from every other `SmeltError` so the CLI can exit with a\n * usage code rather than pretending the library refused.\n */\nexport class CliUsageError extends SmeltError {\n override readonly name = 'CliUsageError';\n}\n\n/**\n * Two different blobs hashed to the same key. Astronomically unlikely, and yet: the\n * alternative to throwing is handing the model the wrong bytes and calling it a\n * retrieval, which is precisely the silent failure this library exists to avoid.\n */\nexport class HashCollisionError extends SmeltError {\n override readonly name = 'HashCollisionError';\n\n constructor(hash: string) {\n super(\n `smelt: hash collision on \"${hash}\" — two different blobs share a key. Refusing to ` +\n `store, because retrieving would return the wrong bytes. Please report this.`,\n );\n }\n}\n\n/**\n * A persistent store holds bytes under this hash, but they no longer hash to it — a\n * torn write, a truncation, an edit behind the store's back. Deliberately distinct from\n * {@link UnknownHashError}: \"we hold damaged bytes\" and \"never existed\" call for\n * different responses, and returning the damaged bytes as a retrieval would be the\n * silent wrong answer this library exists to refuse.\n */\nexport class StoreCorruptionError extends SmeltError {\n override readonly name = 'StoreCorruptionError';\n\n constructor(hash: string) {\n super(\n `smelt: the bytes stored under hash \"${hash}\" do not hash to \"${hash}\". Refusing ` +\n `to return them — they are damaged, not merely unknown. The store directory was ` +\n `truncated or edited outside smelt.`,\n );\n }\n}\n\n/**\n * A directory offered as a persistent store carries a format marker this version of\n * smelt does not understand — or no parseable marker at all. Refusing beats guessing:\n * reinterpreting an unknown layout could hand back the wrong bytes with no error.\n */\nexport class StoreFormatError extends SmeltError {\n override readonly name = 'StoreFormatError';\n}\n\n/**\n * A filesystem call the repo map made failed: a root that is not there, a directory\n * that cannot be listed, a file that cannot be read, a cache entry that cannot be\n * written.\n *\n * It exists because of a hole in the one promise the consumer contract makes about\n * errors — **every error smelt throws is an `instanceof SmeltError`**.\n * `buildRepoMap({ root: '/nonexistent' })` used to throw the raw `ENOENT` that\n * `readdirSync` raises, so a caller doing precisely what the documentation says still\n * had a bare `Error` escaping past its `catch`. The guarantee is worth nothing with an\n * exception to it, so every `node:fs` call under `src/repomap/` now arrives here.\n *\n * The message names the path, because \"ENOENT\" without one is a bug report nobody can\n * act on, and the original error is kept as `cause`: wrapping is meant to bring the\n * failure inside the contract, never to hide what Node said.\n */\nexport class RepoMapIoError extends SmeltError {\n override readonly name = 'RepoMapIoError';\n\n constructor(operation: string, path: string, cause: unknown) {\n super(\n `smelt: could not ${operation} \"${path}\" while building the repo map: ` +\n `${describeIoCause(cause)}.`,\n { cause },\n );\n }\n}\n\n/** The `errno` code when Node supplied one, its message otherwise. Never invented. */\nfunction describeIoCause(cause: unknown): string {\n const code = (cause as { code?: string } | null | undefined)?.code;\n if (typeof code === 'string' && code !== '') return code;\n if (cause instanceof Error && cause.message !== '') return cause.message;\n return String(cause);\n}\n"]}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAkB,IAAI,EAAE,MAAM,CAAgB;CAC/C;AAED;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,UAAU;IACjD,SAAkB,IAAI,yBAAyB;IAE/C,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAKxC;CACF;AAED;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,UAAU;IAChD,SAAkB,IAAI,wBAAwB;IAE9C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACF;AAED,0FAA0F;AAC1F,qBAAa,uBAAwB,SAAQ,UAAU;IACrD,SAAkB,IAAI,6BAA6B;CACpD;AAED,mEAAmE;AACnE,qBAAa,qBAAsB,SAAQ,UAAU;IACnD,SAAkB,IAAI,2BAA2B;CAClD;AAED,uEAAuE;AACvE,qBAAa,uBAAwB,SAAQ,UAAU;IACrD,SAAkB,IAAI,6BAA6B;CACpD;AAED,uEAAuE;AACvE,qBAAa,gBAAiB,SAAQ,UAAU;IAC9C,SAAkB,IAAI,sBAAsB;IAE5C,YAAY,IAAI,EAAE,MAAM,EAIvB;CACF;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,UAAU;IACvD,SAAkB,IAAI,+BAA+B;IAErD,YAAY,SAAS,EAAE,MAAM,EAO5B;CACF;AAED;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,UAAU;IAC3C,SAAkB,IAAI,mBAAmB;CAC1C;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,UAAU;IAChD,SAAkB,IAAI,wBAAwB;IAE9C,YAAY,IAAI,EAAE,MAAM,EAKvB;CACF;AAED;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,UAAU;IAClD,SAAkB,IAAI,0BAA0B;IAEhD,YAAY,IAAI,EAAE,MAAM,EAMvB;CACF;AAED;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,UAAU;IAC9C,SAAkB,IAAI,sBAAsB;CAC7C;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,cAAe,SAAQ,UAAU;IAC5C,SAAkB,IAAI,oBAAoB;IAE1C,YAAY,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAM1D;CACF;AAUD;;;;;;GAMG;AACH,qBAAa,gBAAiB,SAAQ,UAAU;IAC9C,SAAkB,IAAI,sBAAsB;CAC7C","sourcesContent":["/**\n * Every error smelt throws is one of these. Consumers can `instanceof SmeltError`\n * to tell \"the library said no\" apart from \"something else blew up\".\n */\nexport class SmeltError extends Error {\n override readonly name: string = 'SmeltError';\n}\n\n/**\n * A scaffold stub. It throws instead of returning a plausible-looking wrong answer,\n * because a context optimizer that quietly returns bad output is indistinguishable\n * from one that works. See CONTRIBUTING.md § \"Silence is the enemy\".\n */\nexport class NotImplementedError extends SmeltError {\n override readonly name = 'NotImplementedError';\n\n constructor(what: string, seeAlso: string) {\n super(\n `smelt: ${what} is not implemented yet. This is a scaffold stub — it throws rather ` +\n `than returning a plausible wrong answer. See ${seeAlso}.`,\n );\n }\n}\n\n/**\n * Thrown when something in the elision path tried to reach a non-local resource.\n * v1 has no network. See docs/ARCHITECTURE.md § \"Law 1 — zero network\".\n */\nexport class NetworkPolicyError extends SmeltError {\n override readonly name = 'NetworkPolicyError';\n\n constructor(message: string) {\n super(`smelt: ${message}`);\n }\n}\n\n/** A plan asked to elide two overlapping ranges. Applying it would corrupt the output. */\nexport class OverlappingElisionError extends SmeltError {\n override readonly name = 'OverlappingElisionError';\n}\n\n/** A plan referenced a byte range that is not inside the input. */\nexport class RangeOutOfBoundsError extends SmeltError {\n override readonly name = 'RangeOutOfBoundsError';\n}\n\n/** A grammar was requested that is not installed or not registered. */\nexport class GrammarUnavailableError extends SmeltError {\n override readonly name = 'GrammarUnavailableError';\n}\n\n/** `retrieve(hash)` was called with a hash the store does not hold. */\nexport class UnknownHashError extends SmeltError {\n override readonly name = 'UnknownHashError';\n\n constructor(hash: string) {\n super(\n `smelt: no stored content for hash \"${hash}\". It was never elided, or the store was reset.`,\n );\n }\n}\n\n/**\n * A planner was handed a `PlanInput` without `pricing`. The type makes `pricing`\n * required, so TypeScript callers cannot get here; a JS caller can, and the honest\n * answer is this error rather than a guessed marker cost — a planner pricing markers\n * itself is exactly the inversion the MarkerPricing seam removed.\n */\nexport class MissingMarkerPricingError extends SmeltError {\n override readonly name = 'MissingMarkerPricingError';\n\n constructor(plannerId: string) {\n super(\n `smelt: ${plannerId} was handed a PlanInput without \\`pricing\\`. A planner never ` +\n `guesses what a marker costs — the applier renders markers, so the applier ` +\n `prices them. Build one with markerPricing(language, marker) from apply.ts and ` +\n `put it on the input; createSmelter and the CLI construct it centrally.`,\n );\n }\n}\n\n/**\n * The CLI was invoked wrongly — a missing `--budget`, an unknown flag, a budget that\n * is not a number. Distinct from every other `SmeltError` so the CLI can exit with a\n * usage code rather than pretending the library refused.\n */\nexport class CliUsageError extends SmeltError {\n override readonly name = 'CliUsageError';\n}\n\n/**\n * Two different blobs hashed to the same key. Astronomically unlikely, and yet: the\n * alternative to throwing is handing the model the wrong bytes and calling it a\n * retrieval, which is precisely the silent failure this library exists to avoid.\n */\nexport class HashCollisionError extends SmeltError {\n override readonly name = 'HashCollisionError';\n\n constructor(hash: string) {\n super(\n `smelt: hash collision on \"${hash}\" — two different blobs share a key. Refusing to ` +\n `store, because retrieving would return the wrong bytes. Please report this.`,\n );\n }\n}\n\n/**\n * A persistent store holds bytes under this hash, but they no longer hash to it — a\n * torn write, a truncation, an edit behind the store's back. Deliberately distinct from\n * {@link UnknownHashError}: \"we hold damaged bytes\" and \"never existed\" call for\n * different responses, and returning the damaged bytes as a retrieval would be the\n * silent wrong answer this library exists to refuse.\n */\nexport class StoreCorruptionError extends SmeltError {\n override readonly name = 'StoreCorruptionError';\n\n constructor(hash: string) {\n super(\n `smelt: the bytes stored under hash \"${hash}\" do not hash to \"${hash}\". Refusing ` +\n `to return them — they are damaged, not merely unknown. The store directory was ` +\n `truncated or edited outside smelt.`,\n );\n }\n}\n\n/**\n * A directory offered as a persistent store carries a format marker this version of\n * smelt does not understand — or no parseable marker at all. Refusing beats guessing:\n * reinterpreting an unknown layout could hand back the wrong bytes with no error.\n */\nexport class StoreFormatError extends SmeltError {\n override readonly name = 'StoreFormatError';\n}\n\n/**\n * A filesystem call the repo map made failed: a root that is not there, a directory\n * that cannot be listed, a file that cannot be read, a cache entry that cannot be\n * written.\n *\n * It exists because of a hole in the one promise the consumer contract makes about\n * errors — **every error smelt throws is an `instanceof SmeltError`**.\n * `buildRepoMap({ root: '/nonexistent' })` used to throw the raw `ENOENT` that\n * `readdirSync` raises, so a caller doing precisely what the documentation says still\n * had a bare `Error` escaping past its `catch`. The guarantee is worth nothing with an\n * exception to it, so every `node:fs` call under `src/repomap/` now arrives here.\n *\n * The message names the path, because \"ENOENT\" without one is a bug report nobody can\n * act on, and the original error is kept as `cause`: wrapping is meant to bring the\n * failure inside the contract, never to hide what Node said.\n */\nexport class RepoMapIoError extends SmeltError {\n override readonly name = 'RepoMapIoError';\n\n constructor(operation: string, path: string, cause: unknown) {\n super(\n `smelt: could not ${operation} \"${path}\" while building the repo map: ` +\n `${describeIoCause(cause)}.`,\n { cause },\n );\n }\n}\n\n/** The `errno` code when Node supplied one, its message otherwise. Never invented. */\nfunction describeIoCause(cause: unknown): string {\n const code = (cause as { code?: string } | null | undefined)?.code;\n if (typeof code === 'string' && code !== '') return code;\n if (cause instanceof Error && cause.message !== '') return cause.message;\n return String(cause);\n}\n\n/**\n * A content-kind planner (`json`, `diff`) was asked to plan text that is not that kind.\n * It refuses rather than approximating, for the reason the structural planner refuses\n * an unknown language: output labelled `json/v1` that is really line windows would be\n * undetectable from outside. `auto` never raises this — it probes the kind first and\n * routes only what the probe proved.\n */\nexport class ContentKindError extends SmeltError {\n override readonly name = 'ContentKindError';\n}\n"]}
package/dist/errors.js CHANGED
@@ -136,4 +136,14 @@ function describeIoCause(cause) {
136
136
  return cause.message;
137
137
  return String(cause);
138
138
  }
139
+ /**
140
+ * A content-kind planner (`json`, `diff`) was asked to plan text that is not that kind.
141
+ * It refuses rather than approximating, for the reason the structural planner refuses
142
+ * an unknown language: output labelled `json/v1` that is really line windows would be
143
+ * undetectable from outside. `auto` never raises this — it probes the kind first and
144
+ * routes only what the probe proved.
145
+ */
146
+ export class ContentKindError extends SmeltError {
147
+ name = 'ContentKindError';
148
+ }
139
149
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every error smelt throws is one of these. Consumers can `instanceof SmeltError`\n * to tell \"the library said no\" apart from \"something else blew up\".\n */\nexport class SmeltError extends Error {\n override readonly name: string = 'SmeltError';\n}\n\n/**\n * A scaffold stub. It throws instead of returning a plausible-looking wrong answer,\n * because a context optimizer that quietly returns bad output is indistinguishable\n * from one that works. See CONTRIBUTING.md § \"Silence is the enemy\".\n */\nexport class NotImplementedError extends SmeltError {\n override readonly name = 'NotImplementedError';\n\n constructor(what: string, seeAlso: string) {\n super(\n `smelt: ${what} is not implemented yet. This is a scaffold stub — it throws rather ` +\n `than returning a plausible wrong answer. See ${seeAlso}.`,\n );\n }\n}\n\n/**\n * Thrown when something in the elision path tried to reach a non-local resource.\n * v1 has no network. See docs/ARCHITECTURE.md § \"Law 1 — zero network\".\n */\nexport class NetworkPolicyError extends SmeltError {\n override readonly name = 'NetworkPolicyError';\n\n constructor(message: string) {\n super(`smelt: ${message}`);\n }\n}\n\n/** A plan asked to elide two overlapping ranges. Applying it would corrupt the output. */\nexport class OverlappingElisionError extends SmeltError {\n override readonly name = 'OverlappingElisionError';\n}\n\n/** A plan referenced a byte range that is not inside the input. */\nexport class RangeOutOfBoundsError extends SmeltError {\n override readonly name = 'RangeOutOfBoundsError';\n}\n\n/** A grammar was requested that is not installed or not registered. */\nexport class GrammarUnavailableError extends SmeltError {\n override readonly name = 'GrammarUnavailableError';\n}\n\n/** `retrieve(hash)` was called with a hash the store does not hold. */\nexport class UnknownHashError extends SmeltError {\n override readonly name = 'UnknownHashError';\n\n constructor(hash: string) {\n super(\n `smelt: no stored content for hash \"${hash}\". It was never elided, or the store was reset.`,\n );\n }\n}\n\n/**\n * A planner was handed a `PlanInput` without `pricing`. The type makes `pricing`\n * required, so TypeScript callers cannot get here; a JS caller can, and the honest\n * answer is this error rather than a guessed marker cost — a planner pricing markers\n * itself is exactly the inversion the MarkerPricing seam removed.\n */\nexport class MissingMarkerPricingError extends SmeltError {\n override readonly name = 'MissingMarkerPricingError';\n\n constructor(plannerId: string) {\n super(\n `smelt: ${plannerId} was handed a PlanInput without \\`pricing\\`. A planner never ` +\n `guesses what a marker costs — the applier renders markers, so the applier ` +\n `prices them. Build one with markerPricing(language, marker) from apply.ts and ` +\n `put it on the input; createSmelter and the CLI construct it centrally.`,\n );\n }\n}\n\n/**\n * The CLI was invoked wrongly — a missing `--budget`, an unknown flag, a budget that\n * is not a number. Distinct from every other `SmeltError` so the CLI can exit with a\n * usage code rather than pretending the library refused.\n */\nexport class CliUsageError extends SmeltError {\n override readonly name = 'CliUsageError';\n}\n\n/**\n * Two different blobs hashed to the same key. Astronomically unlikely, and yet: the\n * alternative to throwing is handing the model the wrong bytes and calling it a\n * retrieval, which is precisely the silent failure this library exists to avoid.\n */\nexport class HashCollisionError extends SmeltError {\n override readonly name = 'HashCollisionError';\n\n constructor(hash: string) {\n super(\n `smelt: hash collision on \"${hash}\" — two different blobs share a key. Refusing to ` +\n `store, because retrieving would return the wrong bytes. Please report this.`,\n );\n }\n}\n\n/**\n * A persistent store holds bytes under this hash, but they no longer hash to it — a\n * torn write, a truncation, an edit behind the store's back. Deliberately distinct from\n * {@link UnknownHashError}: \"we hold damaged bytes\" and \"never existed\" call for\n * different responses, and returning the damaged bytes as a retrieval would be the\n * silent wrong answer this library exists to refuse.\n */\nexport class StoreCorruptionError extends SmeltError {\n override readonly name = 'StoreCorruptionError';\n\n constructor(hash: string) {\n super(\n `smelt: the bytes stored under hash \"${hash}\" do not hash to \"${hash}\". Refusing ` +\n `to return them — they are damaged, not merely unknown. The store directory was ` +\n `truncated or edited outside smelt.`,\n );\n }\n}\n\n/**\n * A directory offered as a persistent store carries a format marker this version of\n * smelt does not understand — or no parseable marker at all. Refusing beats guessing:\n * reinterpreting an unknown layout could hand back the wrong bytes with no error.\n */\nexport class StoreFormatError extends SmeltError {\n override readonly name = 'StoreFormatError';\n}\n\n/**\n * A filesystem call the repo map made failed: a root that is not there, a directory\n * that cannot be listed, a file that cannot be read, a cache entry that cannot be\n * written.\n *\n * It exists because of a hole in the one promise the consumer contract makes about\n * errors — **every error smelt throws is an `instanceof SmeltError`**.\n * `buildRepoMap({ root: '/nonexistent' })` used to throw the raw `ENOENT` that\n * `readdirSync` raises, so a caller doing precisely what the documentation says still\n * had a bare `Error` escaping past its `catch`. The guarantee is worth nothing with an\n * exception to it, so every `node:fs` call under `src/repomap/` now arrives here.\n *\n * The message names the path, because \"ENOENT\" without one is a bug report nobody can\n * act on, and the original error is kept as `cause`: wrapping is meant to bring the\n * failure inside the contract, never to hide what Node said.\n */\nexport class RepoMapIoError extends SmeltError {\n override readonly name = 'RepoMapIoError';\n\n constructor(operation: string, path: string, cause: unknown) {\n super(\n `smelt: could not ${operation} \"${path}\" while building the repo map: ` +\n `${describeIoCause(cause)}.`,\n { cause },\n );\n }\n}\n\n/** The `errno` code when Node supplied one, its message otherwise. Never invented. */\nfunction describeIoCause(cause: unknown): string {\n const code = (cause as { code?: string } | null | undefined)?.code;\n if (typeof code === 'string' && code !== '') return code;\n if (cause instanceof Error && cause.message !== '') return cause.message;\n return String(cause);\n}\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every error smelt throws is one of these. Consumers can `instanceof SmeltError`\n * to tell \"the library said no\" apart from \"something else blew up\".\n */\nexport class SmeltError extends Error {\n override readonly name: string = 'SmeltError';\n}\n\n/**\n * A scaffold stub. It throws instead of returning a plausible-looking wrong answer,\n * because a context optimizer that quietly returns bad output is indistinguishable\n * from one that works. See CONTRIBUTING.md § \"Silence is the enemy\".\n */\nexport class NotImplementedError extends SmeltError {\n override readonly name = 'NotImplementedError';\n\n constructor(what: string, seeAlso: string) {\n super(\n `smelt: ${what} is not implemented yet. This is a scaffold stub — it throws rather ` +\n `than returning a plausible wrong answer. See ${seeAlso}.`,\n );\n }\n}\n\n/**\n * Thrown when something in the elision path tried to reach a non-local resource.\n * v1 has no network. See docs/ARCHITECTURE.md § \"Law 1 — zero network\".\n */\nexport class NetworkPolicyError extends SmeltError {\n override readonly name = 'NetworkPolicyError';\n\n constructor(message: string) {\n super(`smelt: ${message}`);\n }\n}\n\n/** A plan asked to elide two overlapping ranges. Applying it would corrupt the output. */\nexport class OverlappingElisionError extends SmeltError {\n override readonly name = 'OverlappingElisionError';\n}\n\n/** A plan referenced a byte range that is not inside the input. */\nexport class RangeOutOfBoundsError extends SmeltError {\n override readonly name = 'RangeOutOfBoundsError';\n}\n\n/** A grammar was requested that is not installed or not registered. */\nexport class GrammarUnavailableError extends SmeltError {\n override readonly name = 'GrammarUnavailableError';\n}\n\n/** `retrieve(hash)` was called with a hash the store does not hold. */\nexport class UnknownHashError extends SmeltError {\n override readonly name = 'UnknownHashError';\n\n constructor(hash: string) {\n super(\n `smelt: no stored content for hash \"${hash}\". It was never elided, or the store was reset.`,\n );\n }\n}\n\n/**\n * A planner was handed a `PlanInput` without `pricing`. The type makes `pricing`\n * required, so TypeScript callers cannot get here; a JS caller can, and the honest\n * answer is this error rather than a guessed marker cost — a planner pricing markers\n * itself is exactly the inversion the MarkerPricing seam removed.\n */\nexport class MissingMarkerPricingError extends SmeltError {\n override readonly name = 'MissingMarkerPricingError';\n\n constructor(plannerId: string) {\n super(\n `smelt: ${plannerId} was handed a PlanInput without \\`pricing\\`. A planner never ` +\n `guesses what a marker costs — the applier renders markers, so the applier ` +\n `prices them. Build one with markerPricing(language, marker) from apply.ts and ` +\n `put it on the input; createSmelter and the CLI construct it centrally.`,\n );\n }\n}\n\n/**\n * The CLI was invoked wrongly — a missing `--budget`, an unknown flag, a budget that\n * is not a number. Distinct from every other `SmeltError` so the CLI can exit with a\n * usage code rather than pretending the library refused.\n */\nexport class CliUsageError extends SmeltError {\n override readonly name = 'CliUsageError';\n}\n\n/**\n * Two different blobs hashed to the same key. Astronomically unlikely, and yet: the\n * alternative to throwing is handing the model the wrong bytes and calling it a\n * retrieval, which is precisely the silent failure this library exists to avoid.\n */\nexport class HashCollisionError extends SmeltError {\n override readonly name = 'HashCollisionError';\n\n constructor(hash: string) {\n super(\n `smelt: hash collision on \"${hash}\" — two different blobs share a key. Refusing to ` +\n `store, because retrieving would return the wrong bytes. Please report this.`,\n );\n }\n}\n\n/**\n * A persistent store holds bytes under this hash, but they no longer hash to it — a\n * torn write, a truncation, an edit behind the store's back. Deliberately distinct from\n * {@link UnknownHashError}: \"we hold damaged bytes\" and \"never existed\" call for\n * different responses, and returning the damaged bytes as a retrieval would be the\n * silent wrong answer this library exists to refuse.\n */\nexport class StoreCorruptionError extends SmeltError {\n override readonly name = 'StoreCorruptionError';\n\n constructor(hash: string) {\n super(\n `smelt: the bytes stored under hash \"${hash}\" do not hash to \"${hash}\". Refusing ` +\n `to return them — they are damaged, not merely unknown. The store directory was ` +\n `truncated or edited outside smelt.`,\n );\n }\n}\n\n/**\n * A directory offered as a persistent store carries a format marker this version of\n * smelt does not understand — or no parseable marker at all. Refusing beats guessing:\n * reinterpreting an unknown layout could hand back the wrong bytes with no error.\n */\nexport class StoreFormatError extends SmeltError {\n override readonly name = 'StoreFormatError';\n}\n\n/**\n * A filesystem call the repo map made failed: a root that is not there, a directory\n * that cannot be listed, a file that cannot be read, a cache entry that cannot be\n * written.\n *\n * It exists because of a hole in the one promise the consumer contract makes about\n * errors — **every error smelt throws is an `instanceof SmeltError`**.\n * `buildRepoMap({ root: '/nonexistent' })` used to throw the raw `ENOENT` that\n * `readdirSync` raises, so a caller doing precisely what the documentation says still\n * had a bare `Error` escaping past its `catch`. The guarantee is worth nothing with an\n * exception to it, so every `node:fs` call under `src/repomap/` now arrives here.\n *\n * The message names the path, because \"ENOENT\" without one is a bug report nobody can\n * act on, and the original error is kept as `cause`: wrapping is meant to bring the\n * failure inside the contract, never to hide what Node said.\n */\nexport class RepoMapIoError extends SmeltError {\n override readonly name = 'RepoMapIoError';\n\n constructor(operation: string, path: string, cause: unknown) {\n super(\n `smelt: could not ${operation} \"${path}\" while building the repo map: ` +\n `${describeIoCause(cause)}.`,\n { cause },\n );\n }\n}\n\n/** The `errno` code when Node supplied one, its message otherwise. Never invented. */\nfunction describeIoCause(cause: unknown): string {\n const code = (cause as { code?: string } | null | undefined)?.code;\n if (typeof code === 'string' && code !== '') return code;\n if (cause instanceof Error && cause.message !== '') return cause.message;\n return String(cause);\n}\n\n/**\n * A content-kind planner (`json`, `diff`) was asked to plan text that is not that kind.\n * It refuses rather than approximating, for the reason the structural planner refuses\n * an unknown language: output labelled `json/v1` that is really line windows would be\n * undetectable from outside. `auto` never raises this — it probes the kind first and\n * routes only what the probe proved.\n */\nexport class ContentKindError extends SmeltError {\n override readonly name = 'ContentKindError';\n}\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Focus terms, derived from the command that produced a blob — one derivation, zero
3
+ * imports, shared by the hooks guard and the ops seam.
4
+ *
5
+ * The guard is the producer expert: to decide anything about a `grep` it has already
6
+ * parsed the pattern out of the command. Before this file that knowledge died inside
7
+ * the guard — the deny reason printed `--focus <?>` and the model reinvented what the
8
+ * guard knew. Now the derivation lives here, beside the guard and importing nothing
9
+ * (the guard's latency budget is a stat and an exit, so this must stay a
10
+ * zero-dependency sibling rather than an exception to its no-library-import rule),
11
+ * and `smeltBlob` applies the same function to a caller's `producer` hint — so the
12
+ * guard's rewrite wrap, `smelt --producer` and the `smelt_file` tool cannot disagree
13
+ * about which terms a command names.
14
+ *
15
+ * The question it answers is narrower than "what did the command search for". It is:
16
+ * **which terms, if any, distinguish the output lines the task is about from the ones
17
+ * it is not.** Focus is what the lexical planner keeps; a term every output line
18
+ * carries keeps everything and cuts nothing, exactly when the output is large. So:
19
+ *
20
+ * - a plain `grep`/`rg` prints only matching lines — every line has the pattern, the
21
+ * pattern distinguishes nothing, and the answer is no terms (the planner's
22
+ * head-and-tail rule is the right cut);
23
+ * - a search with context (`-C`, `-A`, `-B`, `--context`…) prints non-matching lines
24
+ * around each hit, and there the pattern is exactly the focus;
25
+ * - a listing search (`-l`, `-c`, `--files-with-matches`…) prints no matching lines
26
+ * at all, so its pattern names nothing in the output;
27
+ * - a producer that states no term (`cat`, `git diff`, `sed`) yields none, and so
28
+ * does a command this parser cannot see whole — unsure means no terms, the same
29
+ * fail-open rule the guard lives under.
30
+ *
31
+ * Every rule above is a fact about what the command prints, stated as data in
32
+ * {@link SEARCH_PROGRAMS}, {@link CONTEXT_FLAGS} and {@link LISTING_FLAGS} — never a
33
+ * guess about the text.
34
+ */
35
+ /**
36
+ * The terms a producer command names that distinguish output lines the task is about.
37
+ * `[]` whenever the honest answer is "none" — see the module doc. Never throws.
38
+ */
39
+ export declare function focusTermsFor(command: string | undefined): readonly string[];
40
+ /**
41
+ * Split a command into words IF it is one simple command: no pipes, no logic, no
42
+ * redirects, no substitutions, no expansions this code would have to model. Anything
43
+ * else returns `undefined` and the caller treats the command as unknowable — the guard
44
+ * allows, this derivation names no terms.
45
+ */
46
+ export declare function simpleCommandWords(command: string): readonly string[] | undefined;
47
+ /**
48
+ * Every pattern a grep/rg invocation searches for: each explicit `-e`/`--regexp`
49
+ * value if any are given, else the first word that is not a flag or a flag's value.
50
+ * Empty when the parse is not sure.
51
+ */
52
+ export declare function searchPatterns(words: readonly string[]): readonly string[];
53
+ /** The first pattern of {@link searchPatterns}; `undefined` when there is none. */
54
+ export declare function searchPattern(words: readonly string[]): string | undefined;
55
+ /** Single-quote a value for `sh` unless it is plainly safe bare. */
56
+ export declare function shellQuote(value: string): string;
57
+ //# sourceMappingURL=focus-terms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-terms.d.ts","sourceRoot":"","sources":["../../src/hooks/focus-terms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AA2BH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,MAAM,EAAE,CAS5E;AA0BD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAuCjF;AAoCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CA6B1E;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAE1E;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhD","sourcesContent":["/**\n * Focus terms, derived from the command that produced a blob — one derivation, zero\n * imports, shared by the hooks guard and the ops seam.\n *\n * The guard is the producer expert: to decide anything about a `grep` it has already\n * parsed the pattern out of the command. Before this file that knowledge died inside\n * the guard — the deny reason printed `--focus <?>` and the model reinvented what the\n * guard knew. Now the derivation lives here, beside the guard and importing nothing\n * (the guard's latency budget is a stat and an exit, so this must stay a\n * zero-dependency sibling rather than an exception to its no-library-import rule),\n * and `smeltBlob` applies the same function to a caller's `producer` hint — so the\n * guard's rewrite wrap, `smelt --producer` and the `smelt_file` tool cannot disagree\n * about which terms a command names.\n *\n * The question it answers is narrower than \"what did the command search for\". It is:\n * **which terms, if any, distinguish the output lines the task is about from the ones\n * it is not.** Focus is what the lexical planner keeps; a term every output line\n * carries keeps everything and cuts nothing, exactly when the output is large. So:\n *\n * - a plain `grep`/`rg` prints only matching lines — every line has the pattern, the\n * pattern distinguishes nothing, and the answer is no terms (the planner's\n * head-and-tail rule is the right cut);\n * - a search with context (`-C`, `-A`, `-B`, `--context`…) prints non-matching lines\n * around each hit, and there the pattern is exactly the focus;\n * - a listing search (`-l`, `-c`, `--files-with-matches`…) prints no matching lines\n * at all, so its pattern names nothing in the output;\n * - a producer that states no term (`cat`, `git diff`, `sed`) yields none, and so\n * does a command this parser cannot see whole — unsure means no terms, the same\n * fail-open rule the guard lives under.\n *\n * Every rule above is a fact about what the command prints, stated as data in\n * {@link SEARCH_PROGRAMS}, {@link CONTEXT_FLAGS} and {@link LISTING_FLAGS} — never a\n * guess about the text.\n */\n\n/** The search programs whose first non-flag word (or every `-e`) is a pattern. */\nconst SEARCH_PROGRAMS: ReadonlySet<string> = new Set(['grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack']);\n\n/** Flags under which a search prints lines that do *not* match — where focus pays. */\nconst CONTEXT_FLAGS: ReadonlySet<string> = new Set([\n '-A',\n '-B',\n '-C',\n '--after-context',\n '--before-context',\n '--context',\n]);\n\n/** Flags under which a search prints no matching lines at all. */\nconst LISTING_FLAGS: ReadonlySet<string> = new Set([\n '-l',\n '-L',\n '-c',\n '--files-with-matches',\n '--files-without-match',\n '--count',\n '--count-matches',\n '--files',\n]);\n\n/**\n * The terms a producer command names that distinguish output lines the task is about.\n * `[]` whenever the honest answer is \"none\" — see the module doc. Never throws.\n */\nexport function focusTermsFor(command: string | undefined): readonly string[] {\n if (command === undefined) return [];\n const words = simpleCommandWords(command.trim());\n if (words === undefined || words.length === 0) return [];\n const search = searchWords(words);\n if (search === undefined) return [];\n if (!printsContext(search)) return [];\n if (isListing(search)) return [];\n return searchPatterns(search);\n}\n\n/**\n * The words of the search invocation, with `git grep` normalised to `grep`, or\n * `undefined` when the program is not a search at all.\n */\nfunction searchWords(words: readonly string[]): readonly string[] | undefined {\n const program = words[0]!.split('/').at(-1)!;\n if (program === 'git' && words[1] === 'grep') return ['grep', ...words.slice(2)];\n return SEARCH_PROGRAMS.has(program) ? words : undefined;\n}\n\n/** True when a context flag is present — spaced (`-C 3`), compact (`-C3`) or `=`-joined. */\nfunction printsContext(words: readonly string[]): boolean {\n return words.slice(1).some((word) => {\n if (CONTEXT_FLAGS.has(word)) return true;\n const long = word.startsWith('--') ? word.split('=')[0]! : undefined;\n if (long !== undefined) return CONTEXT_FLAGS.has(long);\n return /^-[ABC]\\d+$/.test(word);\n });\n}\n\nfunction isListing(words: readonly string[]): boolean {\n return words.slice(1).some((word) => LISTING_FLAGS.has(word.split('=')[0]!));\n}\n\n/**\n * Split a command into words IF it is one simple command: no pipes, no logic, no\n * redirects, no substitutions, no expansions this code would have to model. Anything\n * else returns `undefined` and the caller treats the command as unknowable — the guard\n * allows, this derivation names no terms.\n */\nexport function simpleCommandWords(command: string): readonly string[] | undefined {\n const words: string[] = [];\n let current = '';\n let started = false;\n let i = 0;\n const push = (): void => {\n if (started) words.push(current);\n current = '';\n started = false;\n };\n while (i < command.length) {\n const ch = command[i]!;\n if ('|&;<>()`$\\\\\\n*?~{}!'.includes(ch)) return undefined; // shell would interpret it\n if (ch === \"'\" || ch === '\"') {\n const quote = ch;\n i += 1;\n started = true;\n while (i < command.length && command[i] !== quote) {\n if (quote === '\"' && (command[i] === '$' || command[i] === '`' || command[i] === '\\\\')) {\n return undefined; // expansions inside double quotes — not simple\n }\n current += command[i]!;\n i += 1;\n }\n if (i >= command.length) return undefined; // unterminated quote\n i += 1;\n continue;\n }\n if (ch === ' ' || ch === '\\t') {\n push();\n i += 1;\n continue;\n }\n current += ch;\n started = true;\n i += 1;\n }\n push();\n return words;\n}\n\n/** Flags that consume the next word, so it is never mistaken for the pattern. */\nconst TAKES_VALUE: ReadonlySet<string> = new Set([\n '-e',\n '--regexp',\n '-f',\n '--file',\n '-m',\n '--max-count',\n '-A',\n '--after-context',\n '-B',\n '--before-context',\n '-C',\n '--context',\n '-d',\n '--directories',\n '-D',\n '--devices',\n '--include',\n '--exclude',\n '--exclude-dir',\n '-t',\n '--type',\n '-T',\n '--type-not',\n '-g',\n '--glob',\n '--iglob',\n '-j',\n '--threads',\n '--color',\n '--colour',\n]);\n\n/**\n * Every pattern a grep/rg invocation searches for: each explicit `-e`/`--regexp`\n * value if any are given, else the first word that is not a flag or a flag's value.\n * Empty when the parse is not sure.\n */\nexport function searchPatterns(words: readonly string[]): readonly string[] {\n const explicit: string[] = [];\n let positional: string | undefined;\n let i = 1;\n while (i < words.length) {\n const word = words[i]!;\n if (word === '--') {\n positional ??= words[i + 1];\n break;\n }\n if (word === '-e' || word === '--regexp') {\n const value = words[i + 1];\n if (value !== undefined) explicit.push(value);\n i += 2;\n continue;\n }\n if (word.startsWith('--') && word.includes('=')) {\n i += 1;\n continue;\n }\n if (word.startsWith('-') && word.length > 1) {\n i += TAKES_VALUE.has(word) ? 2 : 1;\n continue;\n }\n if (positional === undefined && explicit.length === 0) positional = word;\n i += 1;\n }\n if (explicit.length > 0) return explicit;\n return positional === undefined ? [] : [positional];\n}\n\n/** The first pattern of {@link searchPatterns}; `undefined` when there is none. */\nexport function searchPattern(words: readonly string[]): string | undefined {\n return searchPatterns(words)[0];\n}\n\n/** Single-quote a value for `sh` unless it is plainly safe bare. */\nexport function shellQuote(value: string): string {\n if (/^[A-Za-z0-9_./:=-]+$/.test(value)) return value;\n return `'${value.replaceAll(\"'\", `'\"'\"'`)}'`;\n}\n"]}
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Focus terms, derived from the command that produced a blob — one derivation, zero
3
+ * imports, shared by the hooks guard and the ops seam.
4
+ *
5
+ * The guard is the producer expert: to decide anything about a `grep` it has already
6
+ * parsed the pattern out of the command. Before this file that knowledge died inside
7
+ * the guard — the deny reason printed `--focus <?>` and the model reinvented what the
8
+ * guard knew. Now the derivation lives here, beside the guard and importing nothing
9
+ * (the guard's latency budget is a stat and an exit, so this must stay a
10
+ * zero-dependency sibling rather than an exception to its no-library-import rule),
11
+ * and `smeltBlob` applies the same function to a caller's `producer` hint — so the
12
+ * guard's rewrite wrap, `smelt --producer` and the `smelt_file` tool cannot disagree
13
+ * about which terms a command names.
14
+ *
15
+ * The question it answers is narrower than "what did the command search for". It is:
16
+ * **which terms, if any, distinguish the output lines the task is about from the ones
17
+ * it is not.** Focus is what the lexical planner keeps; a term every output line
18
+ * carries keeps everything and cuts nothing, exactly when the output is large. So:
19
+ *
20
+ * - a plain `grep`/`rg` prints only matching lines — every line has the pattern, the
21
+ * pattern distinguishes nothing, and the answer is no terms (the planner's
22
+ * head-and-tail rule is the right cut);
23
+ * - a search with context (`-C`, `-A`, `-B`, `--context`…) prints non-matching lines
24
+ * around each hit, and there the pattern is exactly the focus;
25
+ * - a listing search (`-l`, `-c`, `--files-with-matches`…) prints no matching lines
26
+ * at all, so its pattern names nothing in the output;
27
+ * - a producer that states no term (`cat`, `git diff`, `sed`) yields none, and so
28
+ * does a command this parser cannot see whole — unsure means no terms, the same
29
+ * fail-open rule the guard lives under.
30
+ *
31
+ * Every rule above is a fact about what the command prints, stated as data in
32
+ * {@link SEARCH_PROGRAMS}, {@link CONTEXT_FLAGS} and {@link LISTING_FLAGS} — never a
33
+ * guess about the text.
34
+ */
35
+ /** The search programs whose first non-flag word (or every `-e`) is a pattern. */
36
+ const SEARCH_PROGRAMS = new Set(['grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack']);
37
+ /** Flags under which a search prints lines that do *not* match — where focus pays. */
38
+ const CONTEXT_FLAGS = new Set([
39
+ '-A',
40
+ '-B',
41
+ '-C',
42
+ '--after-context',
43
+ '--before-context',
44
+ '--context',
45
+ ]);
46
+ /** Flags under which a search prints no matching lines at all. */
47
+ const LISTING_FLAGS = new Set([
48
+ '-l',
49
+ '-L',
50
+ '-c',
51
+ '--files-with-matches',
52
+ '--files-without-match',
53
+ '--count',
54
+ '--count-matches',
55
+ '--files',
56
+ ]);
57
+ /**
58
+ * The terms a producer command names that distinguish output lines the task is about.
59
+ * `[]` whenever the honest answer is "none" — see the module doc. Never throws.
60
+ */
61
+ export function focusTermsFor(command) {
62
+ if (command === undefined)
63
+ return [];
64
+ const words = simpleCommandWords(command.trim());
65
+ if (words === undefined || words.length === 0)
66
+ return [];
67
+ const search = searchWords(words);
68
+ if (search === undefined)
69
+ return [];
70
+ if (!printsContext(search))
71
+ return [];
72
+ if (isListing(search))
73
+ return [];
74
+ return searchPatterns(search);
75
+ }
76
+ /**
77
+ * The words of the search invocation, with `git grep` normalised to `grep`, or
78
+ * `undefined` when the program is not a search at all.
79
+ */
80
+ function searchWords(words) {
81
+ const program = words[0].split('/').at(-1);
82
+ if (program === 'git' && words[1] === 'grep')
83
+ return ['grep', ...words.slice(2)];
84
+ return SEARCH_PROGRAMS.has(program) ? words : undefined;
85
+ }
86
+ /** True when a context flag is present — spaced (`-C 3`), compact (`-C3`) or `=`-joined. */
87
+ function printsContext(words) {
88
+ return words.slice(1).some((word) => {
89
+ if (CONTEXT_FLAGS.has(word))
90
+ return true;
91
+ const long = word.startsWith('--') ? word.split('=')[0] : undefined;
92
+ if (long !== undefined)
93
+ return CONTEXT_FLAGS.has(long);
94
+ return /^-[ABC]\d+$/.test(word);
95
+ });
96
+ }
97
+ function isListing(words) {
98
+ return words.slice(1).some((word) => LISTING_FLAGS.has(word.split('=')[0]));
99
+ }
100
+ /**
101
+ * Split a command into words IF it is one simple command: no pipes, no logic, no
102
+ * redirects, no substitutions, no expansions this code would have to model. Anything
103
+ * else returns `undefined` and the caller treats the command as unknowable — the guard
104
+ * allows, this derivation names no terms.
105
+ */
106
+ export function simpleCommandWords(command) {
107
+ const words = [];
108
+ let current = '';
109
+ let started = false;
110
+ let i = 0;
111
+ const push = () => {
112
+ if (started)
113
+ words.push(current);
114
+ current = '';
115
+ started = false;
116
+ };
117
+ while (i < command.length) {
118
+ const ch = command[i];
119
+ if ('|&;<>()`$\\\n*?~{}!'.includes(ch))
120
+ return undefined; // shell would interpret it
121
+ if (ch === "'" || ch === '"') {
122
+ const quote = ch;
123
+ i += 1;
124
+ started = true;
125
+ while (i < command.length && command[i] !== quote) {
126
+ if (quote === '"' && (command[i] === '$' || command[i] === '`' || command[i] === '\\')) {
127
+ return undefined; // expansions inside double quotes — not simple
128
+ }
129
+ current += command[i];
130
+ i += 1;
131
+ }
132
+ if (i >= command.length)
133
+ return undefined; // unterminated quote
134
+ i += 1;
135
+ continue;
136
+ }
137
+ if (ch === ' ' || ch === '\t') {
138
+ push();
139
+ i += 1;
140
+ continue;
141
+ }
142
+ current += ch;
143
+ started = true;
144
+ i += 1;
145
+ }
146
+ push();
147
+ return words;
148
+ }
149
+ /** Flags that consume the next word, so it is never mistaken for the pattern. */
150
+ const TAKES_VALUE = new Set([
151
+ '-e',
152
+ '--regexp',
153
+ '-f',
154
+ '--file',
155
+ '-m',
156
+ '--max-count',
157
+ '-A',
158
+ '--after-context',
159
+ '-B',
160
+ '--before-context',
161
+ '-C',
162
+ '--context',
163
+ '-d',
164
+ '--directories',
165
+ '-D',
166
+ '--devices',
167
+ '--include',
168
+ '--exclude',
169
+ '--exclude-dir',
170
+ '-t',
171
+ '--type',
172
+ '-T',
173
+ '--type-not',
174
+ '-g',
175
+ '--glob',
176
+ '--iglob',
177
+ '-j',
178
+ '--threads',
179
+ '--color',
180
+ '--colour',
181
+ ]);
182
+ /**
183
+ * Every pattern a grep/rg invocation searches for: each explicit `-e`/`--regexp`
184
+ * value if any are given, else the first word that is not a flag or a flag's value.
185
+ * Empty when the parse is not sure.
186
+ */
187
+ export function searchPatterns(words) {
188
+ const explicit = [];
189
+ let positional;
190
+ let i = 1;
191
+ while (i < words.length) {
192
+ const word = words[i];
193
+ if (word === '--') {
194
+ positional ??= words[i + 1];
195
+ break;
196
+ }
197
+ if (word === '-e' || word === '--regexp') {
198
+ const value = words[i + 1];
199
+ if (value !== undefined)
200
+ explicit.push(value);
201
+ i += 2;
202
+ continue;
203
+ }
204
+ if (word.startsWith('--') && word.includes('=')) {
205
+ i += 1;
206
+ continue;
207
+ }
208
+ if (word.startsWith('-') && word.length > 1) {
209
+ i += TAKES_VALUE.has(word) ? 2 : 1;
210
+ continue;
211
+ }
212
+ if (positional === undefined && explicit.length === 0)
213
+ positional = word;
214
+ i += 1;
215
+ }
216
+ if (explicit.length > 0)
217
+ return explicit;
218
+ return positional === undefined ? [] : [positional];
219
+ }
220
+ /** The first pattern of {@link searchPatterns}; `undefined` when there is none. */
221
+ export function searchPattern(words) {
222
+ return searchPatterns(words)[0];
223
+ }
224
+ /** Single-quote a value for `sh` unless it is plainly safe bare. */
225
+ export function shellQuote(value) {
226
+ if (/^[A-Za-z0-9_./:=-]+$/.test(value))
227
+ return value;
228
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
229
+ }
230
+ //# sourceMappingURL=focus-terms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-terms.js","sourceRoot":"","sources":["../../src/hooks/focus-terms.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Focus terms, derived from the command that produced a blob — one derivation, zero\n * imports, shared by the hooks guard and the ops seam.\n *\n * The guard is the producer expert: to decide anything about a `grep` it has already\n * parsed the pattern out of the command. Before this file that knowledge died inside\n * the guard — the deny reason printed `--focus <?>` and the model reinvented what the\n * guard knew. Now the derivation lives here, beside the guard and importing nothing\n * (the guard's latency budget is a stat and an exit, so this must stay a\n * zero-dependency sibling rather than an exception to its no-library-import rule),\n * and `smeltBlob` applies the same function to a caller's `producer` hint — so the\n * guard's rewrite wrap, `smelt --producer` and the `smelt_file` tool cannot disagree\n * about which terms a command names.\n *\n * The question it answers is narrower than \"what did the command search for\". It is:\n * **which terms, if any, distinguish the output lines the task is about from the ones\n * it is not.** Focus is what the lexical planner keeps; a term every output line\n * carries keeps everything and cuts nothing, exactly when the output is large. So:\n *\n * - a plain `grep`/`rg` prints only matching lines — every line has the pattern, the\n * pattern distinguishes nothing, and the answer is no terms (the planner's\n * head-and-tail rule is the right cut);\n * - a search with context (`-C`, `-A`, `-B`, `--context`…) prints non-matching lines\n * around each hit, and there the pattern is exactly the focus;\n * - a listing search (`-l`, `-c`, `--files-with-matches`…) prints no matching lines\n * at all, so its pattern names nothing in the output;\n * - a producer that states no term (`cat`, `git diff`, `sed`) yields none, and so\n * does a command this parser cannot see whole — unsure means no terms, the same\n * fail-open rule the guard lives under.\n *\n * Every rule above is a fact about what the command prints, stated as data in\n * {@link SEARCH_PROGRAMS}, {@link CONTEXT_FLAGS} and {@link LISTING_FLAGS} — never a\n * guess about the text.\n */\n\n/** The search programs whose first non-flag word (or every `-e`) is a pattern. */\nconst SEARCH_PROGRAMS: ReadonlySet<string> = new Set(['grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack']);\n\n/** Flags under which a search prints lines that do *not* match — where focus pays. */\nconst CONTEXT_FLAGS: ReadonlySet<string> = new Set([\n '-A',\n '-B',\n '-C',\n '--after-context',\n '--before-context',\n '--context',\n]);\n\n/** Flags under which a search prints no matching lines at all. */\nconst LISTING_FLAGS: ReadonlySet<string> = new Set([\n '-l',\n '-L',\n '-c',\n '--files-with-matches',\n '--files-without-match',\n '--count',\n '--count-matches',\n '--files',\n]);\n\n/**\n * The terms a producer command names that distinguish output lines the task is about.\n * `[]` whenever the honest answer is \"none\" — see the module doc. Never throws.\n */\nexport function focusTermsFor(command: string | undefined): readonly string[] {\n if (command === undefined) return [];\n const words = simpleCommandWords(command.trim());\n if (words === undefined || words.length === 0) return [];\n const search = searchWords(words);\n if (search === undefined) return [];\n if (!printsContext(search)) return [];\n if (isListing(search)) return [];\n return searchPatterns(search);\n}\n\n/**\n * The words of the search invocation, with `git grep` normalised to `grep`, or\n * `undefined` when the program is not a search at all.\n */\nfunction searchWords(words: readonly string[]): readonly string[] | undefined {\n const program = words[0]!.split('/').at(-1)!;\n if (program === 'git' && words[1] === 'grep') return ['grep', ...words.slice(2)];\n return SEARCH_PROGRAMS.has(program) ? words : undefined;\n}\n\n/** True when a context flag is present — spaced (`-C 3`), compact (`-C3`) or `=`-joined. */\nfunction printsContext(words: readonly string[]): boolean {\n return words.slice(1).some((word) => {\n if (CONTEXT_FLAGS.has(word)) return true;\n const long = word.startsWith('--') ? word.split('=')[0]! : undefined;\n if (long !== undefined) return CONTEXT_FLAGS.has(long);\n return /^-[ABC]\\d+$/.test(word);\n });\n}\n\nfunction isListing(words: readonly string[]): boolean {\n return words.slice(1).some((word) => LISTING_FLAGS.has(word.split('=')[0]!));\n}\n\n/**\n * Split a command into words IF it is one simple command: no pipes, no logic, no\n * redirects, no substitutions, no expansions this code would have to model. Anything\n * else returns `undefined` and the caller treats the command as unknowable — the guard\n * allows, this derivation names no terms.\n */\nexport function simpleCommandWords(command: string): readonly string[] | undefined {\n const words: string[] = [];\n let current = '';\n let started = false;\n let i = 0;\n const push = (): void => {\n if (started) words.push(current);\n current = '';\n started = false;\n };\n while (i < command.length) {\n const ch = command[i]!;\n if ('|&;<>()`$\\\\\\n*?~{}!'.includes(ch)) return undefined; // shell would interpret it\n if (ch === \"'\" || ch === '\"') {\n const quote = ch;\n i += 1;\n started = true;\n while (i < command.length && command[i] !== quote) {\n if (quote === '\"' && (command[i] === '$' || command[i] === '`' || command[i] === '\\\\')) {\n return undefined; // expansions inside double quotes — not simple\n }\n current += command[i]!;\n i += 1;\n }\n if (i >= command.length) return undefined; // unterminated quote\n i += 1;\n continue;\n }\n if (ch === ' ' || ch === '\\t') {\n push();\n i += 1;\n continue;\n }\n current += ch;\n started = true;\n i += 1;\n }\n push();\n return words;\n}\n\n/** Flags that consume the next word, so it is never mistaken for the pattern. */\nconst TAKES_VALUE: ReadonlySet<string> = new Set([\n '-e',\n '--regexp',\n '-f',\n '--file',\n '-m',\n '--max-count',\n '-A',\n '--after-context',\n '-B',\n '--before-context',\n '-C',\n '--context',\n '-d',\n '--directories',\n '-D',\n '--devices',\n '--include',\n '--exclude',\n '--exclude-dir',\n '-t',\n '--type',\n '-T',\n '--type-not',\n '-g',\n '--glob',\n '--iglob',\n '-j',\n '--threads',\n '--color',\n '--colour',\n]);\n\n/**\n * Every pattern a grep/rg invocation searches for: each explicit `-e`/`--regexp`\n * value if any are given, else the first word that is not a flag or a flag's value.\n * Empty when the parse is not sure.\n */\nexport function searchPatterns(words: readonly string[]): readonly string[] {\n const explicit: string[] = [];\n let positional: string | undefined;\n let i = 1;\n while (i < words.length) {\n const word = words[i]!;\n if (word === '--') {\n positional ??= words[i + 1];\n break;\n }\n if (word === '-e' || word === '--regexp') {\n const value = words[i + 1];\n if (value !== undefined) explicit.push(value);\n i += 2;\n continue;\n }\n if (word.startsWith('--') && word.includes('=')) {\n i += 1;\n continue;\n }\n if (word.startsWith('-') && word.length > 1) {\n i += TAKES_VALUE.has(word) ? 2 : 1;\n continue;\n }\n if (positional === undefined && explicit.length === 0) positional = word;\n i += 1;\n }\n if (explicit.length > 0) return explicit;\n return positional === undefined ? [] : [positional];\n}\n\n/** The first pattern of {@link searchPatterns}; `undefined` when there is none. */\nexport function searchPattern(words: readonly string[]): string | undefined {\n return searchPatterns(words)[0];\n}\n\n/** Single-quote a value for `sh` unless it is plainly safe bare. */\nexport function shellQuote(value: string): string {\n if (/^[A-Za-z0-9_./:=-]+$/.test(value)) return value;\n return `'${value.replaceAll(\"'\", `'\"'\"'`)}'`;\n}\n"]}
@@ -1,3 +1,9 @@
1
+ /**
2
+ * The command parsing lives in `./focus-terms.ts` — a zero-import sibling, so the
3
+ * guard's no-library-import rule holds — and is re-exported here because this module
4
+ * is the published `hooks/guard-core` subpath every shim and the opencode plugin load.
5
+ */
6
+ export { focusTermsFor, searchPattern, searchPatterns, shellQuote, simpleCommandWords, } from './focus-terms.ts';
1
7
  /**
2
8
  * The guard core — one zero-dependency node module, shared by every harness shim.
3
9
  *
@@ -16,7 +22,8 @@
16
22
  * A guard that can brick a session on bad input is worse than no guard; the agent
17
23
  * loses nothing but the optimization, and the warning says so.
18
24
  * - **No library import on any path.** This module imports node builtins only —
19
- * never `../index.ts`, never a planner, never web-tree-sitter. The allow case is
25
+ * never `../index.ts`, never a planner, never web-tree-sitter plus its one
26
+ * zero-import sibling `./focus-terms.ts`, which owns the command parsing. The allow case is
20
27
  * a stat and an exit; the research note
21
28
  * (docs/research/2026-09-02-agent-enforcement.md § 5) budgets the always-on guard
22
29
  * at tens of milliseconds, and loading grammar machinery here would spend that
@@ -142,21 +149,6 @@ export declare function decide(request: GuardRequest, settings: GuardSettings, c
142
149
  size: number;
143
150
  isFile: boolean;
144
151
  } | undefined): GuardDecision;
145
- /**
146
- * Split a command into words IF it is one simple command: no pipes, no logic, no
147
- * redirects, no substitutions, no expansions this code would have to model. Anything
148
- * else returns `undefined` and the caller allows — the guard judges only what it can
149
- * see whole.
150
- */
151
- export declare function simpleCommandWords(command: string): readonly string[] | undefined;
152
- /**
153
- * The pattern a grep/rg invocation searches for: an explicit `-e`/`--regexp` value if
154
- * given, else the first word that is not a flag or a flag's value. `undefined` when
155
- * the parse is not sure — and unsure means allow, like everything else here.
156
- */
157
- export declare function searchPattern(words: readonly string[]): string | undefined;
158
- /** Single-quote a value for `sh` unless it is plainly safe bare. */
159
- export declare function shellQuote(value: string): string;
160
152
  /** True when this module is the file node was asked to run, not an import. */
161
153
  export declare function isMainModule(moduleUrl: string): boolean;
162
154
  /** Every byte of fd 0 to EOF, retrying EAGAIN — the same shape `cli/bin.ts` uses. */