@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.
- package/dist/apply.d.ts.map +1 -1
- package/dist/apply.js +8 -2
- package/dist/apply.js.map +1 -1
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js +9 -4
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/doctor.d.ts +66 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +137 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/hooks.d.ts +36 -2
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +108 -64
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +12 -16
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/installed.d.ts +56 -0
- package/dist/cli/installed.d.ts.map +1 -0
- package/dist/cli/installed.js +105 -0
- package/dist/cli/installed.js.map +1 -0
- package/dist/cli/lava.d.ts +31 -0
- package/dist/cli/lava.d.ts.map +1 -0
- package/dist/cli/lava.js +82 -0
- package/dist/cli/lava.js.map +1 -0
- package/dist/cli/report.d.ts +10 -1
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +16 -1
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/setup.d.ts +87 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +513 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/shell.d.ts +11 -4
- package/dist/cli/shell.d.ts.map +1 -1
- package/dist/cli/shell.js.map +1 -1
- package/dist/cli/subcommands/agents.d.ts.map +1 -1
- package/dist/cli/subcommands/agents.js +2 -1
- package/dist/cli/subcommands/agents.js.map +1 -1
- package/dist/cli/subcommands/doctor.d.ts +15 -0
- package/dist/cli/subcommands/doctor.d.ts.map +1 -0
- package/dist/cli/subcommands/doctor.js +43 -0
- package/dist/cli/subcommands/doctor.js.map +1 -0
- package/dist/cli/subcommands/flags.d.ts +16 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -1
- package/dist/cli/subcommands/flags.js +43 -4
- package/dist/cli/subcommands/flags.js.map +1 -1
- package/dist/cli/subcommands/hooks.d.ts.map +1 -1
- package/dist/cli/subcommands/hooks.js +10 -2
- package/dist/cli/subcommands/hooks.js.map +1 -1
- package/dist/cli/subcommands/init.d.ts.map +1 -1
- package/dist/cli/subcommands/init.js +2 -1
- package/dist/cli/subcommands/init.js.map +1 -1
- package/dist/cli/subcommands/registry.d.ts +3 -1
- package/dist/cli/subcommands/registry.d.ts.map +1 -1
- package/dist/cli/subcommands/registry.js +6 -0
- package/dist/cli/subcommands/registry.js.map +1 -1
- package/dist/cli/subcommands/setup.d.ts +23 -0
- package/dist/cli/subcommands/setup.d.ts.map +1 -0
- package/dist/cli/subcommands/setup.js +87 -0
- package/dist/cli/subcommands/setup.js.map +1 -0
- package/dist/cli/subcommands/smelt.d.ts +3 -0
- package/dist/cli/subcommands/smelt.d.ts.map +1 -1
- package/dist/cli/subcommands/smelt.js +5 -1
- package/dist/cli/subcommands/smelt.js.map +1 -1
- package/dist/cli/subcommands/stats.d.ts +8 -3
- package/dist/cli/subcommands/stats.d.ts.map +1 -1
- package/dist/cli/subcommands/stats.js +15 -5
- package/dist/cli/subcommands/stats.js.map +1 -1
- package/dist/cli/subcommands/subcommand.d.ts +1 -1
- package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
- package/dist/cli/subcommands/subcommand.js.map +1 -1
- package/dist/cli/wizard.d.ts +72 -0
- package/dist/cli/wizard.d.ts.map +1 -0
- package/dist/cli/wizard.js +86 -0
- package/dist/cli/wizard.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -1
- package/dist/harness/claude-code.d.ts.map +1 -1
- package/dist/harness/claude-code.js +10 -0
- package/dist/harness/claude-code.js.map +1 -1
- package/dist/harness/codex.d.ts.map +1 -1
- package/dist/harness/codex.js +1 -0
- package/dist/harness/codex.js.map +1 -1
- package/dist/harness/grok.d.ts.map +1 -1
- package/dist/harness/grok.js +4 -1
- package/dist/harness/grok.js.map +1 -1
- package/dist/harness/kilocode.d.ts.map +1 -1
- package/dist/harness/kilocode.js +1 -1
- package/dist/harness/kilocode.js.map +1 -1
- package/dist/harness/opencode.d.ts.map +1 -1
- package/dist/harness/opencode.js +10 -0
- package/dist/harness/opencode.js.map +1 -1
- package/dist/harness/profile.d.ts +23 -2
- package/dist/harness/profile.d.ts.map +1 -1
- package/dist/harness/profile.js.map +1 -1
- package/dist/harness/snippet.d.ts +14 -1
- package/dist/harness/snippet.d.ts.map +1 -1
- package/dist/harness/snippet.js +18 -2
- package/dist/harness/snippet.js.map +1 -1
- package/dist/hooks/focus-terms.d.ts +57 -0
- package/dist/hooks/focus-terms.d.ts.map +1 -0
- package/dist/hooks/focus-terms.js +230 -0
- package/dist/hooks/focus-terms.js.map +1 -0
- package/dist/hooks/guard-core.d.ts +8 -16
- package/dist/hooks/guard-core.d.ts.map +1 -1
- package/dist/hooks/guard-core.js +22 -120
- package/dist/hooks/guard-core.js.map +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/ops/index.d.ts +3 -3
- package/dist/ops/index.d.ts.map +1 -1
- package/dist/ops/index.js +2 -2
- package/dist/ops/index.js.map +1 -1
- package/dist/ops/verbs.d.ts +57 -1
- package/dist/ops/verbs.d.ts.map +1 -1
- package/dist/ops/verbs.js +49 -1
- package/dist/ops/verbs.js.map +1 -1
- package/dist/plan/auto.d.ts +9 -2
- package/dist/plan/auto.d.ts.map +1 -1
- package/dist/plan/auto.js +14 -2
- package/dist/plan/auto.js.map +1 -1
- package/dist/plan/diff.d.ts +45 -0
- package/dist/plan/diff.d.ts.map +1 -0
- package/dist/plan/diff.js +284 -0
- package/dist/plan/diff.js.map +1 -0
- package/dist/plan/json.d.ts +37 -0
- package/dist/plan/json.d.ts.map +1 -0
- package/dist/plan/json.js +181 -0
- package/dist/plan/json.js.map +1 -0
- package/dist/plan/kind.d.ts +28 -0
- package/dist/plan/kind.d.ts.map +1 -0
- package/dist/plan/kind.js +57 -0
- package/dist/plan/kind.js.map +1 -0
- package/dist/plan/offsets.d.ts +8 -0
- package/dist/plan/offsets.d.ts.map +1 -0
- package/dist/plan/offsets.js +19 -0
- package/dist/plan/offsets.js.map +1 -0
- package/dist/plan/planners.d.ts +15 -5
- package/dist/plan/planners.d.ts.map +1 -1
- package/dist/plan/planners.js +13 -5
- package/dist/plan/planners.js.map +1 -1
- package/dist/plan/structural.d.ts.map +1 -1
- package/dist/plan/structural.js +106 -14
- package/dist/plan/structural.js.map +1 -1
- package/dist/retrieve.d.ts +22 -1
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +59 -0
- package/dist/retrieve.js.map +1 -1
- package/dist/setup/recipe.d.ts +73 -0
- package/dist/setup/recipe.d.ts.map +1 -0
- package/dist/setup/recipe.js +76 -0
- package/dist/setup/recipe.js.map +1 -0
- package/dist/smelter.d.ts +4 -0
- package/dist/smelter.d.ts.map +1 -1
- package/dist/smelter.js +4 -0
- package/dist/smelter.js.map +1 -1
- package/dist/stats.d.ts +16 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +29 -0
- package/dist/stats.js.map +1 -1
- package/dist/store-dir.d.ts +9 -2
- package/dist/store-dir.d.ts.map +1 -1
- package/dist/store-dir.js +48 -7
- package/dist/store-dir.js.map +1 -1
- package/dist/store.d.ts +4 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +12 -2
- package/dist/store.js.map +1 -1
- package/dist/text/json-edit.d.ts +15 -1
- package/dist/text/json-edit.d.ts.map +1 -1
- package/dist/text/json-edit.js +76 -5
- package/dist/text/json-edit.js.map +1 -1
- package/dist/types.d.ts +116 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/report.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAsB,MAAM,mBAAmB,CAAC;AAC9E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAKD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,WAAW,GAAG,MAAM,CAsE5F;AAED,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,cAAc,GAAG,MAAM,CA+BrF;AAED,uCAAuC;AACvC,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,gBAAgB,EACxB,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,iBAAiB,GACpC,MAAM,CA8GR","sourcesContent":["import { GUIDE, GUIDE_TITLE } from '../agents/guide.ts';\nimport { overBudgetBytes } from '../agents/lint.ts';\nimport type { AgentsLintReport, AgentsMirrorReport } from '../agents/lint.ts';\nimport type { RepoMap } from '../repomap/map.ts';\nimport type { SmeltResult } from '../types.ts';\n\nimport { CONFIG_FILE_NAME } from './config.ts';\nimport { CLI_NAME } from './shell.ts';\n\nexport interface ReportInput {\n readonly result: SmeltResult;\n /** What to call the input in the header: a path, or `'<stdin>'`. */\n readonly source: string;\n /** The budget the caller asked for, so the report can say when it was missed. */\n readonly budgetBytes: number;\n /** The exact text that was smelted. Used only to count lines inside each range. */\n readonly inputText: string;\n}\n\n/** Longest explanation printed in full before it gets an ellipsis. */\nconst EXPLANATION_WIDTH = 46;\n\n/**\n * The report, for stderr.\n *\n * Every total here is read straight off the {@link SmeltResult} — `inputBytes`,\n * `outputBytes`, `elisions.length`. The CLI keeps no counters of its own, because two\n * pieces of code counting the same bytes is how a report ends up disagreeing with the\n * library it is reporting on, and the report is the thing a human believes.\n * `test/cli.test.ts` asserts the printed numbers equal the result's fields.\n */\nexport function formatReport({ result, source, budgetBytes, inputText }: ReportInput): string {\n const lines: string[] = [];\n\n lines.push([CLI_NAME, source, result.language, result.planner].join(' '));\n lines.push(\n `in ${group(result.inputBytes)} B → out ${group(result.outputBytes)} B ` +\n `(${delta(result.inputBytes, result.outputBytes)}, ${count(result.elisions.length, 'elision')})`,\n );\n\n if (result.measured !== undefined) {\n const { input, output, unit, measure } = result.measured;\n lines.push(`in ${group(input)} → out ${group(output)} ${unit} (${measure})`);\n }\n\n if (result.outputBytes > budgetBytes) {\n lines.push('');\n lines.push(\n `OVER BUDGET ${group(result.outputBytes)} B against a ${group(budgetBytes)} B budget ` +\n `— over by ${group(result.outputBytes - budgetBytes)} B.`,\n );\n lines.push(' The plan is reported as it came back. smelt did not cut the regions');\n lines.push(' you asked to keep in order to make a number look right.');\n }\n\n if (result.elisions.length === 0) {\n lines.push('');\n lines.push(' nothing elided — the input already fits, or every run was too small to');\n lines.push(' be worth a marker (a marker that costs more than the lines it replaces');\n lines.push(' makes the output bigger).');\n return `${lines.join('\\n')}\\n`;\n }\n\n const rows = result.elisions.map((elision) => ({\n rule: elision.reason.rule,\n lines: String(lineSpan(inputText, elision.range.start, elision.range.end)),\n bytes: group(elision.bytes),\n hash: elision.hash,\n explanation: clip(elision.reason.explanation, EXPLANATION_WIDTH),\n }));\n\n const ruleWidth = width(\n 'rule',\n rows.map((row) => row.rule),\n );\n const linesWidth = width(\n 'lines',\n rows.map((row) => row.lines),\n );\n const bytesWidth = width(\n 'bytes',\n rows.map((row) => row.bytes),\n );\n const hashWidth = width(\n 'hash',\n rows.map((row) => row.hash),\n );\n\n lines.push('');\n lines.push(\n ` ${'rule'.padEnd(ruleWidth)} ${'lines'.padStart(linesWidth)} ` +\n `${'bytes'.padStart(bytesWidth)} ${'hash'.padEnd(hashWidth)} explanation`,\n );\n for (const row of rows) {\n lines.push(\n ` ${row.rule.padEnd(ruleWidth)} ${row.lines.padStart(linesWidth)} ` +\n `${row.bytes.padStart(bytesWidth)} ${row.hash.padEnd(hashWidth)} ${row.explanation}`,\n );\n }\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** What `smelt map` prints to stderr. */\nexport interface MapReportInput {\n readonly map: RepoMap;\n /** The directory named on the command line, exactly as the user wrote it. */\n readonly source: string;\n /**\n * Where the budget came from — the `ResolvedMapRun.budgetSource` receipt, printed\n * beside the budget so a surprising number can be traced to the flag or the config\n * file that set it without re-deriving the precedence by hand.\n */\n readonly budgetSource: 'flag' | 'config';\n}\n\n/**\n * The map report, for stderr — same law as {@link formatReport}: every number is\n * read straight off the {@link RepoMap} the library returned. In particular the\n * \"bytes used\" figure is `map.outputBytes`, which the library measured off the\n * rendered text — the CLI counts nothing itself, because a report that keeps its\n * own tally is a report that can disagree with the map it describes.\n * `test/guards/repo-map.test.ts` asserts the printed figure equals the actual byte\n * length of what landed on stdout, and a mutation proves the assertion can go red.\n */\nexport function formatMapReport({ map, source, budgetSource }: MapReportInput): string {\n const lines: string[] = [];\n\n lines.push([`${CLI_NAME} map`, source, map.id].join(' '));\n lines.push(\n `files scanned ${group(map.filesScanned)}` +\n (map.binarySkipped === 0 ? '' : ` (${count(map.binarySkipped, 'binary file')} skipped)`) +\n ` symbols ranked ${group(map.definitionsTotal)}`,\n );\n lines.push(\n `included ${group(map.entries.length)} of ${group(map.definitionsTotal)} symbols` +\n (map.pathOnlyTotal === 0\n ? ''\n : ` + ${group(map.pathOnly.length)} of ${group(map.pathOnlyTotal)} path-only files`),\n );\n lines.push(\n `bytes used ${group(map.outputBytes)} of ${group(map.budgetBytes)} budget (${budgetSource}) ` +\n `— the map fits itself to the budget by construction, so there is no over-budget exit`,\n );\n\n if (map.cache !== undefined) {\n lines.push(\n `cache ${count(map.cache.hits, 'hit')}, ${count(map.cache.misses, 'miss', 'es')}, ` +\n `${group(map.cache.discarded)} discarded, ${group(map.cache.pruned)} pruned`,\n );\n }\n for (const warning of map.warnings) {\n lines.push(`warning ${warning.rule}: ${warning.explanation}`);\n }\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** What `smelt agents lint` prints. */\nexport interface AgentsReportInput {\n /** The directory named on the command line, exactly as the user wrote it. */\n readonly source: string;\n /** Whether `--strict` was given — it changes what the closing line promises. */\n readonly strict: boolean;\n}\n\n/**\n * The lint report — and unlike the other two, it goes to **stdout**, because here the\n * report *is* the output. `smelt` and `smelt map` put a payload on stdout and their\n * report on stderr so the two can be piped apart; a lint has no payload to separate\n * from, and sending its only output to stderr would make `smelt agents lint > audit.txt`\n * write an empty file.\n *\n * Same law as the other two, though: every number is read off the\n * {@link AgentsLintReport} the library returned. The renderer counts nothing, so it\n * cannot disagree with what was measured — and in particular both totals are the ones\n * the lint computed over the levels, not a second tally over the printed rows. The\n * guide's cited figure is likewise read from `agents/guide.ts` rather than retyped\n * under the guide's name.\n */\nexport function formatAgentsReport(\n report: AgentsLintReport,\n { source, strict }: AgentsReportInput,\n): string {\n const lines: string[] = [`${CLI_NAME} agents lint ${source}`];\n\n if (report.levels.length === 0) {\n lines.push('');\n lines.push(` no AGENTS.md, CLAUDE.md or GEMINI.md under ${source} — nothing is loaded on`);\n lines.push(' every request, so there is nothing to measure. That is a fine state, not a');\n lines.push(` failure; \\`${CLI_NAME} agents lint\\` has no opinion about whether you want one.`);\n return `${lines.join('\\n')}\\n`;\n }\n\n lines.push('');\n lines.push(' the instruction files in this tree');\n const labelWidth = report.levels.reduce(\n (widest, level) =>\n Math.max(widest, level.path.length, ...level.mirrors.map((m) => m.path.length)),\n Math.max(IMPERATIVES_LABEL.length, PER_REQUEST_LABEL.length, WHOLE_TREE_LABEL.length),\n );\n for (const level of report.levels) {\n lines.push(` ${level.path.padEnd(labelWidth)} ${group(level.bytes).padStart(9)} B`);\n for (const mirror of level.mirrors) {\n lines.push(\n ` ${mirror.path.padEnd(labelWidth)} ${' '.repeat(11)}${mirrorNote(mirror.standing)}`,\n );\n }\n }\n lines.push(\n ` ${PER_REQUEST_LABEL.padEnd(labelWidth)} ${group(report.perRequestBytes).padStart(9)} B`,\n );\n // Printed only when it is a different number — which is exactly when a reader could\n // otherwise mistake the sum for a per-request cost. In a single-chain repository the\n // two are equal and a second row would be noise claiming to be information.\n if (report.totalBytes !== report.perRequestBytes) {\n lines.push(\n ` ${WHOLE_TREE_LABEL.padEnd(labelWidth)} ${group(report.totalBytes).padStart(9)} B`,\n );\n lines.push(' a nested file merges with its ancestors, never with its siblings, so no');\n lines.push(' one request loads the whole tree. Per request is the heaviest chain.');\n }\n lines.push(\n ` ${IMPERATIVES_LABEL.padEnd(labelWidth)} ` +\n `${group(report.imperatives.length).padStart(9)}`,\n );\n // The figure is the guide's, so it is read from where the guide is quoted rather\n // than retyped here: an explanation that paraphrases its source drifts from it\n // silently, and this one is printed under the guide's own name.\n for (const wrapped of wrap(`${GUIDE_TITLE} cites: \"${GUIDE.instructionCeiling}\".`, 68)) {\n lines.push(` ${wrapped}`);\n }\n lines.push(' Printed as a citation, compared to nothing: the only ceiling here is');\n lines.push(' the one you set.');\n\n const over = overBudgetBytes(report);\n lines.push('');\n if (report.budgetBytes === undefined) {\n lines.push(` no budget set — add {\"agents\":{\"budgetBytes\":N}} to ${CONFIG_FILE_NAME} and`);\n lines.push(` exceeding it exits 1. ${CLI_NAME} will never invent that number for you.`);\n } else if (over === undefined) {\n lines.push(\n ` within budget ${group(report.totalBytes)} B of ${group(report.budgetBytes)} B ` +\n `(${CONFIG_FILE_NAME}: agents.budgetBytes, against the whole tree).`,\n );\n } else {\n lines.push(\n ` OVER BUDGET ${group(report.totalBytes)} B against your ${group(report.budgetBytes)} B ` +\n `budget — over by ${group(over)} B.`,\n );\n lines.push(` The budget is yours, from ${CONFIG_FILE_NAME}, and it caps the`);\n lines.push(' whole tree rather than one request — the stricter of the two, so');\n lines.push(' it cannot be met by moving bytes into another package. Exit 1 is');\n lines.push(' the same over-budget code every other smelt run uses.');\n }\n\n if (report.findings.length === 0) {\n lines.push('');\n lines.push(' no findings. Eight advisory rules ran and none matched — either the file is');\n lines.push(' in good shape, or a rule is asleep. `pnpm mutate` is how this repo tells the');\n lines.push(' difference about its own guards; a fixture per rule is how it tells it here.');\n return `${lines.join('\\n')}\\n`;\n }\n\n const rows = report.findings.map((finding) => ({\n place: `${finding.file}:${String(finding.line)}`,\n rule: finding.reason.rule,\n explanation: finding.reason.explanation,\n }));\n const ruleWidth = width(\n '',\n rows.map((row) => row.rule),\n );\n\n lines.push('');\n for (const row of rows) {\n lines.push(` ${row.rule.padEnd(ruleWidth)} ${row.place}`);\n // The explanation gets its own wrapped lines rather than a fourth column. It is\n // the *reason*, which is the part a reader actually has to read — clipped to a\n // terminal column it becomes an ellipsis, and Law 2 promises an explanation, not\n // the first 46 characters of one.\n for (const wrapped of wrap(row.explanation, EXPLANATION_WRAP)) lines.push(` ${wrapped}`);\n }\n\n lines.push('');\n lines.push(\n ` ${count(report.findings.length, 'finding')}. ` +\n (strict\n ? 'Exit 1: --strict was given.'\n : `Advisory — exit 0. Pass --strict to fail a CI run on any of them.`),\n );\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** The label the imperative count is reported under. Never \"instructions\": R6. */\nconst IMPERATIVES_LABEL = 'imperatives (heuristic)';\n\n/** The heaviest ancestor chain — the honest answer to \"what does a request cost\". */\nconst PER_REQUEST_LABEL = 'per request (worst case)';\n\n/** Every level summed. The repository's instruction surface, and not a request's cost. */\nconst WHOLE_TREE_LABEL = 'whole tree';\n\n/** Where a finding's explanation wraps, once its six-column indent is removed. */\nconst EXPLANATION_WRAP = 84;\n\n/** Greedy word wrap. No ICU, no dependency — the output is identical everywhere. */\nfunction wrap(text: string, columns: number): readonly string[] {\n const out: string[] = [];\n let line = '';\n for (const word of text.split(' ')) {\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > columns) {\n out.push(line);\n line = word;\n } else {\n line = candidate;\n }\n }\n if (line !== '') out.push(line);\n return out;\n}\n\n/** How a mirror stands beside its AGENTS.md, in one clause. */\nfunction mirrorNote(standing: AgentsMirrorReport['standing']): string {\n switch (standing) {\n case 'symlink':\n return 'symlink — cannot drift, and costs no extra bytes';\n case 'copy':\n return 'copy, byte-identical today — a symlink could not drift';\n case 'drift':\n return 'DIVERGED — see mirror-drift below';\n }\n}\n\n/** How many lines a byte range covers in the input. Derived, never tallied separately. */ function lineSpan(\n text: string,\n start: number,\n end: number,\n): number {\n const slice = Buffer.from(text, 'utf8').subarray(start, end).toString('utf8');\n return slice.split('\\n').length;\n}\n\nfunction width(header: string, values: readonly string[]): number {\n return values.reduce((widest, value) => Math.max(widest, value.length), header.length);\n}\n\n/** Thousands separators without pulling in ICU, so the output is identical everywhere. */\nfunction group(n: number): string {\n const digits = String(Math.abs(Math.trunc(n)));\n const grouped = digits.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n return n < 0 ? `-${grouped}` : grouped;\n}\n\nfunction delta(inputBytes: number, outputBytes: number): string {\n if (inputBytes === 0) return 'empty input';\n const percent = ((outputBytes - inputBytes) / inputBytes) * 100;\n const sign = percent > 0 ? '+' : '';\n return `${sign}${percent.toFixed(1)}%`;\n}\n\nfunction count(n: number, noun: string, pluralSuffix = 's'): string {\n return `${group(n)} ${noun}${n === 1 ? '' : pluralSuffix}`;\n}\n\nfunction clip(text: string, max: number): string {\n return text.length <= max ? text : `${text.slice(0, max - 1)}…`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAsB,MAAM,mBAAmB,CAAC;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAKD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,MAAM,EACN,WAAW,EACX,SAAS,EACT,KAAK,EACL,YAA2B,GAC5B,EAAE,WAAW,GAAG,MAAM,CAqFtB;AAKD,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,cAAc,GAAG,MAAM,CA+BrF;AAED,uCAAuC;AACvC,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,gBAAgB,EACxB,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,iBAAiB,GACpC,MAAM,CA8GR","sourcesContent":["import { GUIDE, GUIDE_TITLE } from '../agents/guide.ts';\nimport { overBudgetBytes } from '../agents/lint.ts';\nimport type { AgentsLintReport, AgentsMirrorReport } from '../agents/lint.ts';\nimport type { ResolvedFocus } from '../ops/verbs.ts';\nimport type { RepoMap } from '../repomap/map.ts';\nimport type { SmeltResult } from '../types.ts';\n\nimport { CONFIG_FILE_NAME } from './config.ts';\nimport { CLI_NAME } from './shell.ts';\n\nexport interface ReportInput {\n readonly result: SmeltResult;\n /** What to call the input in the header: a path, or `'<stdin>'`. */\n readonly source: string;\n /** The budget the caller asked for, so the report can say when it was missed. */\n readonly budgetBytes: number;\n /** The exact text that was smelted. Used only to count lines inside each range. */\n readonly inputText: string;\n /**\n * The focus the planner saw and whose it was — printed so a reader can tell a cut\n * the caller asked for from one a producer hint derived. Absent when the caller\n * built the input by hand and has no focus to attribute.\n */\n readonly focus?: ResolvedFocus;\n /** How *this* surface spells the producer knob: `--producer` for the CLI. */\n readonly producerKnob?: string;\n}\n\n/** Longest explanation printed in full before it gets an ellipsis. */\nconst EXPLANATION_WIDTH = 46;\n\n/**\n * The report, for stderr.\n *\n * Every total here is read straight off the {@link SmeltResult} — `inputBytes`,\n * `outputBytes`, `elisions.length`. The CLI keeps no counters of its own, because two\n * pieces of code counting the same bytes is how a report ends up disagreeing with the\n * library it is reporting on, and the report is the thing a human believes.\n * `test/cli.test.ts` asserts the printed numbers equal the result's fields.\n */\nexport function formatReport({\n result,\n source,\n budgetBytes,\n inputText,\n focus,\n producerKnob = '--producer',\n}: ReportInput): string {\n const lines: string[] = [];\n\n lines.push([CLI_NAME, source, result.language, result.planner].join(' '));\n lines.push(\n `in ${group(result.inputBytes)} B → out ${group(result.outputBytes)} B ` +\n `(${delta(result.inputBytes, result.outputBytes)}, ${count(result.elisions.length, 'elision')})`,\n );\n if (focus !== undefined && focus.terms.length > 0) {\n lines.push(\n `focus ${focus.terms.join(', ')}` +\n (focus.source === 'producer' ? ` (from ${producerKnob})` : ''),\n );\n }\n\n if (result.measured !== undefined) {\n const { input, output, unit, measure } = result.measured;\n lines.push(`in ${group(input)} → out ${group(output)} ${unit} (${measure})`);\n }\n\n if (result.outputBytes > budgetBytes) {\n lines.push('');\n lines.push(\n `OVER BUDGET ${group(result.outputBytes)} B against a ${group(budgetBytes)} B budget ` +\n `— over by ${group(result.outputBytes - budgetBytes)} B.`,\n );\n lines.push(' The plan is reported as it came back. smelt did not cut the regions');\n lines.push(' you asked to keep in order to make a number look right.');\n }\n\n if (result.elisions.length === 0) {\n lines.push('');\n lines.push(' nothing elided — the input already fits, or every run was too small to');\n lines.push(' be worth a marker (a marker that costs more than the lines it replaces');\n lines.push(' makes the output bigger).');\n return `${lines.join('\\n')}\\n`;\n }\n\n const rows = result.elisions.map((elision) => ({\n rule: elision.reason.rule,\n lines: String(lineSpan(inputText, elision.range.start, elision.range.end)),\n bytes: group(elision.bytes),\n hash: elision.hash,\n explanation: clip(elision.reason.explanation, EXPLANATION_WIDTH),\n names: elision.names ?? [],\n }));\n\n const ruleWidth = width(\n 'rule',\n rows.map((row) => row.rule),\n );\n const linesWidth = width(\n 'lines',\n rows.map((row) => row.lines),\n );\n const bytesWidth = width(\n 'bytes',\n rows.map((row) => row.bytes),\n );\n const hashWidth = width(\n 'hash',\n rows.map((row) => row.hash),\n );\n\n lines.push('');\n lines.push(\n ` ${'rule'.padEnd(ruleWidth)} ${'lines'.padStart(linesWidth)} ` +\n `${'bytes'.padStart(bytesWidth)} ${'hash'.padEnd(hashWidth)} explanation`,\n );\n for (const row of rows) {\n lines.push(\n ` ${row.rule.padEnd(ruleWidth)} ${row.lines.padStart(linesWidth)} ` +\n `${row.bytes.padStart(bytesWidth)} ${row.hash.padEnd(hashWidth)} ${row.explanation}`,\n );\n // The outline — what is behind this marker, by name — on its own wrapped lines\n // beneath the row. Never clipped: it is the index a reader (or a model deciding\n // whether to retrieve) needs whole, and Law 2's explanation is already the row.\n if (row.names.length > 0) {\n for (const wrapped of wrap(`${OUTLINE_LEADER} ${row.names.join(', ')}`, EXPLANATION_WRAP)) {\n lines.push(` ${wrapped}`);\n }\n }\n }\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** Introduces an elision's outline line. */\nconst OUTLINE_LEADER = '↳ names:';\n\n/** What `smelt map` prints to stderr. */\nexport interface MapReportInput {\n readonly map: RepoMap;\n /** The directory named on the command line, exactly as the user wrote it. */\n readonly source: string;\n /**\n * Where the budget came from — the `ResolvedMapRun.budgetSource` receipt, printed\n * beside the budget so a surprising number can be traced to the flag or the config\n * file that set it without re-deriving the precedence by hand.\n */\n readonly budgetSource: 'flag' | 'config';\n}\n\n/**\n * The map report, for stderr — same law as {@link formatReport}: every number is\n * read straight off the {@link RepoMap} the library returned. In particular the\n * \"bytes used\" figure is `map.outputBytes`, which the library measured off the\n * rendered text — the CLI counts nothing itself, because a report that keeps its\n * own tally is a report that can disagree with the map it describes.\n * `test/guards/repo-map.test.ts` asserts the printed figure equals the actual byte\n * length of what landed on stdout, and a mutation proves the assertion can go red.\n */\nexport function formatMapReport({ map, source, budgetSource }: MapReportInput): string {\n const lines: string[] = [];\n\n lines.push([`${CLI_NAME} map`, source, map.id].join(' '));\n lines.push(\n `files scanned ${group(map.filesScanned)}` +\n (map.binarySkipped === 0 ? '' : ` (${count(map.binarySkipped, 'binary file')} skipped)`) +\n ` symbols ranked ${group(map.definitionsTotal)}`,\n );\n lines.push(\n `included ${group(map.entries.length)} of ${group(map.definitionsTotal)} symbols` +\n (map.pathOnlyTotal === 0\n ? ''\n : ` + ${group(map.pathOnly.length)} of ${group(map.pathOnlyTotal)} path-only files`),\n );\n lines.push(\n `bytes used ${group(map.outputBytes)} of ${group(map.budgetBytes)} budget (${budgetSource}) ` +\n `— the map fits itself to the budget by construction, so there is no over-budget exit`,\n );\n\n if (map.cache !== undefined) {\n lines.push(\n `cache ${count(map.cache.hits, 'hit')}, ${count(map.cache.misses, 'miss', 'es')}, ` +\n `${group(map.cache.discarded)} discarded, ${group(map.cache.pruned)} pruned`,\n );\n }\n for (const warning of map.warnings) {\n lines.push(`warning ${warning.rule}: ${warning.explanation}`);\n }\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** What `smelt agents lint` prints. */\nexport interface AgentsReportInput {\n /** The directory named on the command line, exactly as the user wrote it. */\n readonly source: string;\n /** Whether `--strict` was given — it changes what the closing line promises. */\n readonly strict: boolean;\n}\n\n/**\n * The lint report — and unlike the other two, it goes to **stdout**, because here the\n * report *is* the output. `smelt` and `smelt map` put a payload on stdout and their\n * report on stderr so the two can be piped apart; a lint has no payload to separate\n * from, and sending its only output to stderr would make `smelt agents lint > audit.txt`\n * write an empty file.\n *\n * Same law as the other two, though: every number is read off the\n * {@link AgentsLintReport} the library returned. The renderer counts nothing, so it\n * cannot disagree with what was measured — and in particular both totals are the ones\n * the lint computed over the levels, not a second tally over the printed rows. The\n * guide's cited figure is likewise read from `agents/guide.ts` rather than retyped\n * under the guide's name.\n */\nexport function formatAgentsReport(\n report: AgentsLintReport,\n { source, strict }: AgentsReportInput,\n): string {\n const lines: string[] = [`${CLI_NAME} agents lint ${source}`];\n\n if (report.levels.length === 0) {\n lines.push('');\n lines.push(` no AGENTS.md, CLAUDE.md or GEMINI.md under ${source} — nothing is loaded on`);\n lines.push(' every request, so there is nothing to measure. That is a fine state, not a');\n lines.push(` failure; \\`${CLI_NAME} agents lint\\` has no opinion about whether you want one.`);\n return `${lines.join('\\n')}\\n`;\n }\n\n lines.push('');\n lines.push(' the instruction files in this tree');\n const labelWidth = report.levels.reduce(\n (widest, level) =>\n Math.max(widest, level.path.length, ...level.mirrors.map((m) => m.path.length)),\n Math.max(IMPERATIVES_LABEL.length, PER_REQUEST_LABEL.length, WHOLE_TREE_LABEL.length),\n );\n for (const level of report.levels) {\n lines.push(` ${level.path.padEnd(labelWidth)} ${group(level.bytes).padStart(9)} B`);\n for (const mirror of level.mirrors) {\n lines.push(\n ` ${mirror.path.padEnd(labelWidth)} ${' '.repeat(11)}${mirrorNote(mirror.standing)}`,\n );\n }\n }\n lines.push(\n ` ${PER_REQUEST_LABEL.padEnd(labelWidth)} ${group(report.perRequestBytes).padStart(9)} B`,\n );\n // Printed only when it is a different number — which is exactly when a reader could\n // otherwise mistake the sum for a per-request cost. In a single-chain repository the\n // two are equal and a second row would be noise claiming to be information.\n if (report.totalBytes !== report.perRequestBytes) {\n lines.push(\n ` ${WHOLE_TREE_LABEL.padEnd(labelWidth)} ${group(report.totalBytes).padStart(9)} B`,\n );\n lines.push(' a nested file merges with its ancestors, never with its siblings, so no');\n lines.push(' one request loads the whole tree. Per request is the heaviest chain.');\n }\n lines.push(\n ` ${IMPERATIVES_LABEL.padEnd(labelWidth)} ` +\n `${group(report.imperatives.length).padStart(9)}`,\n );\n // The figure is the guide's, so it is read from where the guide is quoted rather\n // than retyped here: an explanation that paraphrases its source drifts from it\n // silently, and this one is printed under the guide's own name.\n for (const wrapped of wrap(`${GUIDE_TITLE} cites: \"${GUIDE.instructionCeiling}\".`, 68)) {\n lines.push(` ${wrapped}`);\n }\n lines.push(' Printed as a citation, compared to nothing: the only ceiling here is');\n lines.push(' the one you set.');\n\n const over = overBudgetBytes(report);\n lines.push('');\n if (report.budgetBytes === undefined) {\n lines.push(` no budget set — add {\"agents\":{\"budgetBytes\":N}} to ${CONFIG_FILE_NAME} and`);\n lines.push(` exceeding it exits 1. ${CLI_NAME} will never invent that number for you.`);\n } else if (over === undefined) {\n lines.push(\n ` within budget ${group(report.totalBytes)} B of ${group(report.budgetBytes)} B ` +\n `(${CONFIG_FILE_NAME}: agents.budgetBytes, against the whole tree).`,\n );\n } else {\n lines.push(\n ` OVER BUDGET ${group(report.totalBytes)} B against your ${group(report.budgetBytes)} B ` +\n `budget — over by ${group(over)} B.`,\n );\n lines.push(` The budget is yours, from ${CONFIG_FILE_NAME}, and it caps the`);\n lines.push(' whole tree rather than one request — the stricter of the two, so');\n lines.push(' it cannot be met by moving bytes into another package. Exit 1 is');\n lines.push(' the same over-budget code every other smelt run uses.');\n }\n\n if (report.findings.length === 0) {\n lines.push('');\n lines.push(' no findings. Eight advisory rules ran and none matched — either the file is');\n lines.push(' in good shape, or a rule is asleep. `pnpm mutate` is how this repo tells the');\n lines.push(' difference about its own guards; a fixture per rule is how it tells it here.');\n return `${lines.join('\\n')}\\n`;\n }\n\n const rows = report.findings.map((finding) => ({\n place: `${finding.file}:${String(finding.line)}`,\n rule: finding.reason.rule,\n explanation: finding.reason.explanation,\n }));\n const ruleWidth = width(\n '',\n rows.map((row) => row.rule),\n );\n\n lines.push('');\n for (const row of rows) {\n lines.push(` ${row.rule.padEnd(ruleWidth)} ${row.place}`);\n // The explanation gets its own wrapped lines rather than a fourth column. It is\n // the *reason*, which is the part a reader actually has to read — clipped to a\n // terminal column it becomes an ellipsis, and Law 2 promises an explanation, not\n // the first 46 characters of one.\n for (const wrapped of wrap(row.explanation, EXPLANATION_WRAP)) lines.push(` ${wrapped}`);\n }\n\n lines.push('');\n lines.push(\n ` ${count(report.findings.length, 'finding')}. ` +\n (strict\n ? 'Exit 1: --strict was given.'\n : `Advisory — exit 0. Pass --strict to fail a CI run on any of them.`),\n );\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** The label the imperative count is reported under. Never \"instructions\": R6. */\nconst IMPERATIVES_LABEL = 'imperatives (heuristic)';\n\n/** The heaviest ancestor chain — the honest answer to \"what does a request cost\". */\nconst PER_REQUEST_LABEL = 'per request (worst case)';\n\n/** Every level summed. The repository's instruction surface, and not a request's cost. */\nconst WHOLE_TREE_LABEL = 'whole tree';\n\n/** Where a finding's explanation wraps, once its six-column indent is removed. */\nconst EXPLANATION_WRAP = 84;\n\n/** Greedy word wrap. No ICU, no dependency — the output is identical everywhere. */\nfunction wrap(text: string, columns: number): readonly string[] {\n const out: string[] = [];\n let line = '';\n for (const word of text.split(' ')) {\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > columns) {\n out.push(line);\n line = word;\n } else {\n line = candidate;\n }\n }\n if (line !== '') out.push(line);\n return out;\n}\n\n/** How a mirror stands beside its AGENTS.md, in one clause. */\nfunction mirrorNote(standing: AgentsMirrorReport['standing']): string {\n switch (standing) {\n case 'symlink':\n return 'symlink — cannot drift, and costs no extra bytes';\n case 'copy':\n return 'copy, byte-identical today — a symlink could not drift';\n case 'drift':\n return 'DIVERGED — see mirror-drift below';\n }\n}\n\n/** How many lines a byte range covers in the input. Derived, never tallied separately. */ function lineSpan(\n text: string,\n start: number,\n end: number,\n): number {\n const slice = Buffer.from(text, 'utf8').subarray(start, end).toString('utf8');\n return slice.split('\\n').length;\n}\n\nfunction width(header: string, values: readonly string[]): number {\n return values.reduce((widest, value) => Math.max(widest, value.length), header.length);\n}\n\n/** Thousands separators without pulling in ICU, so the output is identical everywhere. */\nfunction group(n: number): string {\n const digits = String(Math.abs(Math.trunc(n)));\n const grouped = digits.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n return n < 0 ? `-${grouped}` : grouped;\n}\n\nfunction delta(inputBytes: number, outputBytes: number): string {\n if (inputBytes === 0) return 'empty input';\n const percent = ((outputBytes - inputBytes) / inputBytes) * 100;\n const sign = percent > 0 ? '+' : '';\n return `${sign}${percent.toFixed(1)}%`;\n}\n\nfunction count(n: number, noun: string, pluralSuffix = 's'): string {\n return `${group(n)} ${noun}${n === 1 ? '' : pluralSuffix}`;\n}\n\nfunction clip(text: string, max: number): string {\n return text.length <= max ? text : `${text.slice(0, max - 1)}…`;\n}\n"]}
|
package/dist/cli/report.js
CHANGED
|
@@ -13,11 +13,15 @@ const EXPLANATION_WIDTH = 46;
|
|
|
13
13
|
* library it is reporting on, and the report is the thing a human believes.
|
|
14
14
|
* `test/cli.test.ts` asserts the printed numbers equal the result's fields.
|
|
15
15
|
*/
|
|
16
|
-
export function formatReport({ result, source, budgetBytes, inputText }) {
|
|
16
|
+
export function formatReport({ result, source, budgetBytes, inputText, focus, producerKnob = '--producer', }) {
|
|
17
17
|
const lines = [];
|
|
18
18
|
lines.push([CLI_NAME, source, result.language, result.planner].join(' '));
|
|
19
19
|
lines.push(`in ${group(result.inputBytes)} B → out ${group(result.outputBytes)} B ` +
|
|
20
20
|
`(${delta(result.inputBytes, result.outputBytes)}, ${count(result.elisions.length, 'elision')})`);
|
|
21
|
+
if (focus !== undefined && focus.terms.length > 0) {
|
|
22
|
+
lines.push(`focus ${focus.terms.join(', ')}` +
|
|
23
|
+
(focus.source === 'producer' ? ` (from ${producerKnob})` : ''));
|
|
24
|
+
}
|
|
21
25
|
if (result.measured !== undefined) {
|
|
22
26
|
const { input, output, unit, measure } = result.measured;
|
|
23
27
|
lines.push(`in ${group(input)} → out ${group(output)} ${unit} (${measure})`);
|
|
@@ -42,6 +46,7 @@ export function formatReport({ result, source, budgetBytes, inputText }) {
|
|
|
42
46
|
bytes: group(elision.bytes),
|
|
43
47
|
hash: elision.hash,
|
|
44
48
|
explanation: clip(elision.reason.explanation, EXPLANATION_WIDTH),
|
|
49
|
+
names: elision.names ?? [],
|
|
45
50
|
}));
|
|
46
51
|
const ruleWidth = width('rule', rows.map((row) => row.rule));
|
|
47
52
|
const linesWidth = width('lines', rows.map((row) => row.lines));
|
|
@@ -53,9 +58,19 @@ export function formatReport({ result, source, budgetBytes, inputText }) {
|
|
|
53
58
|
for (const row of rows) {
|
|
54
59
|
lines.push(` ${row.rule.padEnd(ruleWidth)} ${row.lines.padStart(linesWidth)} ` +
|
|
55
60
|
`${row.bytes.padStart(bytesWidth)} ${row.hash.padEnd(hashWidth)} ${row.explanation}`);
|
|
61
|
+
// The outline — what is behind this marker, by name — on its own wrapped lines
|
|
62
|
+
// beneath the row. Never clipped: it is the index a reader (or a model deciding
|
|
63
|
+
// whether to retrieve) needs whole, and Law 2's explanation is already the row.
|
|
64
|
+
if (row.names.length > 0) {
|
|
65
|
+
for (const wrapped of wrap(`${OUTLINE_LEADER} ${row.names.join(', ')}`, EXPLANATION_WRAP)) {
|
|
66
|
+
lines.push(` ${wrapped}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
}
|
|
57
70
|
return `${lines.join('\n')}\n`;
|
|
58
71
|
}
|
|
72
|
+
/** Introduces an elision's outline line. */
|
|
73
|
+
const OUTLINE_LEADER = '↳ names:';
|
|
59
74
|
/**
|
|
60
75
|
* The map report, for stderr — same law as {@link formatReport}: every number is
|
|
61
76
|
* read straight off the {@link RepoMap} the library returned. In particular the
|
package/dist/cli/report.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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","sourcesContent":["import { GUIDE, GUIDE_TITLE } from '../agents/guide.ts';\nimport { overBudgetBytes } from '../agents/lint.ts';\nimport type { AgentsLintReport, AgentsMirrorReport } from '../agents/lint.ts';\nimport type { RepoMap } from '../repomap/map.ts';\nimport type { SmeltResult } from '../types.ts';\n\nimport { CONFIG_FILE_NAME } from './config.ts';\nimport { CLI_NAME } from './shell.ts';\n\nexport interface ReportInput {\n readonly result: SmeltResult;\n /** What to call the input in the header: a path, or `'<stdin>'`. */\n readonly source: string;\n /** The budget the caller asked for, so the report can say when it was missed. */\n readonly budgetBytes: number;\n /** The exact text that was smelted. Used only to count lines inside each range. */\n readonly inputText: string;\n}\n\n/** Longest explanation printed in full before it gets an ellipsis. */\nconst EXPLANATION_WIDTH = 46;\n\n/**\n * The report, for stderr.\n *\n * Every total here is read straight off the {@link SmeltResult} — `inputBytes`,\n * `outputBytes`, `elisions.length`. The CLI keeps no counters of its own, because two\n * pieces of code counting the same bytes is how a report ends up disagreeing with the\n * library it is reporting on, and the report is the thing a human believes.\n * `test/cli.test.ts` asserts the printed numbers equal the result's fields.\n */\nexport function formatReport({ result, source, budgetBytes, inputText }: ReportInput): string {\n const lines: string[] = [];\n\n lines.push([CLI_NAME, source, result.language, result.planner].join(' '));\n lines.push(\n `in ${group(result.inputBytes)} B → out ${group(result.outputBytes)} B ` +\n `(${delta(result.inputBytes, result.outputBytes)}, ${count(result.elisions.length, 'elision')})`,\n );\n\n if (result.measured !== undefined) {\n const { input, output, unit, measure } = result.measured;\n lines.push(`in ${group(input)} → out ${group(output)} ${unit} (${measure})`);\n }\n\n if (result.outputBytes > budgetBytes) {\n lines.push('');\n lines.push(\n `OVER BUDGET ${group(result.outputBytes)} B against a ${group(budgetBytes)} B budget ` +\n `— over by ${group(result.outputBytes - budgetBytes)} B.`,\n );\n lines.push(' The plan is reported as it came back. smelt did not cut the regions');\n lines.push(' you asked to keep in order to make a number look right.');\n }\n\n if (result.elisions.length === 0) {\n lines.push('');\n lines.push(' nothing elided — the input already fits, or every run was too small to');\n lines.push(' be worth a marker (a marker that costs more than the lines it replaces');\n lines.push(' makes the output bigger).');\n return `${lines.join('\\n')}\\n`;\n }\n\n const rows = result.elisions.map((elision) => ({\n rule: elision.reason.rule,\n lines: String(lineSpan(inputText, elision.range.start, elision.range.end)),\n bytes: group(elision.bytes),\n hash: elision.hash,\n explanation: clip(elision.reason.explanation, EXPLANATION_WIDTH),\n }));\n\n const ruleWidth = width(\n 'rule',\n rows.map((row) => row.rule),\n );\n const linesWidth = width(\n 'lines',\n rows.map((row) => row.lines),\n );\n const bytesWidth = width(\n 'bytes',\n rows.map((row) => row.bytes),\n );\n const hashWidth = width(\n 'hash',\n rows.map((row) => row.hash),\n );\n\n lines.push('');\n lines.push(\n ` ${'rule'.padEnd(ruleWidth)} ${'lines'.padStart(linesWidth)} ` +\n `${'bytes'.padStart(bytesWidth)} ${'hash'.padEnd(hashWidth)} explanation`,\n );\n for (const row of rows) {\n lines.push(\n ` ${row.rule.padEnd(ruleWidth)} ${row.lines.padStart(linesWidth)} ` +\n `${row.bytes.padStart(bytesWidth)} ${row.hash.padEnd(hashWidth)} ${row.explanation}`,\n );\n }\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** What `smelt map` prints to stderr. */\nexport interface MapReportInput {\n readonly map: RepoMap;\n /** The directory named on the command line, exactly as the user wrote it. */\n readonly source: string;\n /**\n * Where the budget came from — the `ResolvedMapRun.budgetSource` receipt, printed\n * beside the budget so a surprising number can be traced to the flag or the config\n * file that set it without re-deriving the precedence by hand.\n */\n readonly budgetSource: 'flag' | 'config';\n}\n\n/**\n * The map report, for stderr — same law as {@link formatReport}: every number is\n * read straight off the {@link RepoMap} the library returned. In particular the\n * \"bytes used\" figure is `map.outputBytes`, which the library measured off the\n * rendered text — the CLI counts nothing itself, because a report that keeps its\n * own tally is a report that can disagree with the map it describes.\n * `test/guards/repo-map.test.ts` asserts the printed figure equals the actual byte\n * length of what landed on stdout, and a mutation proves the assertion can go red.\n */\nexport function formatMapReport({ map, source, budgetSource }: MapReportInput): string {\n const lines: string[] = [];\n\n lines.push([`${CLI_NAME} map`, source, map.id].join(' '));\n lines.push(\n `files scanned ${group(map.filesScanned)}` +\n (map.binarySkipped === 0 ? '' : ` (${count(map.binarySkipped, 'binary file')} skipped)`) +\n ` symbols ranked ${group(map.definitionsTotal)}`,\n );\n lines.push(\n `included ${group(map.entries.length)} of ${group(map.definitionsTotal)} symbols` +\n (map.pathOnlyTotal === 0\n ? ''\n : ` + ${group(map.pathOnly.length)} of ${group(map.pathOnlyTotal)} path-only files`),\n );\n lines.push(\n `bytes used ${group(map.outputBytes)} of ${group(map.budgetBytes)} budget (${budgetSource}) ` +\n `— the map fits itself to the budget by construction, so there is no over-budget exit`,\n );\n\n if (map.cache !== undefined) {\n lines.push(\n `cache ${count(map.cache.hits, 'hit')}, ${count(map.cache.misses, 'miss', 'es')}, ` +\n `${group(map.cache.discarded)} discarded, ${group(map.cache.pruned)} pruned`,\n );\n }\n for (const warning of map.warnings) {\n lines.push(`warning ${warning.rule}: ${warning.explanation}`);\n }\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** What `smelt agents lint` prints. */\nexport interface AgentsReportInput {\n /** The directory named on the command line, exactly as the user wrote it. */\n readonly source: string;\n /** Whether `--strict` was given — it changes what the closing line promises. */\n readonly strict: boolean;\n}\n\n/**\n * The lint report — and unlike the other two, it goes to **stdout**, because here the\n * report *is* the output. `smelt` and `smelt map` put a payload on stdout and their\n * report on stderr so the two can be piped apart; a lint has no payload to separate\n * from, and sending its only output to stderr would make `smelt agents lint > audit.txt`\n * write an empty file.\n *\n * Same law as the other two, though: every number is read off the\n * {@link AgentsLintReport} the library returned. The renderer counts nothing, so it\n * cannot disagree with what was measured — and in particular both totals are the ones\n * the lint computed over the levels, not a second tally over the printed rows. The\n * guide's cited figure is likewise read from `agents/guide.ts` rather than retyped\n * under the guide's name.\n */\nexport function formatAgentsReport(\n report: AgentsLintReport,\n { source, strict }: AgentsReportInput,\n): string {\n const lines: string[] = [`${CLI_NAME} agents lint ${source}`];\n\n if (report.levels.length === 0) {\n lines.push('');\n lines.push(` no AGENTS.md, CLAUDE.md or GEMINI.md under ${source} — nothing is loaded on`);\n lines.push(' every request, so there is nothing to measure. That is a fine state, not a');\n lines.push(` failure; \\`${CLI_NAME} agents lint\\` has no opinion about whether you want one.`);\n return `${lines.join('\\n')}\\n`;\n }\n\n lines.push('');\n lines.push(' the instruction files in this tree');\n const labelWidth = report.levels.reduce(\n (widest, level) =>\n Math.max(widest, level.path.length, ...level.mirrors.map((m) => m.path.length)),\n Math.max(IMPERATIVES_LABEL.length, PER_REQUEST_LABEL.length, WHOLE_TREE_LABEL.length),\n );\n for (const level of report.levels) {\n lines.push(` ${level.path.padEnd(labelWidth)} ${group(level.bytes).padStart(9)} B`);\n for (const mirror of level.mirrors) {\n lines.push(\n ` ${mirror.path.padEnd(labelWidth)} ${' '.repeat(11)}${mirrorNote(mirror.standing)}`,\n );\n }\n }\n lines.push(\n ` ${PER_REQUEST_LABEL.padEnd(labelWidth)} ${group(report.perRequestBytes).padStart(9)} B`,\n );\n // Printed only when it is a different number — which is exactly when a reader could\n // otherwise mistake the sum for a per-request cost. In a single-chain repository the\n // two are equal and a second row would be noise claiming to be information.\n if (report.totalBytes !== report.perRequestBytes) {\n lines.push(\n ` ${WHOLE_TREE_LABEL.padEnd(labelWidth)} ${group(report.totalBytes).padStart(9)} B`,\n );\n lines.push(' a nested file merges with its ancestors, never with its siblings, so no');\n lines.push(' one request loads the whole tree. Per request is the heaviest chain.');\n }\n lines.push(\n ` ${IMPERATIVES_LABEL.padEnd(labelWidth)} ` +\n `${group(report.imperatives.length).padStart(9)}`,\n );\n // The figure is the guide's, so it is read from where the guide is quoted rather\n // than retyped here: an explanation that paraphrases its source drifts from it\n // silently, and this one is printed under the guide's own name.\n for (const wrapped of wrap(`${GUIDE_TITLE} cites: \"${GUIDE.instructionCeiling}\".`, 68)) {\n lines.push(` ${wrapped}`);\n }\n lines.push(' Printed as a citation, compared to nothing: the only ceiling here is');\n lines.push(' the one you set.');\n\n const over = overBudgetBytes(report);\n lines.push('');\n if (report.budgetBytes === undefined) {\n lines.push(` no budget set — add {\"agents\":{\"budgetBytes\":N}} to ${CONFIG_FILE_NAME} and`);\n lines.push(` exceeding it exits 1. ${CLI_NAME} will never invent that number for you.`);\n } else if (over === undefined) {\n lines.push(\n ` within budget ${group(report.totalBytes)} B of ${group(report.budgetBytes)} B ` +\n `(${CONFIG_FILE_NAME}: agents.budgetBytes, against the whole tree).`,\n );\n } else {\n lines.push(\n ` OVER BUDGET ${group(report.totalBytes)} B against your ${group(report.budgetBytes)} B ` +\n `budget — over by ${group(over)} B.`,\n );\n lines.push(` The budget is yours, from ${CONFIG_FILE_NAME}, and it caps the`);\n lines.push(' whole tree rather than one request — the stricter of the two, so');\n lines.push(' it cannot be met by moving bytes into another package. Exit 1 is');\n lines.push(' the same over-budget code every other smelt run uses.');\n }\n\n if (report.findings.length === 0) {\n lines.push('');\n lines.push(' no findings. Eight advisory rules ran and none matched — either the file is');\n lines.push(' in good shape, or a rule is asleep. `pnpm mutate` is how this repo tells the');\n lines.push(' difference about its own guards; a fixture per rule is how it tells it here.');\n return `${lines.join('\\n')}\\n`;\n }\n\n const rows = report.findings.map((finding) => ({\n place: `${finding.file}:${String(finding.line)}`,\n rule: finding.reason.rule,\n explanation: finding.reason.explanation,\n }));\n const ruleWidth = width(\n '',\n rows.map((row) => row.rule),\n );\n\n lines.push('');\n for (const row of rows) {\n lines.push(` ${row.rule.padEnd(ruleWidth)} ${row.place}`);\n // The explanation gets its own wrapped lines rather than a fourth column. It is\n // the *reason*, which is the part a reader actually has to read — clipped to a\n // terminal column it becomes an ellipsis, and Law 2 promises an explanation, not\n // the first 46 characters of one.\n for (const wrapped of wrap(row.explanation, EXPLANATION_WRAP)) lines.push(` ${wrapped}`);\n }\n\n lines.push('');\n lines.push(\n ` ${count(report.findings.length, 'finding')}. ` +\n (strict\n ? 'Exit 1: --strict was given.'\n : `Advisory — exit 0. Pass --strict to fail a CI run on any of them.`),\n );\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** The label the imperative count is reported under. Never \"instructions\": R6. */\nconst IMPERATIVES_LABEL = 'imperatives (heuristic)';\n\n/** The heaviest ancestor chain — the honest answer to \"what does a request cost\". */\nconst PER_REQUEST_LABEL = 'per request (worst case)';\n\n/** Every level summed. The repository's instruction surface, and not a request's cost. */\nconst WHOLE_TREE_LABEL = 'whole tree';\n\n/** Where a finding's explanation wraps, once its six-column indent is removed. */\nconst EXPLANATION_WRAP = 84;\n\n/** Greedy word wrap. No ICU, no dependency — the output is identical everywhere. */\nfunction wrap(text: string, columns: number): readonly string[] {\n const out: string[] = [];\n let line = '';\n for (const word of text.split(' ')) {\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > columns) {\n out.push(line);\n line = word;\n } else {\n line = candidate;\n }\n }\n if (line !== '') out.push(line);\n return out;\n}\n\n/** How a mirror stands beside its AGENTS.md, in one clause. */\nfunction mirrorNote(standing: AgentsMirrorReport['standing']): string {\n switch (standing) {\n case 'symlink':\n return 'symlink — cannot drift, and costs no extra bytes';\n case 'copy':\n return 'copy, byte-identical today — a symlink could not drift';\n case 'drift':\n return 'DIVERGED — see mirror-drift below';\n }\n}\n\n/** How many lines a byte range covers in the input. Derived, never tallied separately. */ function lineSpan(\n text: string,\n start: number,\n end: number,\n): number {\n const slice = Buffer.from(text, 'utf8').subarray(start, end).toString('utf8');\n return slice.split('\\n').length;\n}\n\nfunction width(header: string, values: readonly string[]): number {\n return values.reduce((widest, value) => Math.max(widest, value.length), header.length);\n}\n\n/** Thousands separators without pulling in ICU, so the output is identical everywhere. */\nfunction group(n: number): string {\n const digits = String(Math.abs(Math.trunc(n)));\n const grouped = digits.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n return n < 0 ? `-${grouped}` : grouped;\n}\n\nfunction delta(inputBytes: number, outputBytes: number): string {\n if (inputBytes === 0) return 'empty input';\n const percent = ((outputBytes - inputBytes) / inputBytes) * 100;\n const sign = percent > 0 ? '+' : '';\n return `${sign}${percent.toFixed(1)}%`;\n}\n\nfunction count(n: number, noun: string, pluralSuffix = 's'): string {\n return `${group(n)} ${noun}${n === 1 ? '' : pluralSuffix}`;\n}\n\nfunction clip(text: string, max: number): string {\n return text.length <= max ? text : `${text.slice(0, max - 1)}…`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAMpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAoBtC,sEAAsE;AACtE,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,MAAM,EACN,MAAM,EACN,WAAW,EACX,SAAS,EACT,KAAK,EACL,YAAY,GAAG,YAAY,GACf;IACZ,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3E,KAAK,CAAC,IAAI,CACR,MAAM,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO;QACxE,IAAI,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,WAAW,CAAC,KAAK,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,CACnG,CAAC;IACF,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,KAAK,CAAC,IAAI,CACR,UAAU,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YAChC,CAAC,KAAK,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,YAAY,YAAY,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CACnE,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC;QACzD,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,KAAK,CAAC,UAAU,KAAK,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,OAAO,GAAG,CAAC,CAAC;IAC/E,CAAC;IAED,IAAI,MAAM,CAAC,WAAW,GAAG,WAAW,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CACR,gBAAgB,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,gBAAgB,KAAK,CAAC,WAAW,CAAC,YAAY;YACrF,aAAa,KAAK,CAAC,MAAM,CAAC,WAAW,GAAG,WAAW,CAAC,KAAK,CAC5D,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,kFAAkF,CAAC,CAAC;QAC/F,KAAK,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;IACrF,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC;QACvF,KAAK,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC;QACvF,KAAK,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAC1C,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACjC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC7C,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;QACzB,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1E,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,iBAAiB,CAAC;QAChE,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,EAAE;KAC3B,CAAC,CAAC,CAAC;IAEJ,MAAM,SAAS,GAAG,KAAK,CACrB,MAAM,EACN,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAC5B,CAAC;IACF,MAAM,UAAU,GAAG,KAAK,CACtB,OAAO,EACP,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAC7B,CAAC;IACF,MAAM,UAAU,GAAG,KAAK,CACtB,OAAO,EACP,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAC7B,CAAC;IACF,MAAM,SAAS,GAAG,KAAK,CACrB,MAAM,EACN,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAC5B,CAAC;IAEF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACR,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI;QAChE,GAAG,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,eAAe,CAC9E,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CACR,KAAK,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI;YACpE,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,WAAW,EAAE,CACzF,CAAC;QACF,+EAA+E;QAC/E,gFAAgF;QAChF,gFAAgF;QAChF,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,GAAG,cAAc,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,gBAAgB,CAAC,EAAE,CAAC;gBAC1F,KAAK,CAAC,IAAI,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC;AAED,4CAA4C;AAC5C,MAAM,cAAc,GAAG,UAAU,CAAC;AAelC;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAkB;IAC3E,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,QAAQ,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3D,KAAK,CAAC,IAAI,CACR,iBAAiB,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE;QACxC,CAAC,GAAG,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,WAAW,CAAC;QACxF,qBAAqB,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CACrD,CAAC;IACF,KAAK,CAAC,IAAI,CACR,YAAY,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,UAAU;QAC/E,CAAC,GAAG,CAAC,aAAa,KAAK,CAAC;YACtB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,kBAAkB,CAAC,CACzF,CAAC;IACF,KAAK,CAAC,IAAI,CACR,cAAc,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,YAAY,YAAY,IAAI;QAC3F,sFAAsF,CACzF,CAAC;IAEF,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CACR,UAAU,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI;YAClF,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,eAAe,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAC/E,CAAC;IACJ,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,YAAY,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC;AAUD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAwB,EACxB,EAAE,MAAM,EAAE,MAAM,EAAqB;IAErC,MAAM,KAAK,GAAa,CAAC,GAAG,QAAQ,iBAAiB,MAAM,EAAE,CAAC,CAAC;IAE/D,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,gDAAgD,MAAM,yBAAyB,CAAC,CAAC;QAC5F,KAAK,CAAC,IAAI,CAAC,8EAA8E,CAAC,CAAC;QAC3F,KAAK,CAAC,IAAI,CAAC,gBAAgB,QAAQ,2DAA2D,CAAC,CAAC;QAChG,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IACnD,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CACrC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAChB,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EACjF,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,CAAC,MAAM,CAAC,CACtF,CAAC;IACF,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACxF,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YACnC,KAAK,CAAC,IAAI,CACR,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CACzF,CAAC;QACJ,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CACR,OAAO,iBAAiB,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,KAAK,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAC9F,CAAC;IACF,oFAAoF;IACpF,qFAAqF;IACrF,4EAA4E;IAC5E,IAAI,MAAM,CAAC,UAAU,KAAK,MAAM,CAAC,eAAe,EAAE,CAAC;QACjD,KAAK,CAAC,IAAI,CACR,OAAO,gBAAgB,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CACxF,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,6EAA6E,CAAC,CAAC;QAC1F,KAAK,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC;IACzF,CAAC;IACD,KAAK,CAAC,IAAI,CACR,OAAO,iBAAiB,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI;QAC7C,GAAG,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CACpD,CAAC;IACF,iFAAiF;IACjF,+EAA+E;IAC/E,gEAAgE;IAChE,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,GAAG,WAAW,YAAY,KAAK,CAAC,kBAAkB,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;QACvF,KAAK,CAAC,IAAI,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC;IAC/B,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC;IACvF,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IAEnC,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IACrC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,yDAAyD,gBAAgB,MAAM,CAAC,CAAC;QAC5F,KAAK,CAAC,IAAI,CAAC,2BAA2B,QAAQ,yCAAyC,CAAC,CAAC;IAC3F,CAAC;SAAM,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CACR,oBAAoB,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK;YACjF,IAAI,gBAAgB,gDAAgD,CACvE,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CACR,kBAAkB,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,mBAAmB,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK;YACzF,oBAAoB,KAAK,CAAC,IAAI,CAAC,KAAK,CACvC,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,4CAA4C,gBAAgB,mBAAmB,CAAC,CAAC;QAC5F,KAAK,CAAC,IAAI,CAAC,iFAAiF,CAAC,CAAC;QAC9F,KAAK,CAAC,IAAI,CAAC,iFAAiF,CAAC,CAAC;QAC9F,KAAK,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;IACrF,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;QAC5F,KAAK,CAAC,IAAI,CAAC,gFAAgF,CAAC,CAAC;QAC7F,KAAK,CAAC,IAAI,CAAC,gFAAgF,CAAC,CAAC;QAC7F,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACjC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC7C,KAAK,EAAE,GAAG,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;QAChD,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;QACzB,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW;KACxC,CAAC,CAAC,CAAC;IACJ,MAAM,SAAS,GAAG,KAAK,CACrB,EAAE,EACF,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAC5B,CAAC;IAEF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;QAC5D,gFAAgF;QAChF,+EAA+E;QAC/E,iFAAiF;QACjF,kCAAkC;QAClC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,gBAAgB,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;IAChG,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACR,KAAK,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI;QAC/C,CAAC,MAAM;YACL,CAAC,CAAC,6BAA6B;YAC/B,CAAC,CAAC,mEAAmE,CAAC,CAC3E,CAAC;IAEF,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC;AAED,kFAAkF;AAClF,MAAM,iBAAiB,GAAG,yBAAyB,CAAC;AAEpD,qFAAqF;AACrF,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;AAErD,0FAA0F;AAC1F,MAAM,gBAAgB,GAAG,YAAY,CAAC;AAEtC,kFAAkF;AAClF,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,oFAAoF;AACpF,SAAS,IAAI,CAAC,IAAY,EAAE,OAAe;IACzC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;QACzD,IAAI,IAAI,KAAK,EAAE,IAAI,SAAS,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;YAC9C,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACf,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;aAAM,CAAC;YACN,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,IAAI,IAAI,KAAK,EAAE;QAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,OAAO,GAAG,CAAC;AACb,CAAC;AAED,+DAA+D;AAC/D,SAAS,UAAU,CAAC,QAAwC;IAC1D,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,SAAS;YACZ,OAAO,kDAAkD,CAAC;QAC5D,KAAK,MAAM;YACT,OAAO,wDAAwD,CAAC;QAClE,KAAK,OAAO;YACV,OAAO,mCAAmC,CAAC;IAC/C,CAAC;AACH,CAAC;AAED,0FAA0F,CAAC,SAAS,QAAQ,CAC1G,IAAY,EACZ,KAAa,EACb,GAAW;IAEX,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9E,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;AAClC,CAAC;AAED,SAAS,KAAK,CAAC,MAAc,EAAE,MAAyB;IACtD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;AACzF,CAAC;AAED,0FAA0F;AAC1F,SAAS,KAAK,CAAC,CAAS;IACtB,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;AACzC,CAAC;AAED,SAAS,KAAK,CAAC,UAAkB,EAAE,WAAmB;IACpD,IAAI,UAAU,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC;IAC3C,MAAM,OAAO,GAAG,CAAC,CAAC,WAAW,GAAG,UAAU,CAAC,GAAG,UAAU,CAAC,GAAG,GAAG,CAAC;IAChE,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACpC,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;AACzC,CAAC;AAED,SAAS,KAAK,CAAC,CAAS,EAAE,IAAY,EAAE,YAAY,GAAG,GAAG;IACxD,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC;AAC7D,CAAC;AAED,SAAS,IAAI,CAAC,IAAY,EAAE,GAAW;IACrC,OAAO,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;AAClE,CAAC","sourcesContent":["import { GUIDE, GUIDE_TITLE } from '../agents/guide.ts';\nimport { overBudgetBytes } from '../agents/lint.ts';\nimport type { AgentsLintReport, AgentsMirrorReport } from '../agents/lint.ts';\nimport type { ResolvedFocus } from '../ops/verbs.ts';\nimport type { RepoMap } from '../repomap/map.ts';\nimport type { SmeltResult } from '../types.ts';\n\nimport { CONFIG_FILE_NAME } from './config.ts';\nimport { CLI_NAME } from './shell.ts';\n\nexport interface ReportInput {\n readonly result: SmeltResult;\n /** What to call the input in the header: a path, or `'<stdin>'`. */\n readonly source: string;\n /** The budget the caller asked for, so the report can say when it was missed. */\n readonly budgetBytes: number;\n /** The exact text that was smelted. Used only to count lines inside each range. */\n readonly inputText: string;\n /**\n * The focus the planner saw and whose it was — printed so a reader can tell a cut\n * the caller asked for from one a producer hint derived. Absent when the caller\n * built the input by hand and has no focus to attribute.\n */\n readonly focus?: ResolvedFocus;\n /** How *this* surface spells the producer knob: `--producer` for the CLI. */\n readonly producerKnob?: string;\n}\n\n/** Longest explanation printed in full before it gets an ellipsis. */\nconst EXPLANATION_WIDTH = 46;\n\n/**\n * The report, for stderr.\n *\n * Every total here is read straight off the {@link SmeltResult} — `inputBytes`,\n * `outputBytes`, `elisions.length`. The CLI keeps no counters of its own, because two\n * pieces of code counting the same bytes is how a report ends up disagreeing with the\n * library it is reporting on, and the report is the thing a human believes.\n * `test/cli.test.ts` asserts the printed numbers equal the result's fields.\n */\nexport function formatReport({\n result,\n source,\n budgetBytes,\n inputText,\n focus,\n producerKnob = '--producer',\n}: ReportInput): string {\n const lines: string[] = [];\n\n lines.push([CLI_NAME, source, result.language, result.planner].join(' '));\n lines.push(\n `in ${group(result.inputBytes)} B → out ${group(result.outputBytes)} B ` +\n `(${delta(result.inputBytes, result.outputBytes)}, ${count(result.elisions.length, 'elision')})`,\n );\n if (focus !== undefined && focus.terms.length > 0) {\n lines.push(\n `focus ${focus.terms.join(', ')}` +\n (focus.source === 'producer' ? ` (from ${producerKnob})` : ''),\n );\n }\n\n if (result.measured !== undefined) {\n const { input, output, unit, measure } = result.measured;\n lines.push(`in ${group(input)} → out ${group(output)} ${unit} (${measure})`);\n }\n\n if (result.outputBytes > budgetBytes) {\n lines.push('');\n lines.push(\n `OVER BUDGET ${group(result.outputBytes)} B against a ${group(budgetBytes)} B budget ` +\n `— over by ${group(result.outputBytes - budgetBytes)} B.`,\n );\n lines.push(' The plan is reported as it came back. smelt did not cut the regions');\n lines.push(' you asked to keep in order to make a number look right.');\n }\n\n if (result.elisions.length === 0) {\n lines.push('');\n lines.push(' nothing elided — the input already fits, or every run was too small to');\n lines.push(' be worth a marker (a marker that costs more than the lines it replaces');\n lines.push(' makes the output bigger).');\n return `${lines.join('\\n')}\\n`;\n }\n\n const rows = result.elisions.map((elision) => ({\n rule: elision.reason.rule,\n lines: String(lineSpan(inputText, elision.range.start, elision.range.end)),\n bytes: group(elision.bytes),\n hash: elision.hash,\n explanation: clip(elision.reason.explanation, EXPLANATION_WIDTH),\n names: elision.names ?? [],\n }));\n\n const ruleWidth = width(\n 'rule',\n rows.map((row) => row.rule),\n );\n const linesWidth = width(\n 'lines',\n rows.map((row) => row.lines),\n );\n const bytesWidth = width(\n 'bytes',\n rows.map((row) => row.bytes),\n );\n const hashWidth = width(\n 'hash',\n rows.map((row) => row.hash),\n );\n\n lines.push('');\n lines.push(\n ` ${'rule'.padEnd(ruleWidth)} ${'lines'.padStart(linesWidth)} ` +\n `${'bytes'.padStart(bytesWidth)} ${'hash'.padEnd(hashWidth)} explanation`,\n );\n for (const row of rows) {\n lines.push(\n ` ${row.rule.padEnd(ruleWidth)} ${row.lines.padStart(linesWidth)} ` +\n `${row.bytes.padStart(bytesWidth)} ${row.hash.padEnd(hashWidth)} ${row.explanation}`,\n );\n // The outline — what is behind this marker, by name — on its own wrapped lines\n // beneath the row. Never clipped: it is the index a reader (or a model deciding\n // whether to retrieve) needs whole, and Law 2's explanation is already the row.\n if (row.names.length > 0) {\n for (const wrapped of wrap(`${OUTLINE_LEADER} ${row.names.join(', ')}`, EXPLANATION_WRAP)) {\n lines.push(` ${wrapped}`);\n }\n }\n }\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** Introduces an elision's outline line. */\nconst OUTLINE_LEADER = '↳ names:';\n\n/** What `smelt map` prints to stderr. */\nexport interface MapReportInput {\n readonly map: RepoMap;\n /** The directory named on the command line, exactly as the user wrote it. */\n readonly source: string;\n /**\n * Where the budget came from — the `ResolvedMapRun.budgetSource` receipt, printed\n * beside the budget so a surprising number can be traced to the flag or the config\n * file that set it without re-deriving the precedence by hand.\n */\n readonly budgetSource: 'flag' | 'config';\n}\n\n/**\n * The map report, for stderr — same law as {@link formatReport}: every number is\n * read straight off the {@link RepoMap} the library returned. In particular the\n * \"bytes used\" figure is `map.outputBytes`, which the library measured off the\n * rendered text — the CLI counts nothing itself, because a report that keeps its\n * own tally is a report that can disagree with the map it describes.\n * `test/guards/repo-map.test.ts` asserts the printed figure equals the actual byte\n * length of what landed on stdout, and a mutation proves the assertion can go red.\n */\nexport function formatMapReport({ map, source, budgetSource }: MapReportInput): string {\n const lines: string[] = [];\n\n lines.push([`${CLI_NAME} map`, source, map.id].join(' '));\n lines.push(\n `files scanned ${group(map.filesScanned)}` +\n (map.binarySkipped === 0 ? '' : ` (${count(map.binarySkipped, 'binary file')} skipped)`) +\n ` symbols ranked ${group(map.definitionsTotal)}`,\n );\n lines.push(\n `included ${group(map.entries.length)} of ${group(map.definitionsTotal)} symbols` +\n (map.pathOnlyTotal === 0\n ? ''\n : ` + ${group(map.pathOnly.length)} of ${group(map.pathOnlyTotal)} path-only files`),\n );\n lines.push(\n `bytes used ${group(map.outputBytes)} of ${group(map.budgetBytes)} budget (${budgetSource}) ` +\n `— the map fits itself to the budget by construction, so there is no over-budget exit`,\n );\n\n if (map.cache !== undefined) {\n lines.push(\n `cache ${count(map.cache.hits, 'hit')}, ${count(map.cache.misses, 'miss', 'es')}, ` +\n `${group(map.cache.discarded)} discarded, ${group(map.cache.pruned)} pruned`,\n );\n }\n for (const warning of map.warnings) {\n lines.push(`warning ${warning.rule}: ${warning.explanation}`);\n }\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** What `smelt agents lint` prints. */\nexport interface AgentsReportInput {\n /** The directory named on the command line, exactly as the user wrote it. */\n readonly source: string;\n /** Whether `--strict` was given — it changes what the closing line promises. */\n readonly strict: boolean;\n}\n\n/**\n * The lint report — and unlike the other two, it goes to **stdout**, because here the\n * report *is* the output. `smelt` and `smelt map` put a payload on stdout and their\n * report on stderr so the two can be piped apart; a lint has no payload to separate\n * from, and sending its only output to stderr would make `smelt agents lint > audit.txt`\n * write an empty file.\n *\n * Same law as the other two, though: every number is read off the\n * {@link AgentsLintReport} the library returned. The renderer counts nothing, so it\n * cannot disagree with what was measured — and in particular both totals are the ones\n * the lint computed over the levels, not a second tally over the printed rows. The\n * guide's cited figure is likewise read from `agents/guide.ts` rather than retyped\n * under the guide's name.\n */\nexport function formatAgentsReport(\n report: AgentsLintReport,\n { source, strict }: AgentsReportInput,\n): string {\n const lines: string[] = [`${CLI_NAME} agents lint ${source}`];\n\n if (report.levels.length === 0) {\n lines.push('');\n lines.push(` no AGENTS.md, CLAUDE.md or GEMINI.md under ${source} — nothing is loaded on`);\n lines.push(' every request, so there is nothing to measure. That is a fine state, not a');\n lines.push(` failure; \\`${CLI_NAME} agents lint\\` has no opinion about whether you want one.`);\n return `${lines.join('\\n')}\\n`;\n }\n\n lines.push('');\n lines.push(' the instruction files in this tree');\n const labelWidth = report.levels.reduce(\n (widest, level) =>\n Math.max(widest, level.path.length, ...level.mirrors.map((m) => m.path.length)),\n Math.max(IMPERATIVES_LABEL.length, PER_REQUEST_LABEL.length, WHOLE_TREE_LABEL.length),\n );\n for (const level of report.levels) {\n lines.push(` ${level.path.padEnd(labelWidth)} ${group(level.bytes).padStart(9)} B`);\n for (const mirror of level.mirrors) {\n lines.push(\n ` ${mirror.path.padEnd(labelWidth)} ${' '.repeat(11)}${mirrorNote(mirror.standing)}`,\n );\n }\n }\n lines.push(\n ` ${PER_REQUEST_LABEL.padEnd(labelWidth)} ${group(report.perRequestBytes).padStart(9)} B`,\n );\n // Printed only when it is a different number — which is exactly when a reader could\n // otherwise mistake the sum for a per-request cost. In a single-chain repository the\n // two are equal and a second row would be noise claiming to be information.\n if (report.totalBytes !== report.perRequestBytes) {\n lines.push(\n ` ${WHOLE_TREE_LABEL.padEnd(labelWidth)} ${group(report.totalBytes).padStart(9)} B`,\n );\n lines.push(' a nested file merges with its ancestors, never with its siblings, so no');\n lines.push(' one request loads the whole tree. Per request is the heaviest chain.');\n }\n lines.push(\n ` ${IMPERATIVES_LABEL.padEnd(labelWidth)} ` +\n `${group(report.imperatives.length).padStart(9)}`,\n );\n // The figure is the guide's, so it is read from where the guide is quoted rather\n // than retyped here: an explanation that paraphrases its source drifts from it\n // silently, and this one is printed under the guide's own name.\n for (const wrapped of wrap(`${GUIDE_TITLE} cites: \"${GUIDE.instructionCeiling}\".`, 68)) {\n lines.push(` ${wrapped}`);\n }\n lines.push(' Printed as a citation, compared to nothing: the only ceiling here is');\n lines.push(' the one you set.');\n\n const over = overBudgetBytes(report);\n lines.push('');\n if (report.budgetBytes === undefined) {\n lines.push(` no budget set — add {\"agents\":{\"budgetBytes\":N}} to ${CONFIG_FILE_NAME} and`);\n lines.push(` exceeding it exits 1. ${CLI_NAME} will never invent that number for you.`);\n } else if (over === undefined) {\n lines.push(\n ` within budget ${group(report.totalBytes)} B of ${group(report.budgetBytes)} B ` +\n `(${CONFIG_FILE_NAME}: agents.budgetBytes, against the whole tree).`,\n );\n } else {\n lines.push(\n ` OVER BUDGET ${group(report.totalBytes)} B against your ${group(report.budgetBytes)} B ` +\n `budget — over by ${group(over)} B.`,\n );\n lines.push(` The budget is yours, from ${CONFIG_FILE_NAME}, and it caps the`);\n lines.push(' whole tree rather than one request — the stricter of the two, so');\n lines.push(' it cannot be met by moving bytes into another package. Exit 1 is');\n lines.push(' the same over-budget code every other smelt run uses.');\n }\n\n if (report.findings.length === 0) {\n lines.push('');\n lines.push(' no findings. Eight advisory rules ran and none matched — either the file is');\n lines.push(' in good shape, or a rule is asleep. `pnpm mutate` is how this repo tells the');\n lines.push(' difference about its own guards; a fixture per rule is how it tells it here.');\n return `${lines.join('\\n')}\\n`;\n }\n\n const rows = report.findings.map((finding) => ({\n place: `${finding.file}:${String(finding.line)}`,\n rule: finding.reason.rule,\n explanation: finding.reason.explanation,\n }));\n const ruleWidth = width(\n '',\n rows.map((row) => row.rule),\n );\n\n lines.push('');\n for (const row of rows) {\n lines.push(` ${row.rule.padEnd(ruleWidth)} ${row.place}`);\n // The explanation gets its own wrapped lines rather than a fourth column. It is\n // the *reason*, which is the part a reader actually has to read — clipped to a\n // terminal column it becomes an ellipsis, and Law 2 promises an explanation, not\n // the first 46 characters of one.\n for (const wrapped of wrap(row.explanation, EXPLANATION_WRAP)) lines.push(` ${wrapped}`);\n }\n\n lines.push('');\n lines.push(\n ` ${count(report.findings.length, 'finding')}. ` +\n (strict\n ? 'Exit 1: --strict was given.'\n : `Advisory — exit 0. Pass --strict to fail a CI run on any of them.`),\n );\n\n return `${lines.join('\\n')}\\n`;\n}\n\n/** The label the imperative count is reported under. Never \"instructions\": R6. */\nconst IMPERATIVES_LABEL = 'imperatives (heuristic)';\n\n/** The heaviest ancestor chain — the honest answer to \"what does a request cost\". */\nconst PER_REQUEST_LABEL = 'per request (worst case)';\n\n/** Every level summed. The repository's instruction surface, and not a request's cost. */\nconst WHOLE_TREE_LABEL = 'whole tree';\n\n/** Where a finding's explanation wraps, once its six-column indent is removed. */\nconst EXPLANATION_WRAP = 84;\n\n/** Greedy word wrap. No ICU, no dependency — the output is identical everywhere. */\nfunction wrap(text: string, columns: number): readonly string[] {\n const out: string[] = [];\n let line = '';\n for (const word of text.split(' ')) {\n const candidate = line === '' ? word : `${line} ${word}`;\n if (line !== '' && candidate.length > columns) {\n out.push(line);\n line = word;\n } else {\n line = candidate;\n }\n }\n if (line !== '') out.push(line);\n return out;\n}\n\n/** How a mirror stands beside its AGENTS.md, in one clause. */\nfunction mirrorNote(standing: AgentsMirrorReport['standing']): string {\n switch (standing) {\n case 'symlink':\n return 'symlink — cannot drift, and costs no extra bytes';\n case 'copy':\n return 'copy, byte-identical today — a symlink could not drift';\n case 'drift':\n return 'DIVERGED — see mirror-drift below';\n }\n}\n\n/** How many lines a byte range covers in the input. Derived, never tallied separately. */ function lineSpan(\n text: string,\n start: number,\n end: number,\n): number {\n const slice = Buffer.from(text, 'utf8').subarray(start, end).toString('utf8');\n return slice.split('\\n').length;\n}\n\nfunction width(header: string, values: readonly string[]): number {\n return values.reduce((widest, value) => Math.max(widest, value.length), header.length);\n}\n\n/** Thousands separators without pulling in ICU, so the output is identical everywhere. */\nfunction group(n: number): string {\n const digits = String(Math.abs(Math.trunc(n)));\n const grouped = digits.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n return n < 0 ? `-${grouped}` : grouped;\n}\n\nfunction delta(inputBytes: number, outputBytes: number): string {\n if (inputBytes === 0) return 'empty input';\n const percent = ((outputBytes - inputBytes) / inputBytes) * 100;\n const sign = percent > 0 ? '+' : '';\n return `${sign}${percent.toFixed(1)}%`;\n}\n\nfunction count(n: number, noun: string, pluralSuffix = 's'): string {\n return `${group(n)} ${noun}${n === 1 ? '' : pluralSuffix}`;\n}\n\nfunction clip(text: string, max: number): string {\n return text.length <= max ? text : `${text.slice(0, max - 1)}…`;\n}\n"]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { AnswerStream } from './shell.ts';
|
|
2
|
+
/**
|
|
3
|
+
* `smelt setup` — the SetupRecipe (CONTEXT.md) applied end-to-end: config, the hooks
|
|
4
|
+
* preset, the MCP registration step, and a real smelt → retrieve round trip to prove
|
|
5
|
+
* the loop. The verb is `subcommands/setup.ts`; this file is the flow, a pure function
|
|
6
|
+
* over an injected input/output pair — the `init`/`hooks` discipline, so the wizard is
|
|
7
|
+
* guard-tested in-process and a renderer (KOT-253) slots in behind the same stream.
|
|
8
|
+
*
|
|
9
|
+
* The two paths share one apply path, because two apply paths would drift:
|
|
10
|
+
*
|
|
11
|
+
* - `--yes` answers everything from the recipe: the budget it recommends (printed
|
|
12
|
+
* loudly, written only when the config lacks one — the `smelt` verb's own
|
|
13
|
+
* budget-required refusal is untouched), a directory store at the recipe's path
|
|
14
|
+
* when the config carries none, and the hooks preset's currently-installed
|
|
15
|
+
* defaults, read the same way `smelt hooks install` reads them.
|
|
16
|
+
* - interactive asks four questions, each with an Enter default, then confirms.
|
|
17
|
+
*
|
|
18
|
+
* The one hard rule is inherited unchanged from `init` and `hooks`: an existing file
|
|
19
|
+
* that is not smelt's own config is never written — not by `--yes`, not by a wizard
|
|
20
|
+
* answer. It is skipped with a note pointing at `smelt hooks install`, which asks per
|
|
21
|
+
* file. `smelt.config.json` is smelt's own file; `setup` updates it and says exactly
|
|
22
|
+
* what it added.
|
|
23
|
+
*
|
|
24
|
+
* Idempotent by construction: a re-run on a current machine plans `unchanged` for
|
|
25
|
+
* every file, writes nothing, and exits 0.
|
|
26
|
+
*/
|
|
27
|
+
/** Where the flow's bytes come from and go. Injected, so guards run it in-process. */
|
|
28
|
+
export interface SetupIo {
|
|
29
|
+
/**
|
|
30
|
+
* Interactive input — the real stdin in `bin.ts`, a scripted stream in tests.
|
|
31
|
+
* Required only for the interactive path; `--yes` never asks.
|
|
32
|
+
*/
|
|
33
|
+
readonly input?: AnswerStream;
|
|
34
|
+
readonly output: (text: string) => void;
|
|
35
|
+
/** Where the recipe is applied: config discovery, hooks files, the store. */
|
|
36
|
+
readonly cwd: string;
|
|
37
|
+
/** The home directory, for harness detection. Defaults to the real one. */
|
|
38
|
+
readonly home?: string;
|
|
39
|
+
/** The release running setup — stamped into the instruction block for `smelt doctor`. */
|
|
40
|
+
readonly version?: string;
|
|
41
|
+
/** The lava renderer's switch — computed by the verb from CliIo, interactive-only. */
|
|
42
|
+
readonly color?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Everything the verb resolved before the flow ran. Pure data, both paths. */
|
|
45
|
+
export interface SetupOptions {
|
|
46
|
+
/** Validated harness ids — `--harness`, repeatable. Empty means none was named. */
|
|
47
|
+
readonly harnessIds: readonly string[];
|
|
48
|
+
readonly yes: boolean;
|
|
49
|
+
readonly noMcp: boolean;
|
|
50
|
+
readonly json: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** One file's fate, as the receipt and the confirm listing both spell it. */
|
|
53
|
+
export interface SetupFileAction {
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly action: 'written' | 'updated' | 'unchanged' | 'skipped';
|
|
56
|
+
/** Why a file was skipped, or what an update added. */
|
|
57
|
+
readonly detail?: string;
|
|
58
|
+
}
|
|
59
|
+
/** One verification result. A check states what it proved, not what it ran. */
|
|
60
|
+
export interface SetupCheck {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly ok: boolean;
|
|
63
|
+
readonly detail: string;
|
|
64
|
+
}
|
|
65
|
+
/** The machine receipt — `--json` with `--yes`. Everything the flow decided. */
|
|
66
|
+
export interface SetupReceipt {
|
|
67
|
+
readonly format: 'smelt.setup.v1';
|
|
68
|
+
readonly cwd: string;
|
|
69
|
+
readonly config: {
|
|
70
|
+
readonly action: 'written' | 'updated' | 'current';
|
|
71
|
+
};
|
|
72
|
+
readonly files: readonly SetupFileAction[];
|
|
73
|
+
readonly mcp: {
|
|
74
|
+
readonly status: 'applied' | 'manual' | 'skipped';
|
|
75
|
+
/** The recipe's registration command — what was written, or what is left to you. */
|
|
76
|
+
readonly command?: string;
|
|
77
|
+
};
|
|
78
|
+
readonly checks: readonly SetupCheck[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The flow, start to finish. Returns an exit code: 0 when every check passed, the
|
|
82
|
+
* refused exit when one did not — a setup that cannot prove its own round trip is not
|
|
83
|
+
* a finished setup, and an agent reading `--json` must be able to see that in the
|
|
84
|
+
* exit code without parsing prose.
|
|
85
|
+
*/
|
|
86
|
+
export declare function runSetup(options: SetupOptions, io: SetupIo): Promise<number>;
|
|
87
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/cli/setup.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,sFAAsF;AACtF,MAAM,WAAW,OAAO;IACtB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;IACjE,uDAAuD;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,+EAA+E;AAC/E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;KAAE,CAAC;IACxE,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,QAAQ,CAAC,GAAG,EAAE;QACZ,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;QAClD,oFAAoF;QACpF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;CACxC;AAgCD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAoClF","sourcesContent":["import {\n chmodSync,\n existsSync,\n mkdirSync,\n mkdtempSync,\n readFileSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { homedir, tmpdir } from 'node:os';\nimport { basename, dirname, join } from 'node:path';\n\nimport { CliUsageError } from '../errors.ts';\nimport { DEFAULT_THRESHOLD_BYTES } from '../hooks/guard-core.ts';\nimport { detectedHarnesses, planInstall, presetToggles } from './hooks.ts';\nimport { fileIsOurs } from './installed.ts';\nimport { confirmLoop, listPlannedFiles, walkSteps, wizardAsk } from './wizard.ts';\nimport type { Ask, Step } from './wizard.ts';\nimport type { HooksChoices } from './hooks.ts';\nimport {\n CONFIG_FILE_NAME,\n CONFIG_VERSION,\n findConfigFile,\n parseConfig,\n renderConfig,\n} from './config.ts';\nimport type { SmeltConfig, SmeltConfigStore } from './config.ts';\nimport { HARNESSES, harnessById } from '../harness/registry.ts';\nimport type { HarnessProfile } from '../harness/profile.ts';\nimport { harnessLabel, TIER_HONESTY } from '../harness/profile.ts';\nimport { DirectoryElisionStore } from '../store-dir.ts';\nimport { createSmelter } from '../smelter.ts';\nimport { DEFAULT_STRATEGY } from '../plan/planners.ts';\nimport { SETUP_RECIPE } from '../setup/recipe.ts';\nimport { CLI_NAME, EXIT } from './shell.ts';\nimport type { AnswerStream } from './shell.ts';\nimport { lavaBanner } from './lava.ts';\n\n/**\n * `smelt setup` — the SetupRecipe (CONTEXT.md) applied end-to-end: config, the hooks\n * preset, the MCP registration step, and a real smelt → retrieve round trip to prove\n * the loop. The verb is `subcommands/setup.ts`; this file is the flow, a pure function\n * over an injected input/output pair — the `init`/`hooks` discipline, so the wizard is\n * guard-tested in-process and a renderer (KOT-253) slots in behind the same stream.\n *\n * The two paths share one apply path, because two apply paths would drift:\n *\n * - `--yes` answers everything from the recipe: the budget it recommends (printed\n * loudly, written only when the config lacks one — the `smelt` verb's own\n * budget-required refusal is untouched), a directory store at the recipe's path\n * when the config carries none, and the hooks preset's currently-installed\n * defaults, read the same way `smelt hooks install` reads them.\n * - interactive asks four questions, each with an Enter default, then confirms.\n *\n * The one hard rule is inherited unchanged from `init` and `hooks`: an existing file\n * that is not smelt's own config is never written — not by `--yes`, not by a wizard\n * answer. It is skipped with a note pointing at `smelt hooks install`, which asks per\n * file. `smelt.config.json` is smelt's own file; `setup` updates it and says exactly\n * what it added.\n *\n * Idempotent by construction: a re-run on a current machine plans `unchanged` for\n * every file, writes nothing, and exits 0.\n */\n\n/** Where the flow's bytes come from and go. Injected, so guards run it in-process. */\nexport interface SetupIo {\n /**\n * Interactive input — the real stdin in `bin.ts`, a scripted stream in tests.\n * Required only for the interactive path; `--yes` never asks.\n */\n readonly input?: AnswerStream;\n readonly output: (text: string) => void;\n /** Where the recipe is applied: config discovery, hooks files, the store. */\n readonly cwd: string;\n /** The home directory, for harness detection. Defaults to the real one. */\n readonly home?: string;\n /** The release running setup — stamped into the instruction block for `smelt doctor`. */\n readonly version?: string;\n /** The lava renderer's switch — computed by the verb from CliIo, interactive-only. */\n readonly color?: boolean;\n}\n\n/** Everything the verb resolved before the flow ran. Pure data, both paths. */\nexport interface SetupOptions {\n /** Validated harness ids — `--harness`, repeatable. Empty means none was named. */\n readonly harnessIds: readonly string[];\n readonly yes: boolean;\n readonly noMcp: boolean;\n readonly json: boolean;\n}\n\n/** One file's fate, as the receipt and the confirm listing both spell it. */\nexport interface SetupFileAction {\n readonly name: string;\n readonly action: 'written' | 'updated' | 'unchanged' | 'skipped';\n /** Why a file was skipped, or what an update added. */\n readonly detail?: string;\n}\n\n/** One verification result. A check states what it proved, not what it ran. */\nexport interface SetupCheck {\n readonly name: string;\n readonly ok: boolean;\n readonly detail: string;\n}\n\n/** The machine receipt — `--json` with `--yes`. Everything the flow decided. */\nexport interface SetupReceipt {\n readonly format: 'smelt.setup.v1';\n readonly cwd: string;\n readonly config: { readonly action: 'written' | 'updated' | 'current' };\n readonly files: readonly SetupFileAction[];\n readonly mcp: {\n readonly status: 'applied' | 'manual' | 'skipped';\n /** The recipe's registration command — what was written, or what is left to you. */\n readonly command?: string;\n };\n readonly checks: readonly SetupCheck[];\n}\n\n/** Everything the wizard or `--yes` decided. Pure data until apply. */\ninterface SetupChoices {\n harnesses: HarnessProfile[];\n budgetBytes: number | undefined;\n store: SmeltConfigStore | undefined;\n registerMcp: boolean;\n}\n\n/**\n * The probe the round-trip check smelts: big enough that the probe budget forces\n * cuts, with one focus term that must survive. The lexical planner is deterministic,\n * so every machine that runs setup proves the same round trip.\n */\nconst PROBE_SOURCE: string = `${Array.from(\n { length: 40 },\n (_, i): string =>\n `export function helper${String(i)}(input: string): string {\\n` +\n ` const trimmed = input.trim();\\n` +\n ` return trimmed + \" (${String(i)})\";\\n` +\n `}\\n`,\n).join('')}\\nexport function renderTicket(id: string): string {\\n return 'ticket-' + id;\\n}\\n`;\n\nconst PROBE_BUDGET_BYTES = 600;\n\nfunction readIfExists(path: string): string | undefined {\n return existsSync(path) ? readFileSync(path, 'utf8') : undefined;\n}\n\ntype Say = (text: string) => void;\n\n/**\n * The flow, start to finish. Returns an exit code: 0 when every check passed, the\n * refused exit when one did not — a setup that cannot prove its own round trip is not\n * a finished setup, and an agent reading `--json` must be able to see that in the\n * exit code without parsing prose.\n */\nexport async function runSetup(options: SetupOptions, io: SetupIo): Promise<number> {\n const wizard = options.yes\n ? undefined\n : wizardAsk(\n io.input!,\n io.output,\n `${CLI_NAME} setup: input ended before the wizard finished. Nothing was ` +\n `written. Non-interactive:\\n ${CLI_NAME} setup --yes [--harness <id>]... ` +\n `[--no-mcp] [--json]`,\n );\n const ask: Ask =\n wizard?.ask ??\n (async () => {\n // --yes never asks; a question reached with no stream is a bug in the flow,\n // not an answer the user owes.\n throw new CliUsageError(\n `${CLI_NAME} setup: a question was reached with no interactive input — ` +\n `this is a bug in the flow, not an answer you owe.`,\n );\n });\n // Prose is suppressed in --json mode: the receipt is the whole output, the way the\n // other verbs' envelopes are. A machine parsing the receipt must not also parse\n // around it.\n const say: Say = (text) => {\n if (!options.json) io.output(text);\n };\n\n try {\n const choices: SetupChoices | undefined = options.yes\n ? await yesPath(options, io, say)\n : await wizardPath(options, io, say, ask);\n if (choices === undefined) return EXIT.ok; // declined at the confirm\n return await finish(choices, io, say, options);\n } finally {\n await wizard?.release();\n }\n}\n\n// ── the two decision paths ─────────────────────────────────────────────────────────\n\n/** `--yes`: the recipe's answers, printed loudly — never silently assumed. */\nasync function yesPath(options: SetupOptions, io: SetupIo, say: Say): Promise<SetupChoices> {\n const choices: SetupChoices = {\n harnesses: options.harnessIds.map((id) => harnessById(id)!),\n budgetBytes: SETUP_RECIPE.recommendedBudgetBytes,\n store: { kind: 'directory', path: SETUP_RECIPE.store.defaultDir },\n registerMcp: !options.noMcp,\n };\n say(\n `${CLI_NAME} setup — applying the recipe with --yes:\\n` +\n ` budget: ${String(SETUP_RECIPE.recommendedBudgetBytes)} bytes (written only if ` +\n `the config carries none)\\n` +\n ` store: directory at ${SETUP_RECIPE.store.defaultDir} (only if the config ` +\n `carries none; an explicit store is respected)\\n` +\n ` hooks preset: current defaults for ${\n choices.harnesses.length === 0\n ? 'no harness (none named — config only)'\n : choices.harnesses.map((profile) => profile.id).join(', ')\n }\\n` +\n ` existing files are never overwritten — skipped with a note\\n`,\n );\n return choices;\n}\n\n/** Interactive: four questions, each with an Enter default, then one confirm. */\nasync function wizardPath(\n options: SetupOptions,\n io: SetupIo,\n say: Say,\n ask: Ask,\n): Promise<SetupChoices | undefined> {\n say(lavaBanner('smelt setup', io.color === true));\n say(\n `\\n${CLI_NAME} setup — one command through the whole recipe. Enter accepts every ` +\n `default; nothing is written until the final confirm.\\n\\n`,\n );\n\n const choices: SetupChoices = {\n harnesses: options.harnessIds.map((id) => harnessById(id)!),\n budgetBytes: undefined,\n store: undefined,\n registerMcp: true,\n };\n\n // Budget: the config's own if it carries one, else the recipe's recommendation —\n // Enter is always an answer here, which is the difference from `init`, whose\n // confirm refuses to proceed without a budget someone typed.\n const configPath = findConfigFile(io.cwd) ?? join(io.cwd, CONFIG_FILE_NAME);\n const existingText = readIfExists(configPath);\n const existing = existingText === undefined ? undefined : parseConfig(existingText, configPath);\n const budgetDefault = existing?.defaultBudgetBytes ?? SETUP_RECIPE.recommendedBudgetBytes;\n\n // The wizard kit's step machine, so back is real back: harnesses ← budget ← store\n // ← mcp, each step returning to the one before it (the first says so).\n const steps: readonly Step[] = [\n async (a) => {\n if (options.harnessIds.length > 0) {\n for (const profile of choices.harnesses) say(` ${tierLine(profile)}\\n`);\n return 'ok';\n }\n await stepHarnesses(say, a, choices, detectedHarnesses(io.cwd, io.home ?? homedir()));\n return 'ok';\n },\n async (a) => await stepBudget(say, a, choices, budgetDefault),\n async (a) => await stepStore(say, a, choices),\n async (a) => await stepMcp(say, a, choices),\n ];\n await walkSteps(steps, ask, say);\n for (;;) {\n const verdict = await confirm(say, ask, choices, io);\n if (verdict === 'done') return choices;\n if (verdict === 'declined') {\n say(`Nothing was written.\\n`);\n return undefined;\n }\n // A confirm's back lands on the last step — and from there, real back.\n await walkSteps(steps, ask, say, steps.length - 1);\n }\n}\n\n/** Budget, with the config's or the recipe's number as the Enter default. */\nasync function stepBudget(\n say: Say,\n ask: Ask,\n choices: SetupChoices,\n budgetDefault: number,\n): Promise<'ok' | 'back'> {\n for (;;) {\n const answer = await ask(`default budget in bytes [${String(budgetDefault)}]> `);\n if (answer === 'back') return 'back';\n if (answer === '') {\n choices.budgetBytes = budgetDefault;\n return 'ok';\n }\n if (/^\\d+$/.test(answer) && Number(answer) > 0) {\n choices.budgetBytes = Number(answer);\n return 'ok';\n }\n say(`A whole number of bytes greater than zero, e.g. ${String(budgetDefault)}.\\n`);\n }\n}\n\n// ── the questions ───────────────────────────────────────────────────────────────────\n\nasync function stepHarnesses(\n say: Say,\n ask: Ask,\n choices: SetupChoices,\n detected: readonly HarnessProfile[],\n): Promise<'ok' | 'back'> {\n const all = [...HARNESSES];\n say(`Harnesses to wire with the guard preset:\\n`);\n all.forEach((profile, index) => {\n const mark = detected.some((one) => one.id === profile.id) ? ' (detected)' : '';\n say(` ${String(index + 1)}. ${tierLine(profile)}${mark}\\n`);\n });\n const detectedNote =\n detected.length === 0\n ? 'none detected — Enter means config only'\n : `Enter for detected: ${detected.map((profile) => profile.id).join(', ')}`;\n for (;;) {\n const answer = await ask(`numbers, 'all', or Enter (${detectedNote})> `);\n if (answer === 'back') return 'back'; // the step machine answers it\n if (answer === '') {\n choices.harnesses = [...detected];\n return 'ok';\n }\n if (answer === 'all') {\n choices.harnesses = all;\n return 'ok';\n }\n if (/^\\d+(?:\\s*,\\s*\\d+)*$/u.test(answer)) {\n const picked = answer.split(',').map((piece) => Number(piece.trim()));\n if (picked.every((n) => n >= 1 && n <= all.length)) {\n choices.harnesses = [...new Set(picked)].map((n) => all[n - 1]!);\n return 'ok';\n }\n }\n say(`A comma-separated list of the numbers above, 'all', or Enter.\\n`);\n }\n}\n\nasync function stepStore(say: Say, ask: Ask, choices: SetupChoices): Promise<'ok' | 'back'> {\n say(\n `\\nWhere elided bytes live. Every elision is reversible only while a store holds ` +\n `its bytes (Law 3). An explicit store already in the config is respected.\\n`,\n );\n for (;;) {\n const answer = await ask(`store (1 memory / 2 directory) [2]> `);\n if (answer === 'back') return 'back';\n if (answer === '' || answer === '2') {\n const pathDefault =\n choices.store?.kind === 'directory' ? choices.store.path : SETUP_RECIPE.store.defaultDir;\n const path = await ask(`store directory, relative to ${CONFIG_FILE_NAME} [${pathDefault}]> `);\n if (path === 'back') continue;\n choices.store = { kind: 'directory', path: path === '' ? pathDefault : path };\n return 'ok';\n }\n if (answer === '1') {\n choices.store = { kind: 'memory' };\n return 'ok';\n }\n say(`1 for memory, 2 for directory.\\n`);\n }\n}\n\nasync function stepMcp(say: Say, ask: Ask, choices: SetupChoices): Promise<'ok' | 'back'> {\n for (;;) {\n const answer = await ask(`register the MCP server? (1 yes — prints the command / 2 no) [1]> `);\n if (answer === 'back') return 'back';\n if (answer === '' || answer === '1') {\n choices.registerMcp = true;\n return 'ok';\n }\n if (answer === '2') {\n choices.registerMcp = false;\n return 'ok';\n }\n say(`1 to include the MCP step, 2 to skip it.\\n`);\n }\n}\n\ntype ConfirmVerdict = 'done' | 'declined' | 'back';\n\nasync function confirm(\n say: Say,\n ask: Ask,\n choices: SetupChoices,\n io: SetupIo,\n): Promise<ConfirmVerdict> {\n const plan =\n choices.harnesses.length === 0\n ? undefined\n : planInstall(io.cwd, hooksChoices(choices, io.cwd, io.version));\n say(`\\nAbout to apply, into ${io.cwd}:\\n`);\n say(\n ` ${CONFIG_FILE_NAME.padEnd(32)} (budget ` +\n `${String(choices.budgetBytes ?? SETUP_RECIPE.recommendedBudgetBytes)}, strategy ` +\n `default, store ${describeStore(choices.store)})\\n`,\n );\n if (plan === undefined) {\n say(` no harness selected — the guard preset is skipped\\n`);\n } else {\n listPlannedFiles(\n say,\n plan.files.filter((file) => basename(file.path) !== CONFIG_FILE_NAME),\n plan.skipped,\n fileFate,\n );\n }\n say(\n ` mcp ${\n choices.registerMcp ? `(manual step: ${SETUP_RECIPE.mcp.register})` : '(skipped)'\n }\\nNothing has been written yet.\\n`,\n );\n const confirmed = await confirmLoop(\n ask,\n 'yes to apply, no to leave everything untouched, back to change a step.',\n );\n if (confirmed === 'back') return 'back';\n return confirmed === 'yes' ? 'done' : 'declined';\n}\n\n// ── the one apply path ──────────────────────────────────────────────────────────────\n\n/** Everything one apply decided, as data — the receipt before it is rendered. */\ninterface ApplyOutcome {\n readonly receipt: Omit<SetupReceipt, 'format' | 'cwd'>;\n readonly notes: readonly string[];\n readonly failedChecks: number;\n}\n\n/**\n * The one apply path: config, hooks preset, MCP verdict, checks. Decides and writes;\n * renders nothing — the prose and the JSON receipt are two adapters over the outcome,\n * which is the seam the lava renderer (KOT-253) slots in behind.\n */\nasync function applySetup(choices: SetupChoices, io: SetupIo): Promise<ApplyOutcome> {\n const files: SetupFileAction[] = [];\n const notes: string[] = [];\n\n // ── config first: the hooks plan reads the settled bytes back, so a second run\n // plans the same file as `unchanged` instead of chasing its own tail ──\n const configPath = findConfigFile(io.cwd) ?? join(io.cwd, CONFIG_FILE_NAME);\n const before = readIfExists(configPath);\n const existing = before === undefined ? undefined : parseConfig(before, configPath);\n const budget =\n existing?.defaultBudgetBytes ?? choices.budgetBytes ?? SETUP_RECIPE.recommendedBudgetBytes;\n const store = existing?.store ?? choices.store;\n const next: SmeltConfig = {\n ...existing,\n smeltConfig: CONFIG_VERSION,\n defaultBudgetBytes: budget,\n strategy: existing?.strategy ?? DEFAULT_STRATEGY,\n ...(store === undefined ? {} : { store }),\n };\n const rendered = renderConfig(next);\n let configAction: SetupReceipt['config']['action'];\n if (before === undefined) {\n mkdirSync(dirname(configPath), { recursive: true });\n writeFileSync(configPath, rendered);\n configAction = 'written';\n files.push({ name: CONFIG_FILE_NAME, action: 'written' });\n } else if (before !== rendered) {\n writeFileSync(configPath, rendered);\n configAction = 'updated';\n files.push({\n name: CONFIG_FILE_NAME,\n action: 'updated',\n detail: 'filled the fields the config lacked (budget, strategy default, store)',\n });\n } else {\n configAction = 'current';\n files.push({ name: CONFIG_FILE_NAME, action: 'unchanged' });\n }\n\n // ── hooks preset: the installer's own plan, over the settled config. The plan's\n // config entry rides too — renderConfigWithHooks adds the hooks block the shims\n // read, so setup and hooks install leave the same file, and a re-run plans\n // `unchanged` for it. ──\n const plan =\n choices.harnesses.length === 0\n ? undefined\n : planInstall(io.cwd, hooksChoices(choices, io.cwd, io.version));\n if (plan !== undefined) {\n for (const file of plan.files) {\n if (file.unchanged) {\n files.push({ name: file.name, action: 'unchanged' });\n continue;\n }\n if (!file.exists) {\n mkdirSync(dirname(file.path), { recursive: true });\n writeFileSync(file.path, file.content);\n if (file.mode !== undefined) chmodSync(file.path, file.mode);\n files.push({ name: file.name, action: 'written' });\n continue;\n }\n if (fileIsOursToRepair(file)) {\n // Repair, not consent: this file already carries smelt's own entries, and\n // every byte the plan would change is inside them — a marker-block upsert,\n // a strip-merge of our hook entries, a nested edit of our server entry.\n // That is the doctor → setup loop actually closing: a block written by an\n // older release is brought to this one.\n writeFileSync(file.path, file.content);\n if (file.mode !== undefined) chmodSync(file.path, file.mode);\n files.push({\n name: file.name,\n action: basename(file.path) === CONFIG_FILE_NAME ? 'updated' : 'written',\n detail: \"repaired — only smelt's own entries in it changed\",\n });\n continue;\n }\n // The hard rule, inherited: an existing file with nothing of smelt's in it is\n // never written — not by --yes, not by a wizard answer. Point at the editor.\n files.push({\n name: file.name,\n action: 'skipped',\n detail: 'exists — not overwritten; `smelt hooks install` edits it and asks per file',\n });\n }\n notes.push(...plan.notes);\n }\n\n // ── mcp: applied where a profile carries the registration, handed over as the\n // exact command where none does (no harness named, or a TOML harness) — never\n // pretending it ran something it did not ──\n const mcpApplied = choices.harnesses.some((profile) =>\n profile.install.some((step) => step.kind === 'mcp-registration'),\n );\n const mcp: SetupReceipt['mcp'] = !choices.registerMcp\n ? { status: 'skipped' }\n : mcpApplied\n ? { status: 'applied', command: SETUP_RECIPE.mcp.register }\n : { status: 'manual', command: SETUP_RECIPE.mcp.register };\n\n // ── verify: the checks that make \"set up\" a claim with evidence ──\n const checks: SetupCheck[] = [];\n const afterText = readIfExists(configPath);\n const after = afterText === undefined ? undefined : parseConfig(afterText, configPath);\n checks.push({\n name: 'config parses',\n ok: after?.smeltConfig === CONFIG_VERSION,\n detail: `${CONFIG_FILE_NAME} read back and parsed`,\n });\n const storeDir =\n after?.store?.kind === 'directory' ? join(dirname(configPath), after.store.path) : undefined;\n if (storeDir === undefined) {\n checks.push({\n name: 'round trip',\n ok: true,\n detail: 'memory store — per-process by choice; retrieval works inside one process',\n });\n } else {\n checks.push(...(await probeStore(storeDir, budget)));\n }\n\n const failedChecks = checks.filter((check) => !check.ok).length;\n return {\n receipt: { config: { action: configAction }, files, mcp, checks },\n notes,\n failedChecks,\n };\n}\n\n/** The prose renderer — one adapter over the outcome. */\nfunction renderOutcome(outcome: ApplyOutcome, say: Say): boolean {\n const { receipt } = outcome;\n const { files, mcp, checks } = receipt;\n const ok = outcome.failedChecks === 0;\n\n for (const file of files) {\n say(` ${file.name}: ${file.action}${file.detail === undefined ? '' : ` — ${file.detail}`}\\n`);\n }\n for (const note of outcome.notes) say(`note: ${note}\\n`);\n if (mcp.status === 'applied') {\n say(\n `MCP registration: written to the harness configs beside any servers you already ` +\n `had — \\`smelt hooks remove\\` takes it back out.\\n`,\n );\n }\n if (mcp.status === 'manual') {\n say(\n `MCP registration stays in your hands (no selected harness carries it):\\n` +\n ` ${mcp.command}\\n` +\n `Codex and Grok spell it in TOML — packages/mcp/README.md has both.\\n`,\n );\n }\n for (const check of checks) {\n say(`${check.ok ? ' ✓' : ' ✗'} ${check.name} — ${check.detail}\\n`);\n }\n\n return ok;\n}\n\n/** finish: apply once, render twice — prose for humans, the receipt for machines. */\nasync function finish(\n choices: SetupChoices,\n io: SetupIo,\n say: Say,\n options: SetupOptions,\n): Promise<number> {\n const outcome = await applySetup(choices, io);\n const ok = renderOutcome(outcome, say);\n if (options.json) {\n const receipt: SetupReceipt = {\n format: 'smelt.setup.v1',\n cwd: io.cwd,\n ...outcome.receipt,\n };\n io.output(JSON.stringify(receipt, null, 2) + '\\n');\n }\n return ok ? EXIT.ok : EXIT.refused;\n}\n\n/**\n * The flow's own probe, split where honesty demanded it: the **real** store directory\n * is proven creatable and writable with a scratch file that is removed again — the\n * user's counters stay untouched — and the elide → retrieve round trip runs against a\n * **disposable** store beside it, deleted with the check. The first version of this\n * probe ran the round trip in the production store: its blobs and its one retrieval\n * are permanent (a store that can forget is not reversible), so a fresh machine's\n * first `smelt stats` would have reported setup's probe as the user's work — noise in\n * the exact honest signal the product leads with.\n */\nasync function probeStore(storeDir: string, budget: number): Promise<SetupCheck[]> {\n const checks: SetupCheck[] = [];\n try {\n mkdirSync(storeDir, { recursive: true });\n const scratch = join(storeDir, '.setup-probe-writable');\n writeFileSync(scratch, 'writable');\n rmSync(scratch, { force: true });\n checks.push({\n name: 'store writable',\n ok: true,\n detail: `${storeDir} accepts and removes a scratch file`,\n });\n } catch (error) {\n checks.push({\n name: 'store writable',\n ok: false,\n detail: `${storeDir} is not writable: ${error instanceof Error ? error.message : String(error)}`,\n });\n return checks; // the round trip cannot prove more than this\n }\n\n const disposable = mkdtempSync(join(tmpdir(), 'smelt-setup-probe-'));\n try {\n const store = new DirectoryElisionStore(disposable);\n const smelter = createSmelter({ store });\n const result = await smelter.smelt(PROBE_SOURCE, {\n path: 'setup-probe.ts',\n focus: ['renderTicket'],\n budgetBytes: Math.min(budget, PROBE_BUDGET_BYTES),\n });\n if (result.elisions.length === 0) {\n checks.push({\n name: 'round trip',\n ok: false,\n detail: `the probe produced no elisions at a ${String(PROBE_BUDGET_BYTES)}-byte budget`,\n });\n return checks;\n }\n const first = result.elisions[0]!;\n const original = PROBE_SOURCE.slice(first.range.start, first.range.end);\n const back = store.retrieve(first.hash);\n checks.push({\n name: 'round trip',\n ok: back === original,\n detail:\n back === original\n ? `${String(result.elisions.length)} elisions under the budget; the first cut's ` +\n `${String(first.range.end - first.range.start)} bytes retrieved byte-identical, in a throwaway store`\n : 'the store returned different bytes than were elided',\n });\n return checks;\n } finally {\n rmSync(disposable, { recursive: true, force: true });\n }\n}\n\n// ── small shared pieces ─────────────────────────────────────────────────────────────\n\n/**\n * Whether an existing planned file is smelt's to repair. The config is smelt's own;\n * every other file is ours exactly when it already carries our entries — the marker\n * token in text, or our hook entries in a JSON hooks file (the guard command carries\n * no token, hence the entry-level predicate). A file with nothing of ours in it is\n * somebody else's, and consent — not --yes — is what opens it.\n */\nfunction fileIsOursToRepair(file: { readonly name: string; readonly path: string }): boolean {\n if (basename(file.path) === CONFIG_FILE_NAME) return true;\n return fileIsOurs(file.name, readFileSync(file.path, 'utf8'));\n}\n\nfunction hooksChoices(\n choices: SetupChoices,\n cwd: string,\n version: string | undefined,\n): HooksChoices {\n return {\n harnesses: choices.harnesses,\n ...(version === undefined ? {} : { writtenBy: version }),\n // Read off what is actually installed, falling back to the installer's defaults\n // when nothing of smelt's is on disk — the same \"edit, never reset\" reading the\n // hooks installer itself uses. No second copy of the defaults lives here.\n ...presetToggles(cwd),\n enforcement: 'deny',\n thresholdBytes: DEFAULT_THRESHOLD_BYTES,\n };\n}\n\nfunction tierLine(profile: HarnessProfile): string {\n return `${profile.id.padEnd(12)} ${harnessLabel(profile).padEnd(16)} [${profile.tier}] — ${\n TIER_HONESTY[profile.tier]\n }`;\n}\n\nfunction describeStore(store: SmeltConfigStore | undefined): string {\n if (store === undefined) return 'none (the config keeps whatever it has)';\n return store.kind === 'memory' ? 'memory' : `directory at ${store.path}`;\n}\n\nfunction fileFate(file: {\n readonly name: string;\n readonly exists: boolean;\n readonly unchanged: boolean;\n}): string {\n if (file.unchanged) return 'unchanged — nothing to write';\n if (file.exists) return 'exists — will be skipped, not overwritten';\n return 'new';\n}\n"]}
|