@smeltjs/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/guide.d.ts +68 -0
- package/dist/agents/guide.d.ts.map +1 -0
- package/dist/agents/guide.js +72 -0
- package/dist/agents/guide.js.map +1 -0
- package/dist/agents/instructions.d.ts +133 -0
- package/dist/agents/instructions.d.ts.map +1 -0
- package/dist/agents/instructions.js +212 -0
- package/dist/agents/instructions.js.map +1 -0
- package/dist/agents/lint.d.ts +154 -0
- package/dist/agents/lint.d.ts.map +1 -0
- package/dist/agents/lint.js +761 -0
- package/dist/agents/lint.js.map +1 -0
- package/dist/agents/split.d.ts +113 -0
- package/dist/agents/split.d.ts.map +1 -0
- package/dist/agents/split.js +234 -0
- package/dist/agents/split.js.map +1 -0
- package/dist/cli/agents.d.ts +39 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +121 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js +9 -4
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/config.d.ts +20 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +34 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/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 +61 -18
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +178 -241
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +8 -14
- 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 +23 -0
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +140 -2
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/run.d.ts +5 -3
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +2 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/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 +72 -0
- package/dist/cli/subcommands/agents.d.ts.map +1 -0
- package/dist/cli/subcommands/agents.js +163 -0
- package/dist/cli/subcommands/agents.js.map +1 -0
- 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 +40 -2
- 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 +29 -5
- 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 +4 -1
- package/dist/cli/subcommands/registry.d.ts.map +1 -1
- package/dist/cli/subcommands/registry.js +9 -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/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/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 +2 -0
- package/dist/harness/codex.js.map +1 -1
- package/dist/harness/gemini.d.ts.map +1 -1
- package/dist/harness/gemini.js +1 -0
- package/dist/harness/gemini.js.map +1 -1
- package/dist/harness/grok.d.ts.map +1 -1
- package/dist/harness/grok.js +5 -1
- package/dist/harness/grok.js.map +1 -1
- package/dist/harness/hermes.d.ts.map +1 -1
- package/dist/harness/hermes.js +1 -0
- package/dist/harness/hermes.js.map +1 -1
- package/dist/harness/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 +49 -3
- package/dist/harness/profile.d.ts.map +1 -1
- package/dist/harness/profile.js +15 -0
- package/dist/harness/profile.js.map +1 -1
- package/dist/harness/registry.d.ts +55 -6
- package/dist/harness/registry.d.ts.map +1 -1
- package/dist/harness/registry.js +63 -6
- package/dist/harness/registry.js.map +1 -1
- package/dist/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/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.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/text/json-edit.d.ts +85 -0
- package/dist/text/json-edit.d.ts.map +1 -0
- package/dist/text/json-edit.js +290 -0
- package/dist/text/json-edit.js.map +1 -0
- package/dist/types.d.ts +0 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../../src/agents/lint.ts"],"names":[],"mappings":"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","sourcesContent":["import { nodeFsReader } from '../repomap/reader.ts';\nimport type { RepoReader } from '../repomap/reader.ts';\nimport type { ElisionReason } from '../types.ts';\n\nimport { citing, GUIDE } from './guide.ts';\nimport { ancestorDirs, readInstructionSet, resolvesInTree } from './instructions.ts';\nimport type { InstructionFile, InstructionSet } from './instructions.ts';\n\n/**\n * `smelt agents lint` — the audit of the blob an agent loads on **every** request.\n *\n * smelt's whole subject is what a context window is spent on, and an instruction file\n * is the one blob every request pays for whether or not it is relevant. So the lint is\n * the same three moves smelt makes everywhere else, aimed at a file nobody measures:\n *\n * 1. **Measure, never threshold** (ruling R2). Bytes per level, the most any single\n * request loads, and the repository-wide surface — three numbers, each labelled\n * with the question it answers, because a monorepo makes the last two differ and\n * printing one under the other's heading is a lie about a cost. Plus an imperative\n * count that says out loud it is a heuristic. The only number that can fail a run\n * is `agents.budgetBytes` in `smelt.config.json` — *the user's own*. There is no\n * built-in budget, for exactly the reason `--budget` has none.\n * 2. **Explain every finding** (Law 2, in the {@link ElisionReason} discipline). A\n * finding is a stable `rule` id plus a sentence, and the sentence ends in a phrase\n * from the guide it is applying, attributed — see `./guide.ts`.\n * 3. **Resolve against the real tree** (ruling R3). `dead-path` and `dead-link` are\n * the checks nobody else makes, because everyone else is linting Markdown while\n * the thing that has rotted is the *repository the Markdown describes*. A renamed\n * `src/auth/handlers.ts` does not make the file invalid; it makes it a lie that\n * the agent believes on every request.\n *\n * **Advisory by default.** Findings exit 0. `--strict` turns any finding into exit 1\n * for CI, because a check that cannot be enforced is a check nobody runs, and a check\n * that is enforced by default is smelt deciding somebody's house style for them.\n *\n * The heuristics here are heuristics, and the report says so where it matters: the\n * imperative count carries \"(heuristic)\" in its own label and in every receipt,\n * `generated-boilerplate` calls itself the softest rule in its own explanation, and the\n * closing line of every run states that findings are advisory and exit 0 unless\n * `--strict` was asked for. A rule that fires on a file the guide would call minimal is\n * not automatically a bug in the file — it may be a bug in the rule, or in the guide,\n * and the answer is worth writing down either way (ruling R9; see this repository's own\n * `AGENTS.md`).\n */\n\n/* ------------------------------------------------------------------------------------\n * Rule ids — stable, and the whole machine-readable surface of a finding\n * ---------------------------------------------------------------------------------- */\n\n/** A path-like token in the prose that resolves to nothing in the tree. The flagship. */\nexport const DEAD_PATH_RULE = 'dead-path';\n/** A Markdown link whose relative target is not in the tree. */\nexport const DEAD_LINK_RULE = 'dead-link';\n/** \"always\", \"never\", ALL-CAPS forcing. */\nexport const FORCING_LANGUAGE_RULE = 'forcing-language';\n/** A directory tree, or a run of bare path lines. */\nexport const STRUCTURE_DUMP_RULE = 'structure-dump';\n/** The fingerprints an init script leaves. The softest rule here, and it says so. */\nexport const GENERATED_BOILERPLATE_RULE = 'generated-boilerplate';\n/** A code-style rule that loads on every request to be relevant on some of them. */\nexport const LANGUAGE_RULE_RULE = 'language-rule';\n/** A mirror (CLAUDE.md / GEMINI.md) that has diverged from its AGENTS.md. */\nexport const MIRROR_DRIFT_RULE = 'mirror-drift';\n/** The same instruction present at a level and at one of its ancestors. */\nexport const RESTATED_AT_LEVEL_RULE = 'restated-at-level';\n\n/**\n * The rules a finding can carry, in report order.\n *\n * `Object.freeze`-flat on purpose: the ids are a wire surface. They go into `--json`,\n * into CI greps and into whatever a user filters on, so they are declared once and\n * never spelled again in prose.\n */\nexport const AGENTS_LINT_RULES = [\n DEAD_PATH_RULE,\n DEAD_LINK_RULE,\n FORCING_LANGUAGE_RULE,\n STRUCTURE_DUMP_RULE,\n GENERATED_BOILERPLATE_RULE,\n LANGUAGE_RULE_RULE,\n MIRROR_DRIFT_RULE,\n RESTATED_AT_LEVEL_RULE,\n] as const;\n\n/**\n * The imperative counter's rule id (ruling R6).\n *\n * **Deliberately not in {@link AGENTS_LINT_RULES}.** An imperative is not a defect —\n * an instruction file is *made* of imperatives — so counting them is a measurement,\n * like `outputBytes`, and putting them among the findings would make `--strict` red on\n * every real AGENTS.md and therefore useless. Each counted line still carries a\n * receipt naming the verb that matched, because a heuristic whose matches you cannot\n * inspect is a number nobody can check.\n */\nexport const IMPERATIVE_LINE_RULE = 'imperative-line';\n\n/* ------------------------------------------------------------------------------------\n * The report\n * ---------------------------------------------------------------------------------- */\n\n/** One thing the lint noticed, at one place, with its reason. */\nexport interface AgentsFinding {\n /** Root-relative path of the instruction file. */\n readonly file: string;\n /** 1-based line within that file. */\n readonly line: number;\n /** Stable `rule` id plus the sentence explaining it — Law 2's shape. */\n readonly reason: ElisionReason;\n}\n\n/** What one level of the merged set costs, and what stands beside it. */\nexport interface AgentsLevelReport {\n /** Root-relative directory; `''` is the repository root. */\n readonly dir: string;\n /** The file this level contributes — see {@link InstructionLevel}. */\n readonly path: string;\n readonly bytes: number;\n /** The mirrors at this level, and how each one stands. */\n readonly mirrors: readonly AgentsMirrorReport[];\n}\n\n/** A `CLAUDE.md`/`GEMINI.md` beside an `AGENTS.md`, and whether it can drift. */\nexport interface AgentsMirrorReport {\n readonly path: string;\n readonly bytes: number;\n /**\n * `'symlink'` — the arrangement the guide recommends, and the only one in which\n * drift is impossible. `'copy'` — byte-identical today. `'drift'` — diverged, and a\n * {@link MIRROR_DRIFT_RULE} finding.\n */\n readonly standing: 'symlink' | 'copy' | 'drift';\n}\n\n/** Everything one `smelt agents lint` run measured and found. */\nexport interface AgentsLintReport {\n /** The directory that was linted, as the caller spelled it. */\n readonly root: string;\n /** Root level first. Empty when the tree holds no instruction file at all. */\n readonly levels: readonly AgentsLevelReport[];\n /**\n * The repository-wide instruction surface: every level's primary, summed.\n *\n * **Not a per-request cost** — see {@link perRequestBytes}, and the note in\n * `./instructions.ts` on why the two differ in any monorepo. This is the number the\n * user's `agents.budgetBytes` is compared against, deliberately: a ceiling on the\n * whole surface is one that cannot be met by moving bytes into a second package.\n */\n readonly totalBytes: number;\n /**\n * What the most expensive single request loads: the heaviest level plus its\n * ancestors. Siblings never merge, so they are never summed into this.\n */\n readonly perRequestBytes: number;\n /** Present only when `smelt.config.json` set one. There is no default (R2). */\n readonly budgetBytes?: number;\n /**\n * Lines counted as instructions, each with the verb that matched. The *count* is the\n * headline (`imperatives (heuristic)`); the receipts make it checkable.\n */\n readonly imperatives: readonly AgentsFinding[];\n /** Every advisory finding, grouped by rule in {@link AGENTS_LINT_RULES} order. */\n readonly findings: readonly AgentsFinding[];\n}\n\n/** What `lintAgents` needs. Everything but the root has a default. */\nexport interface AgentsLintOptions {\n /** The repository root to lint. */\n readonly root: string;\n /** The tree seam. Defaults to {@link nodeFsReader}. */\n readonly reader?: RepoReader;\n /** Replaces the built-in ignore list when given. */\n readonly ignore?: readonly string[];\n /** The user's budget, from `smelt.config.json`. Absent means unbudgeted (R2). */\n readonly budgetBytes?: number;\n}\n\n/**\n * Lint the merged set under `root`.\n *\n * Pure over its inputs and its reader: nothing is written, and every filesystem touch\n * goes through {@link RepoReader}, which has no writer on it.\n */\nexport function lintAgents(options: AgentsLintOptions): AgentsLintReport {\n const reader = options.reader ?? nodeFsReader();\n const set = readInstructionSet({\n root: options.root,\n reader,\n ...(options.ignore === undefined ? {} : { ignore: options.ignore }),\n });\n\n const findings: AgentsFinding[] = [];\n const imperatives: AgentsFinding[] = [];\n\n for (const level of set.levels) {\n // The imperative count is a companion to the byte total, so it is counted over\n // exactly what the byte total is counted over: the primaries. A mirror is an\n // alternative spelling of a level, not a second level — counting it would make\n // the headline number describe a request nobody makes.\n imperatives.push(...countImperatives(level.primary, scanLines(level.primary.text)));\n\n // The rules, though, run over the primary **and every mirror that has actually\n // diverged**: a drifted CLAUDE.md is what Claude Code loads, so its own dead paths\n // are real. A symlink or a byte-identical copy is skipped — it would mint a\n // duplicate of every finding on the primary and say nothing new.\n const linted = [\n level.primary,\n ...level.mirrors.filter((mirror) => standingOf(level.primary, mirror) === 'drift'),\n ];\n for (const file of linted) {\n const lines = scanLines(file.text);\n findings.push(...findDeadLinks(file, lines, options.root, reader));\n findings.push(...findDeadPaths(file, lines, options.root, reader));\n findings.push(...findForcingLanguage(file, lines));\n findings.push(...findStructureDumps(file, lines));\n findings.push(...findGeneratedBoilerplate(file, lines));\n findings.push(...findLanguageRules(file, lines));\n }\n findings.push(...findMirrorDrift(level.primary, level.mirrors));\n }\n findings.push(...findRestatedAcrossLevels(set));\n\n return {\n root: options.root,\n levels: set.levels.map((level) => ({\n dir: level.dir,\n path: level.primary.path,\n bytes: level.primary.bytes,\n mirrors: level.mirrors.map((mirror) => ({\n path: mirror.path,\n bytes: mirror.bytes,\n standing: standingOf(level.primary, mirror),\n })),\n })),\n totalBytes: set.totalBytes,\n perRequestBytes: set.perRequestBytes,\n ...(options.budgetBytes === undefined ? {} : { budgetBytes: options.budgetBytes }),\n imperatives,\n findings: findings.toSorted(byRuleThenPlace),\n };\n}\n\n/** How far over the user's budget the merged set is, or `undefined` when it fits. */\nexport function overBudgetBytes(report: AgentsLintReport): number | undefined {\n if (report.budgetBytes === undefined) return undefined;\n const over = report.totalBytes - report.budgetBytes;\n return over > 0 ? over : undefined;\n}\n\n/** Findings in report order: rule first, then where they were found. */\nfunction byRuleThenPlace(a: AgentsFinding, b: AgentsFinding): number {\n const rank =\n ruleOrder(a.reason.rule) - ruleOrder(b.reason.rule) ||\n (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) ||\n a.line - b.line;\n return rank;\n}\n\nfunction ruleOrder(rule: string): number {\n const index = (AGENTS_LINT_RULES as readonly string[]).indexOf(rule);\n return index === -1 ? AGENTS_LINT_RULES.length : index;\n}\n\n/* ------------------------------------------------------------------------------------\n * Scanning: prose versus fences\n * ---------------------------------------------------------------------------------- */\n\n/** One line of an instruction file, with the one fact every rule branches on. */\ninterface ScannedLine {\n /** 1-based. */\n readonly number: number;\n readonly text: string;\n /** True inside a fenced code block, including the fence lines themselves. */\n readonly fenced: boolean;\n /** True for the ``` or ~~~ line that opens a block. */\n readonly opensFence: boolean;\n}\n\n/**\n * Split a file into lines, marking fenced code.\n *\n * The distinction matters in both directions: a `const x = 1` inside a fence is an\n * *example*, not a `language-rule`, and a tree drawing is only a `structure-dump`\n * because it is a fence full of paths. Rules that read prose skip fences; the one\n * rule that reads fences skips prose.\n */\nfunction scanLines(text: string): readonly ScannedLine[] {\n const out: ScannedLine[] = [];\n let fence: string | undefined;\n text.split('\\n').forEach((line, index) => {\n const marker = /^\\s{0,3}(`{3,}|~{3,})/.exec(line);\n const opensFence = marker !== null && fence === undefined;\n if (marker !== null) {\n if (fence === undefined) fence = marker[1]!.slice(0, 1);\n else if (marker[1]!.startsWith(fence)) fence = undefined;\n out.push({ number: index + 1, text: line, fenced: true, opensFence });\n return;\n }\n out.push({ number: index + 1, text: line, fenced: fence !== undefined, opensFence: false });\n });\n return out;\n}\n\n/** A line stripped of list bullets, heading hashes, blockquote marks and bold runs. */\nfunction bareText(text: string): string {\n return text\n .replace(/^\\s*(?:[-*+]\\s+|\\d+[.)]\\s+|#{1,6}\\s+|>\\s*)+/, '')\n .replace(/\\*\\*/g, '')\n .trim();\n}\n\n/* ------------------------------------------------------------------------------------\n * imperative-line — a labelled heuristic (R6)\n * ---------------------------------------------------------------------------------- */\n\n/**\n * The openers counted as an instruction.\n *\n * A closed list, deliberately: an open-ended part-of-speech guess would be a number\n * nobody could reproduce, and this figure is reported beside a byte count that *is*\n * exact. The modal openers the guide itself names — always / never / do not / must /\n * should — are here alongside the verbs an instruction file actually opens with.\n */\nconst IMPERATIVE_OPENERS: readonly string[] = [\n 'add',\n 'always',\n 'avoid',\n 'build',\n 'check',\n 'commit',\n 'create',\n 'do',\n 'document',\n 'ensure',\n 'follow',\n 'format',\n 'ignore',\n 'implement',\n 'install',\n 'keep',\n 'lint',\n 'make',\n 'must',\n 'name',\n 'never',\n 'place',\n 'prefer',\n 'put',\n 'read',\n 'refuse',\n 'remove',\n 'return',\n 'run',\n 'should',\n 'skip',\n 'test',\n 'throw',\n 'treat',\n 'update',\n 'use',\n 'verify',\n 'write',\n];\n\n/**\n * Count the lines that read as instructions, one receipt each.\n *\n * Reported as `imperatives (heuristic)` and never as a precise figure, because it is\n * not one: \"Run `pnpm verify`\" counts and \"The gate is `pnpm verify`\" does not, and\n * both are the same instruction. The number is useful as a *scale* — the guide cites\n * ~150-200 as what a frontier thinking model follows consistently — and useless as a\n * threshold, which is why nothing here compares it to anything.\n */\nfunction countImperatives(file: InstructionFile, lines: readonly ScannedLine[]): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n for (const line of lines) {\n if (line.fenced) continue;\n const bare = bareText(line.text);\n if (bare === '') continue;\n const opener = /^(do not|[A-Za-z']+)/.exec(bare.toLowerCase())?.[1];\n if (opener === undefined) continue;\n const matched = opener === 'do not' ? 'do not' : opener === \"don't\" ? \"don't\" : opener;\n const counted =\n matched === 'do not' ||\n matched === \"don't\" ||\n IMPERATIVE_OPENERS.includes(matched.replace(/'.*$/, ''));\n if (!counted) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: IMPERATIVE_LINE_RULE,\n explanation:\n `opens with \"${matched}\", so it is counted as one instruction (heuristic)` +\n citing(GUIDE.instructionCeiling),\n },\n });\n }\n return out;\n}\n\n/* ------------------------------------------------------------------------------------\n * dead-link — a Markdown link whose target left the tree\n * ---------------------------------------------------------------------------------- */\n\n/** `[text](target)`, with the target captured. */\nconst MARKDOWN_LINK = /\\[[^\\]]*\\]\\(([^)\\s]+)(?:\\s+\"[^\"]*\")?\\)/g;\n\n/**\n * CommonMark lets a link destination be wrapped in angle brackets — `[x](<a b.md>)`,\n * the only way to write one containing a space. The brackets are delimiters, not part\n * of the path, and statting `<src/kept.ts>` never resolves: the rule would report a\n * live file as dead, which is the exact failure this rule exists to avoid making.\n */\nfunction stripAngles(target: string): string {\n return target.startsWith('<') && target.endsWith('>') ? target.slice(1, -1) : target;\n}\n\nfunction findDeadLinks(\n file: InstructionFile,\n lines: readonly ScannedLine[],\n root: string,\n reader: RepoReader,\n): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n for (const line of lines) {\n if (line.fenced) continue;\n for (const match of line.text.matchAll(MARKDOWN_LINK)) {\n const target = stripFragment(stripAngles(match[1]!));\n if (target === '' || isExternal(target)) continue;\n const resolved = resolveAgainst(file.dir, target);\n if (resolved === undefined) continue;\n if (resolvesInTree(root, reader, resolved)) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: DEAD_LINK_RULE,\n explanation:\n `links to \\`${target}\\`, which is not in the tree — the pointer the root ` +\n `file exists to be goes nowhere` +\n citing(GUIDE.pointsElsewhere),\n },\n });\n }\n }\n return out;\n}\n\n/* ------------------------------------------------------------------------------------\n * dead-path — the flagship (R3)\n * ---------------------------------------------------------------------------------- */\n\n/** A token with a directory separator: `src/auth`, `./scripts/build.mjs`, `docs/`. */\nconst SLASHED = /^\\.{0,2}\\/?[\\w.+-]+(?:\\/[\\w.+-]+)*\\/?$/;\n/** A bare file name whose extension says \"this is a file in this repo\". */\nconst CODE_FILE = /^[\\w.-]+\\.(?:[cm]?[jt]sx?|json|md|ya?ml|toml|py|rs|go|rb|java|sh|sql|css|html)$/;\n/**\n * A host name: what `example.com/guide` is, and what a repository path never is.\n *\n * A scheme-less URL is the one thing that is shaped exactly like a relative path —\n * dotted word, slash, more words — and the guide this very tool cites is written\n * `aihero.dev/a-complete-guide-to-agents-md` in smelt's own help text. Only the\n * segment before the first slash is tested, so `scripts/build.sh` is untouched: `sh`\n * is a TLD *and* an extension, and which one it is depends entirely on where the dot\n * sits relative to the separator.\n */\nconst DOMAIN_HOST =\n /^[\\w-]+(?:\\.[\\w-]+)*\\.(?:com|org|net|io|dev|ai|app|co|me|sh|so|to|xyz|gg|cloud|page|info|blog)$/i;\n/**\n * `Node.js`, `Vue.js`, `Bun.sh`, `Three.js` — a product, not a file in this tree.\n *\n * Narrow on purpose: one capitalised word, then one of the five suffixes products are\n * actually named with. It costs a bare mention of a PascalCase `Button.js`, which is\n * a real filename — but that is a finding not made, and this is a false accusation not\n * made, and on the flagship rule those two are not worth the same.\n */\nconst PRODUCT_NAME = /^[A-Z][A-Za-z]*\\.(?:js|sh|ai|dev|io)$/;\n\nfunction findDeadPaths(\n file: InstructionFile,\n lines: readonly ScannedLine[],\n root: string,\n reader: RepoReader,\n): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n const seen = new Set<string>();\n for (const line of lines) {\n if (line.fenced) continue;\n // Markdown link targets belong to `dead-link`; blank them so one dead pointer is\n // never reported twice under two rules.\n const withoutLinks = line.text.replace(MARKDOWN_LINK, '[]()');\n for (const token of pathCandidates(withoutLinks)) {\n const key = `${String(line.number)}\u0000${token}`;\n if (seen.has(key)) continue;\n seen.add(key);\n const resolved = resolveAgainst(file.dir, token);\n // A token that climbed out of the tree with `..` is not a path this lint can\n // rule on: it names something outside the repository, and both \"dead\" and\n // \"alive\" would be guesses about a directory nobody handed us.\n if (resolved === undefined) continue;\n if (resolvesInTree(root, reader, resolved)) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: DEAD_PATH_RULE,\n explanation:\n `names \\`${token}\\`, which resolves to nothing in this tree — an agent ` +\n `reads this on every request and looks there anyway` +\n citing(GUIDE.stalenessPoisons),\n },\n });\n }\n }\n return out;\n}\n\n/**\n * The path-like tokens on one line.\n *\n * Inline code spans first, because a path in an instruction file is nearly always in\n * backticks; then bare words, filtered hard. The filters are the interesting part —\n * every one of them is a false positive this rule made before it had them:\n *\n * - `https://…`, `mailto:` — not tree paths.\n * - `@smeltjs/core`, `@types/node` — package names, which look exactly like paths.\n * - `src/**\\/*.ts` — a glob describes a set, and a set does not resolve.\n * - `pnpm run build`, `and/or` — anything with whitespace, and anything whose\n * segments carry no extension and no separator worth trusting.\n * - `v1.2/v2` style version prose, caught by requiring a real segment shape.\n * - `example.com/guide`, `aihero.dev/…` — a URL somebody wrote without its scheme.\n * - `Node.js`, `Vue.js`, `Bun.sh` — products whose names end in an extension.\n *\n * The last two are why a token *without* a separator is a candidate only when it came\n * out of a code span. In running prose a dotted bare word is far more often a product\n * than a file, and this rule's whole value is that a reader believes it: one confident\n * sentence accusing `Node.js` of having left the tree costs more trust than a dozen\n * real findings earn. In backticks the author has said \"this is a thing in my\n * repository\", and the rule takes them at their word.\n */\nfunction pathCandidates(text: string): readonly string[] {\n const tokens: string[] = [];\n const add = (raw: string, fromCodeSpan: boolean): void => {\n const token = raw.replace(/[),.:;]+$/, '').trim();\n if (token === '' || !isPathLike(token, fromCodeSpan)) return;\n tokens.push(token);\n };\n const withoutCode = text.replace(/`([^`]+)`/g, (_whole, inner: string) => {\n add(inner, true);\n return ' ';\n });\n for (const word of withoutCode.split(/\\s+/)) add(word, false);\n return tokens;\n}\n\n/**\n * Is `token` a path into this tree?\n *\n * `fromCodeSpan` is the author's own signal, and it decides the one ambiguous case: a\n * dotted word with no separator. See {@link pathCandidates}.\n */\nfunction isPathLike(token: string, fromCodeSpan: boolean): boolean {\n if (isExternal(token)) return false;\n if (token.startsWith('@')) return false;\n if (/[*?[\\]{}<>|\"'`\\\\]/.test(token)) return false;\n if (token.startsWith('#')) return false;\n if (!SLASHED.test(token)) return false;\n if (token.includes('/')) {\n // `example.com/guide` — a URL with its scheme left off, not a directory.\n return !DOMAIN_HOST.test(token.split('/')[0] ?? '');\n }\n return fromCodeSpan && !PRODUCT_NAME.test(token) && CODE_FILE.test(token);\n}\n\n/** True for anything that is not a path into this tree. */\nfunction isExternal(target: string): boolean {\n return /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('//') || target.startsWith('#');\n}\n\nfunction stripFragment(target: string): string {\n const cut = target.indexOf('#');\n return cut === -1 ? target : target.slice(0, cut);\n}\n\n/**\n * A token in a nested instruction file is relative to *that* file's directory, which\n * is the whole reason a nested file can hold a link the root one cannot. `../` is\n * resolved rather than refused, so a nested file may point back up the tree.\n *\n * `undefined` when the token climbs past the repository root. It used to clamp there —\n * `stack.pop()` on an empty stack is a no-op — which silently turned `../sibling/x.ts`\n * into `sibling/x.ts` and then answered a question about the wrong file, in whichever\n * direction happened to be wrong. Outside the tree, this lint has nothing to say.\n */\nfunction resolveAgainst(dir: string, token: string): string | undefined {\n const base = dir === '' ? [] : dir.split('/');\n const parts = token.replace(/\\/+$/, '').split('/');\n const stack = [...base];\n for (const part of parts) {\n if (part === '' || part === '.') continue;\n if (part === '..') {\n if (stack.length === 0) return undefined;\n stack.pop();\n } else stack.push(part);\n }\n return stack.join('/');\n}\n\n/* ------------------------------------------------------------------------------------\n * forcing-language\n * ---------------------------------------------------------------------------------- */\n\n/** ALL-CAPS words a reader meets as shouting rather than as an acronym. */\nconst SHOUTED = /\\b(ALWAYS|NEVER|MUST|DO NOT|DON'T|REQUIRED|MANDATORY|CRITICAL|IMPORTANT)\\b/;\n/** The two words the guide names in its own example of what *not* to write. */\nconst FORCING_WORDS = /\\b(always|never)\\b/i;\n\nfunction findForcingLanguage(\n file: InstructionFile,\n lines: readonly ScannedLine[],\n): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n for (const line of lines) {\n if (line.fenced) continue;\n const shouted = SHOUTED.exec(line.text)?.[1];\n const forcing = shouted ?? FORCING_WORDS.exec(line.text)?.[1];\n if (forcing === undefined) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: FORCING_LANGUAGE_RULE,\n explanation:\n `forces with \"${forcing}\" — forcing language spends tokens on emphasis ` +\n `rather than on information` +\n citing(GUIDE.lightTouch),\n },\n });\n }\n return out;\n}\n\n/* ------------------------------------------------------------------------------------\n * structure-dump\n * ---------------------------------------------------------------------------------- */\n\n/**\n * The characters a generated tree listing is drawn with.\n *\n * Both charsets: `tree` draws box-drawing by default and `|--` / `` `-- `` under\n * `--charset=ascii`, and the ASCII form is what lands in a file written on a machine\n * whose terminal was not UTF-8. Missing it made the whole rule silent on a fenced\n * ASCII tree — and because fenced lines skip `dead-path` too, that block produced no\n * finding of any kind.\n */\nconst TREE_DRAWING = /[├└│─]|^\\s*(?:\\|--|`--|\\|\\s{3}|\\+--)/;\n/** How many path-ish lines in a row read as a dump rather than as an example. */\nconst DUMP_RUN = 3;\n\nfunction findStructureDumps(file: InstructionFile, lines: readonly ScannedLine[]): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n\n // A fenced block whose body is a tree drawing.\n let fenceStart: ScannedLine | undefined;\n let drawn = 0;\n for (const line of lines) {\n if (line.opensFence) {\n fenceStart = line;\n drawn = 0;\n continue;\n }\n if (!line.fenced) {\n fenceStart = undefined;\n continue;\n }\n if (fenceStart === undefined) continue;\n if (TREE_DRAWING.test(line.text)) drawn += 1;\n if (drawn === 2) {\n out.push(structureFinding(file, fenceStart.number, 'a directory tree'));\n fenceStart = undefined;\n }\n }\n\n // A run of bare path lines in prose — the same dump without the box characters.\n let run = 0;\n let runStart = 0;\n for (const line of lines) {\n if (line.fenced) {\n run = 0;\n continue;\n }\n const bare = bareText(line.text);\n // A line whose whole content is one token is a path line, not prose — so the\n // code-span rule `dead-path` needs against running text has nothing to guard here.\n const isPathLine =\n bare !== '' && isPathLike(bare.replace(/`/g, '').split(/\\s+/)[0] ?? '', true);\n if (isPathLine && bare.split(/\\s+/).length <= 2) {\n if (run === 0) runStart = line.number;\n run += 1;\n if (run === DUMP_RUN) out.push(structureFinding(file, runStart, 'a run of path lines'));\n } else {\n run = 0;\n }\n }\n\n return out;\n}\n\nfunction structureFinding(file: InstructionFile, line: number, what: string): AgentsFinding {\n return {\n file: file.path,\n line,\n reason: {\n rule: STRUCTURE_DUMP_RULE,\n explanation:\n `spends the every-request budget on ${what} — layout is the fact in a ` +\n `repository that changes most often, so it is also the fact that rots first` +\n citing(GUIDE.describeCapabilities),\n },\n };\n}\n\n/* ------------------------------------------------------------------------------------\n * generated-boilerplate — the softest rule here, and it says so\n * ---------------------------------------------------------------------------------- */\n\n/** The fingerprints an init script leaves behind, with what each one is. */\nconst BOILERPLATE_SIGNATURES: readonly (readonly [RegExp, string])[] = [\n [/\\bauto-?generated\\b/i, 'an \"auto-generated\" marker'],\n [/\\bgenerated by\\b/i, 'a \"generated by\" credit'],\n [/<!--\\s*generated/i, 'a generated-block comment'],\n [/\\b(?:claude|codex|gemini|cursor|agents?)\\s+init\\b/i, 'an init-command credit'],\n [/\\bthis file was (?:created|generated)\\b/i, 'a \"this file was generated\" line'],\n [/\\bdo not edit\\b/i, 'a \"do not edit\" banner'],\n];\n\nfunction findGeneratedBoilerplate(\n file: InstructionFile,\n lines: readonly ScannedLine[],\n): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n for (const line of lines) {\n if (line.fenced) continue;\n for (const [pattern, what] of BOILERPLATE_SIGNATURES) {\n if (!pattern.test(line.text)) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: GENERATED_BOILERPLATE_RULE,\n explanation:\n `carries ${what}, which suggests this file was generated rather than ` +\n `written. This is the softest rule here: a signature is circumstantial, ` +\n `and a hand-written file may honestly carry one, so it never means more ` +\n `than \"read this file again\"` +\n citing(GUIDE.neverGenerate),\n },\n });\n break;\n }\n }\n return out;\n}\n\n/* ------------------------------------------------------------------------------------\n * language-rule\n * ---------------------------------------------------------------------------------- */\n\n/** Style rules that pay their every-request cost only when the agent writes code. */\nconst LANGUAGE_RULE_SIGNATURES: readonly (readonly [RegExp, string])[] = [\n [/\\bconst\\b[^\\n]*\\blet\\b|\\blet\\b[^\\n]*\\bconst\\b/, 'a const/let rule'],\n [/\\binterface\\b[^\\n]*\\btype\\b|\\btype\\b[^\\n]*\\binterface\\b/, 'an interface-vs-type rule'],\n [/\\bstrict[- ]?null(?:checks)?\\b/i, 'a strict-null rule'],\n [/\\bsemi-?colons?\\b/i, 'a semicolon rule'],\n [/\\b(?:single|double) quotes\\b/i, 'a quote-style rule'],\n [/\\barrow functions?\\b/i, 'an arrow-function rule'],\n [/\\bnamed exports?\\b|\\bdefault exports?\\b/i, 'an export-style rule'],\n [\n /\\btabs? (?:over|versus|vs\\.?) spaces?\\b|\\bspaces? (?:over|versus|vs\\.?) tabs?\\b/i,\n 'an indentation rule',\n ],\n];\n\nfunction findLanguageRules(file: InstructionFile, lines: readonly ScannedLine[]): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n for (const line of lines) {\n if (line.fenced) continue;\n for (const [pattern, what] of LANGUAGE_RULE_SIGNATURES) {\n if (!pattern.test(line.text)) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: LANGUAGE_RULE_RULE,\n explanation:\n `states ${what}, which is paid for on every request and is relevant on ` +\n `few of them — move it behind a link and it costs only the tasks it applies to` +\n citing(GUIDE.loadWhenRelevant),\n },\n });\n break;\n }\n }\n return out;\n}\n\n/* ------------------------------------------------------------------------------------\n * mirror-drift (R4)\n * ---------------------------------------------------------------------------------- */\n\nfunction standingOf(\n primary: InstructionFile,\n mirror: InstructionFile,\n): AgentsMirrorReport['standing'] {\n if (mirror.symlink) return 'symlink';\n return mirror.text === primary.text ? 'copy' : 'drift';\n}\n\n/**\n * A mirror that has diverged from its `AGENTS.md`.\n *\n * A byte-identical copy is **not** a finding: it is not drift, and calling it one\n * would be smelt enforcing the guide's suggestion rather than reporting a fact. What\n * the report does say, beside every copy, is that a symlink cannot drift — which is\n * the guide's suggestion offered, exactly as `smelt hooks` offers rather than does.\n */\nfunction findMirrorDrift(\n primary: InstructionFile,\n mirrors: readonly InstructionFile[],\n): AgentsFinding[] {\n return mirrors\n .filter((mirror) => standingOf(primary, mirror) === 'drift')\n .map((mirror) => ({\n file: mirror.path,\n line: 1,\n reason: {\n rule: MIRROR_DRIFT_RULE,\n explanation:\n `has diverged from \\`${primary.path}\\` (${String(mirror.bytes)} bytes against ` +\n `${String(primary.bytes)}) — two harnesses are now reading two different sets ` +\n `of instructions from one repository` +\n citing(GUIDE.symlinkMirror),\n },\n }));\n}\n\n/* ------------------------------------------------------------------------------------\n * restated-at-level (R8)\n * ---------------------------------------------------------------------------------- */\n\n/** Below this, a repeated line is a heading or a bullet marker, not an instruction. */\nconst RESTATEMENT_MIN_CHARS = 40;\n\n/**\n * The same instruction present at a level **and at one of its ancestors**.\n *\n * The guide's rule is that a nested file *merges with* the root, so a line written in\n * both is a line the agent is handed twice — paid for twice, and the second copy\n * carrying the risk that only one of them is ever updated. Reported on the deeper file,\n * because that is the copy the ancestor already covers.\n *\n * **Only ancestors.** A merge runs up the tree, never across it: an agent working in\n * `pkg/a` loads the root file and `pkg/a`'s, and never `pkg/b`'s. So a line two\n * siblings happen to share is not a line anybody is handed twice, and reporting it as\n * one would print an explanation about a merge that does not happen — a finding whose\n * sentence is false, which is worse than no finding at all (Law 2).\n */\nfunction findRestatedAcrossLevels(set: InstructionSet): AgentsFinding[] {\n /** dir → the lines that level states, each with the file that states them. */\n const stated = new Map<string, Map<string, string>>();\n const out: AgentsFinding[] = [];\n\n // Levels arrive root-first (`readInstructionSet` sorts by depth), so every ancestor\n // of a level has already been recorded by the time the level is read.\n for (const level of set.levels) {\n const file = level.primary;\n const local = new Map<string, string>();\n stated.set(level.dir, local);\n const ancestors = ancestorDirs(level.dir);\n\n scanLines(file.text).forEach((line) => {\n if (line.fenced) return;\n const normalized = bareText(line.text).toLowerCase().replace(/\\s+/g, ' ');\n if (normalized.length < RESTATEMENT_MIN_CHARS) return;\n if (local.has(normalized)) return;\n local.set(normalized, file.path);\n const earlier = ancestors\n .map((dir) => stated.get(dir)?.get(normalized))\n .find((path) => path !== undefined);\n if (earlier === undefined) return;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: RESTATED_AT_LEVEL_RULE,\n explanation:\n `repeats a line already in \\`${earlier}\\`, which is above it — the levels ` +\n `merge, so the agent is handed this twice and only one copy will be kept ` +\n `up to date` +\n citing(GUIDE.nestedMerge),\n },\n });\n });\n }\n return out;\n}\n"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { InstructionFile } from './instructions.ts';
|
|
2
|
+
/**
|
|
3
|
+
* `smelt agents split` — the **mechanical half** of the guide's refactor, and a plain
|
|
4
|
+
* statement of where the other half is.
|
|
5
|
+
*
|
|
6
|
+
* The guide's advice for a bloated instruction file is to cut it down to a small root
|
|
7
|
+
* that points elsewhere, moving the rest into `docs/`. That advice has two halves, and
|
|
8
|
+
* they are not the same kind of work:
|
|
9
|
+
*
|
|
10
|
+
* - **Mechanical.** Find the sections. Decide what each one's file is called. Move
|
|
11
|
+
* the bytes. Fix every relative link that just moved a directory deeper. Leave a
|
|
12
|
+
* link to each moved file behind. All of this is text manipulation with a right
|
|
13
|
+
* answer, and it is what this module does.
|
|
14
|
+
* - **Judgment.** *Which sections are essential enough to stay in the root?* Which
|
|
15
|
+
* two paragraphs contradict each other? Which rule is too vague to be worth a
|
|
16
|
+
* token? That is a reading of a specific project by someone who knows it, which
|
|
17
|
+
* means a model, which Law 1 forbids smelt from being (ruling R1).
|
|
18
|
+
*
|
|
19
|
+
* So the seam is stated instead of straddled: {@link planSplit} returns a partition
|
|
20
|
+
* anyone can check, and {@link refactorPrompt} returns the guide's own refactor
|
|
21
|
+
* prompt **filled in with this file's actual section headings**, for the user to hand
|
|
22
|
+
* to their own agent. smelt does the part with a right answer and hands over the part
|
|
23
|
+
* that needs a reader — the same shape as the unconfigured rerank stage, and the same
|
|
24
|
+
* reason.
|
|
25
|
+
*
|
|
26
|
+
* Nothing here writes. `cli/agents.ts` owns the confirm-listing discipline
|
|
27
|
+
* (`smelt init`'s, verbatim: every file listed, one final confirm, an existing file
|
|
28
|
+
* never overwritten without a per-file `yes`).
|
|
29
|
+
*/
|
|
30
|
+
/** Where the moved sections go. The guide's own suggested layout. */
|
|
31
|
+
export declare const SPLIT_DIR = "docs";
|
|
32
|
+
/** One `##` section of an instruction file. */
|
|
33
|
+
export interface InstructionSection {
|
|
34
|
+
/** The heading text, without its `##`. */
|
|
35
|
+
readonly title: string;
|
|
36
|
+
/** 1-based line of the heading. */
|
|
37
|
+
readonly line: number;
|
|
38
|
+
/** The section's body, heading excluded, trailing blank lines trimmed. */
|
|
39
|
+
readonly body: string;
|
|
40
|
+
/** UTF-8 bytes of heading + body — what moving this section saves the root file. */
|
|
41
|
+
readonly bytes: number;
|
|
42
|
+
}
|
|
43
|
+
/** One file the split would write, already rendered. */
|
|
44
|
+
export interface SplitFile {
|
|
45
|
+
/** Root-relative, `/`-separated. */
|
|
46
|
+
readonly path: string;
|
|
47
|
+
readonly content: string;
|
|
48
|
+
/** What this file is, for the confirm listing. */
|
|
49
|
+
readonly role: 'root' | 'section';
|
|
50
|
+
}
|
|
51
|
+
/** The whole mechanical proposal. Pure data: nothing on disk has moved. */
|
|
52
|
+
export interface SplitPlan {
|
|
53
|
+
/** The file that was read. */
|
|
54
|
+
readonly source: string;
|
|
55
|
+
/** Its size before. */
|
|
56
|
+
readonly beforeBytes: number;
|
|
57
|
+
/** What the root file would become. */
|
|
58
|
+
readonly afterBytes: number;
|
|
59
|
+
/** The sections found, in document order. */
|
|
60
|
+
readonly sections: readonly InstructionSection[];
|
|
61
|
+
/** Root file first, then one file per moved section. */
|
|
62
|
+
readonly files: readonly SplitFile[];
|
|
63
|
+
/**
|
|
64
|
+
* Why the plan is empty, when it is: a file with no `##` sections has nothing to
|
|
65
|
+
* partition **by**, and inventing a partition would be the judgment half.
|
|
66
|
+
*/
|
|
67
|
+
readonly refusal?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Partition one instruction file into a root and one file per `##` section.
|
|
71
|
+
*
|
|
72
|
+
* The partition is **by heading and nothing else**. That is the honest mechanical
|
|
73
|
+
* rule: a split by heading is reproducible, reversible by hand, and obviously not a
|
|
74
|
+
* judgment about what matters — which is exactly what it must not pretend to be.
|
|
75
|
+
*
|
|
76
|
+
* The preamble (everything above the first `##`) stays in the root, because in a
|
|
77
|
+
* file written to the guide's shape the preamble *is* the essentials: the title and
|
|
78
|
+
* the one-sentence description.
|
|
79
|
+
*/
|
|
80
|
+
export declare function planSplit(file: InstructionFile): SplitPlan;
|
|
81
|
+
/**
|
|
82
|
+
* Every `##` section, fences respected.
|
|
83
|
+
*
|
|
84
|
+
* A `## ` inside a fenced block is a Markdown example or a shell comment, not a
|
|
85
|
+
* heading — splitting on one would cut a code block in half, which is the kind of
|
|
86
|
+
* damage that makes a tool untrustworthy the first time it happens.
|
|
87
|
+
*/
|
|
88
|
+
export declare function readSections(text: string): readonly InstructionSection[];
|
|
89
|
+
/**
|
|
90
|
+
* Fix the relative links in a section that is moving one directory down.
|
|
91
|
+
*
|
|
92
|
+
* A section that said `[the gate](CONTRIBUTING.md)` at the root must say
|
|
93
|
+
* `../CONTRIBUTING.md` from `docs/`, or the split turns a working pointer into a
|
|
94
|
+
* `dead-link` — the lint's own flagship finding, minted by the tool that fixes it.
|
|
95
|
+
* External links, anchors and paths that already climb out are left alone.
|
|
96
|
+
*/
|
|
97
|
+
export declare function rewriteLinks(body: string): string;
|
|
98
|
+
/**
|
|
99
|
+
* The guide's refactor prompt, filled in with this file's real sections.
|
|
100
|
+
*
|
|
101
|
+
* This is the seam, printed. smelt will not decide which of somebody's sections are
|
|
102
|
+
* essential — that is a reading of their project, and Law 1 keeps smelt offline and
|
|
103
|
+
* modelless — so it hands over the guide's own five-step prompt with the section list
|
|
104
|
+
* already substituted in, ready to paste into whichever agent the user is running.
|
|
105
|
+
*
|
|
106
|
+
* The five steps are the guide's; the wording is smelt's summary of them, with the
|
|
107
|
+
* article cited so the reader can check it. The list of sections is measured, not
|
|
108
|
+
* invented: every heading and every byte count comes from the file on disk.
|
|
109
|
+
*/
|
|
110
|
+
export declare function refactorPrompt(plan: SplitPlan): string;
|
|
111
|
+
/** The one sentence that says where smelt stops. Printed by the CLI, and in its help. */
|
|
112
|
+
export declare function splitSeamNotice(): string;
|
|
113
|
+
//# sourceMappingURL=split.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"split.d.ts","sourceRoot":"","sources":["../../src/agents/split.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,qEAAqE;AACrE,eAAO,MAAM,SAAS,SAAS,CAAC;AAEhC,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mCAAmC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,wDAAwD;AACxD,MAAM,WAAW,SAAS;IACxB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED,2EAA2E;AAC3E,MAAM,WAAW,SAAS;IACxB,8BAA8B;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uBAAuB;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,uCAAuC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACjD,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,CAyC1D;AAMD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,kBAAkB,EAAE,CA+BxE;AAgCD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYjD;AAwBD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CA8BtD;AAED,yFAAyF;AACzF,wBAAgB,eAAe,IAAI,MAAM,CASxC","sourcesContent":["import { citing, GUIDE, GUIDE_URL } from './guide.ts';\nimport type { InstructionFile } from './instructions.ts';\n\n/**\n * `smelt agents split` — the **mechanical half** of the guide's refactor, and a plain\n * statement of where the other half is.\n *\n * The guide's advice for a bloated instruction file is to cut it down to a small root\n * that points elsewhere, moving the rest into `docs/`. That advice has two halves, and\n * they are not the same kind of work:\n *\n * - **Mechanical.** Find the sections. Decide what each one's file is called. Move\n * the bytes. Fix every relative link that just moved a directory deeper. Leave a\n * link to each moved file behind. All of this is text manipulation with a right\n * answer, and it is what this module does.\n * - **Judgment.** *Which sections are essential enough to stay in the root?* Which\n * two paragraphs contradict each other? Which rule is too vague to be worth a\n * token? That is a reading of a specific project by someone who knows it, which\n * means a model, which Law 1 forbids smelt from being (ruling R1).\n *\n * So the seam is stated instead of straddled: {@link planSplit} returns a partition\n * anyone can check, and {@link refactorPrompt} returns the guide's own refactor\n * prompt **filled in with this file's actual section headings**, for the user to hand\n * to their own agent. smelt does the part with a right answer and hands over the part\n * that needs a reader — the same shape as the unconfigured rerank stage, and the same\n * reason.\n *\n * Nothing here writes. `cli/agents.ts` owns the confirm-listing discipline\n * (`smelt init`'s, verbatim: every file listed, one final confirm, an existing file\n * never overwritten without a per-file `yes`).\n */\n\n/** Where the moved sections go. The guide's own suggested layout. */\nexport const SPLIT_DIR = 'docs';\n\n/** One `##` section of an instruction file. */\nexport interface InstructionSection {\n /** The heading text, without its `##`. */\n readonly title: string;\n /** 1-based line of the heading. */\n readonly line: number;\n /** The section's body, heading excluded, trailing blank lines trimmed. */\n readonly body: string;\n /** UTF-8 bytes of heading + body — what moving this section saves the root file. */\n readonly bytes: number;\n}\n\n/** One file the split would write, already rendered. */\nexport interface SplitFile {\n /** Root-relative, `/`-separated. */\n readonly path: string;\n readonly content: string;\n /** What this file is, for the confirm listing. */\n readonly role: 'root' | 'section';\n}\n\n/** The whole mechanical proposal. Pure data: nothing on disk has moved. */\nexport interface SplitPlan {\n /** The file that was read. */\n readonly source: string;\n /** Its size before. */\n readonly beforeBytes: number;\n /** What the root file would become. */\n readonly afterBytes: number;\n /** The sections found, in document order. */\n readonly sections: readonly InstructionSection[];\n /** Root file first, then one file per moved section. */\n readonly files: readonly SplitFile[];\n /**\n * Why the plan is empty, when it is: a file with no `##` sections has nothing to\n * partition **by**, and inventing a partition would be the judgment half.\n */\n readonly refusal?: string;\n}\n\n/**\n * Partition one instruction file into a root and one file per `##` section.\n *\n * The partition is **by heading and nothing else**. That is the honest mechanical\n * rule: a split by heading is reproducible, reversible by hand, and obviously not a\n * judgment about what matters — which is exactly what it must not pretend to be.\n *\n * The preamble (everything above the first `##`) stays in the root, because in a\n * file written to the guide's shape the preamble *is* the essentials: the title and\n * the one-sentence description.\n */\nexport function planSplit(file: InstructionFile): SplitPlan {\n const sections = readSections(file.text);\n const before = file.bytes;\n\n if (sections.length === 0) {\n return {\n source: file.path,\n beforeBytes: before,\n afterBytes: before,\n sections: [],\n files: [],\n refusal:\n `${file.path} has no \\`##\\` sections, so there is nothing to partition by. ` +\n `A split invented without headings would be smelt deciding what matters, ` +\n `which is the half of the refactor it deliberately does not do.`,\n };\n }\n\n const used = new Set<string>();\n const moved = sections.map((section) => {\n const path = `${SPLIT_DIR}/${uniqueSlug(section.title, used)}.md`;\n return { section, path };\n });\n\n const rootContent = renderRoot(file.text, sections, moved);\n const files: SplitFile[] = [\n { path: file.path, content: rootContent, role: 'root' },\n ...moved.map(({ section, path }): SplitFile => ({\n path,\n content: renderSection(section),\n role: 'section',\n })),\n ];\n\n return {\n source: file.path,\n beforeBytes: before,\n afterBytes: Buffer.byteLength(rootContent, 'utf8'),\n sections,\n files,\n };\n}\n\n/* ------------------------------------------------------------------------------------\n * Reading sections\n * ---------------------------------------------------------------------------------- */\n\n/**\n * Every `##` section, fences respected.\n *\n * A `## ` inside a fenced block is a Markdown example or a shell comment, not a\n * heading — splitting on one would cut a code block in half, which is the kind of\n * damage that makes a tool untrustworthy the first time it happens.\n */\nexport function readSections(text: string): readonly InstructionSection[] {\n const lines = text.split('\\n');\n const starts: { readonly index: number; readonly title: string }[] = [];\n let fence: string | undefined;\n\n lines.forEach((line, index) => {\n const marker = /^\\s{0,3}(`{3,}|~{3,})/.exec(line);\n if (marker !== null) {\n if (fence === undefined) fence = marker[1]!.slice(0, 1);\n else if (marker[1]!.startsWith(fence)) fence = undefined;\n return;\n }\n if (fence !== undefined) return;\n const heading = /^##\\s+(.+?)\\s*$/.exec(line);\n if (heading !== null) starts.push({ index, title: heading[1]! });\n });\n\n return starts.map(({ index, title }, position) => {\n const end = starts[position + 1]?.index ?? lines.length;\n const body = lines\n .slice(index + 1, end)\n .join('\\n')\n .replace(/^\\n+/, '')\n .replace(/\\n+$/, '');\n return {\n title,\n line: index + 1,\n body,\n bytes: Buffer.byteLength(`${lines[index]!}\\n${body}\\n`, 'utf8'),\n };\n });\n}\n\n/* ------------------------------------------------------------------------------------\n * Rendering\n * ---------------------------------------------------------------------------------- */\n\n/** The moved section as its own file: the heading promoted to `#`, links rewritten. */\nfunction renderSection(section: InstructionSection): string {\n return `# ${section.title}\\n\\n${rewriteLinks(section.body)}\\n`;\n}\n\n/**\n * The root file: everything above the first `##`, then one link per moved section.\n *\n * The link list is the point of the whole exercise — the guide's ideal root file is\n * small and points elsewhere — so it is written plainly, one line per section, with\n * the section's own title as the link text.\n */\nfunction renderRoot(\n text: string,\n sections: readonly InstructionSection[],\n moved: readonly { readonly section: InstructionSection; readonly path: string }[],\n): string {\n const lines = text.split('\\n');\n const preamble = lines\n .slice(0, (sections[0]?.line ?? lines.length + 1) - 1)\n .join('\\n')\n .replace(/\\n+$/, '');\n const links = moved.map(({ section, path }) => `- [${section.title}](${path})`).join('\\n');\n return `${preamble}\\n\\n## More\\n\\n${links}\\n`;\n}\n\n/**\n * Fix the relative links in a section that is moving one directory down.\n *\n * A section that said `[the gate](CONTRIBUTING.md)` at the root must say\n * `../CONTRIBUTING.md` from `docs/`, or the split turns a working pointer into a\n * `dead-link` — the lint's own flagship finding, minted by the tool that fixes it.\n * External links, anchors and paths that already climb out are left alone.\n */\nexport function rewriteLinks(body: string): string {\n return body.replace(\n /(\\[[^\\]]*\\]\\()([^)\\s]+)(\\))/g,\n (whole, open: string, target: string, close: string) => {\n if (/^[a-z][a-z0-9+.-]*:/i.test(target)) return whole;\n if (target.startsWith('#') || target.startsWith('/') || target.startsWith('..')) return whole;\n const cleaned = target.startsWith('./') ? target.slice(2) : target;\n if (cleaned.startsWith(`${SPLIT_DIR}/`))\n return `${open}${cleaned.slice(SPLIT_DIR.length + 1)}${close}`;\n return `${open}../${cleaned}${close}`;\n },\n );\n}\n\n/** `Build & test` → `build-test`, made unique within one plan. */\nfunction uniqueSlug(title: string, used: Set<string>): string {\n const base =\n title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40) || 'section';\n let slug = base;\n let counter = 2;\n while (used.has(slug)) {\n slug = `${base}-${String(counter)}`;\n counter += 1;\n }\n used.add(slug);\n return slug;\n}\n\n/* ------------------------------------------------------------------------------------\n * The judgment half, handed over\n * ---------------------------------------------------------------------------------- */\n\n/**\n * The guide's refactor prompt, filled in with this file's real sections.\n *\n * This is the seam, printed. smelt will not decide which of somebody's sections are\n * essential — that is a reading of their project, and Law 1 keeps smelt offline and\n * modelless — so it hands over the guide's own five-step prompt with the section list\n * already substituted in, ready to paste into whichever agent the user is running.\n *\n * The five steps are the guide's; the wording is smelt's summary of them, with the\n * article cited so the reader can check it. The list of sections is measured, not\n * invented: every heading and every byte count comes from the file on disk.\n */\nexport function refactorPrompt(plan: SplitPlan): string {\n const inventory = plan.sections\n .map(\n (section) =>\n ` - \"${section.title}\" (${String(section.bytes)} bytes, line ${String(section.line)})`,\n )\n .join('\\n');\n\n return [\n `Here is my ${plan.source} (${String(plan.beforeBytes)} bytes). Its \\`##\\` sections are:`,\n '',\n inventory,\n '',\n `Refactor it, following the five steps from ${GUIDE_URL}:`,\n '',\n ' 1. Find contradictions. Where two instructions conflict, show me both and ask',\n ' which one to keep — do not pick for me.',\n ' 2. Identify the essentials for the root file: a one-sentence project description,',\n ' the package manager if it is not npm, the build and typecheck commands if they',\n ' are non-standard, and anything genuinely relevant to every task.',\n ' 3. Group the rest into categories (TypeScript, testing, API design, git workflow),',\n ' one file per category.',\n ' 4. Create the file structure: a minimal root that links to each file, and the',\n ' files themselves under docs/.',\n ' 5. Flag for deletion anything redundant, too vague to be actionable, or so obvious',\n ' it is not worth a token on every request.',\n '',\n `Why this matters: ${GUIDE.loadsEveryRequest}.`,\n '',\n ].join('\\n');\n}\n\n/** The one sentence that says where smelt stops. Printed by the CLI, and in its help. */\nexport function splitSeamNotice(): string {\n return (\n `smelt did the mechanical half: it partitioned by heading, rewrote the links that ` +\n `moved a directory, and wrote nothing without asking. It did NOT decide which ` +\n `sections are essential — that is a reading of your project, so it needs a model, ` +\n `and smelt has none by law. The prompt above is the judgment half, filled in with ` +\n `your real sections; hand it to your own agent` +\n citing(GUIDE.pointsElsewhere)\n );\n}\n"]}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { citing, GUIDE, GUIDE_URL } from './guide.js';
|
|
2
|
+
/**
|
|
3
|
+
* `smelt agents split` — the **mechanical half** of the guide's refactor, and a plain
|
|
4
|
+
* statement of where the other half is.
|
|
5
|
+
*
|
|
6
|
+
* The guide's advice for a bloated instruction file is to cut it down to a small root
|
|
7
|
+
* that points elsewhere, moving the rest into `docs/`. That advice has two halves, and
|
|
8
|
+
* they are not the same kind of work:
|
|
9
|
+
*
|
|
10
|
+
* - **Mechanical.** Find the sections. Decide what each one's file is called. Move
|
|
11
|
+
* the bytes. Fix every relative link that just moved a directory deeper. Leave a
|
|
12
|
+
* link to each moved file behind. All of this is text manipulation with a right
|
|
13
|
+
* answer, and it is what this module does.
|
|
14
|
+
* - **Judgment.** *Which sections are essential enough to stay in the root?* Which
|
|
15
|
+
* two paragraphs contradict each other? Which rule is too vague to be worth a
|
|
16
|
+
* token? That is a reading of a specific project by someone who knows it, which
|
|
17
|
+
* means a model, which Law 1 forbids smelt from being (ruling R1).
|
|
18
|
+
*
|
|
19
|
+
* So the seam is stated instead of straddled: {@link planSplit} returns a partition
|
|
20
|
+
* anyone can check, and {@link refactorPrompt} returns the guide's own refactor
|
|
21
|
+
* prompt **filled in with this file's actual section headings**, for the user to hand
|
|
22
|
+
* to their own agent. smelt does the part with a right answer and hands over the part
|
|
23
|
+
* that needs a reader — the same shape as the unconfigured rerank stage, and the same
|
|
24
|
+
* reason.
|
|
25
|
+
*
|
|
26
|
+
* Nothing here writes. `cli/agents.ts` owns the confirm-listing discipline
|
|
27
|
+
* (`smelt init`'s, verbatim: every file listed, one final confirm, an existing file
|
|
28
|
+
* never overwritten without a per-file `yes`).
|
|
29
|
+
*/
|
|
30
|
+
/** Where the moved sections go. The guide's own suggested layout. */
|
|
31
|
+
export const SPLIT_DIR = 'docs';
|
|
32
|
+
/**
|
|
33
|
+
* Partition one instruction file into a root and one file per `##` section.
|
|
34
|
+
*
|
|
35
|
+
* The partition is **by heading and nothing else**. That is the honest mechanical
|
|
36
|
+
* rule: a split by heading is reproducible, reversible by hand, and obviously not a
|
|
37
|
+
* judgment about what matters — which is exactly what it must not pretend to be.
|
|
38
|
+
*
|
|
39
|
+
* The preamble (everything above the first `##`) stays in the root, because in a
|
|
40
|
+
* file written to the guide's shape the preamble *is* the essentials: the title and
|
|
41
|
+
* the one-sentence description.
|
|
42
|
+
*/
|
|
43
|
+
export function planSplit(file) {
|
|
44
|
+
const sections = readSections(file.text);
|
|
45
|
+
const before = file.bytes;
|
|
46
|
+
if (sections.length === 0) {
|
|
47
|
+
return {
|
|
48
|
+
source: file.path,
|
|
49
|
+
beforeBytes: before,
|
|
50
|
+
afterBytes: before,
|
|
51
|
+
sections: [],
|
|
52
|
+
files: [],
|
|
53
|
+
refusal: `${file.path} has no \`##\` sections, so there is nothing to partition by. ` +
|
|
54
|
+
`A split invented without headings would be smelt deciding what matters, ` +
|
|
55
|
+
`which is the half of the refactor it deliberately does not do.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const used = new Set();
|
|
59
|
+
const moved = sections.map((section) => {
|
|
60
|
+
const path = `${SPLIT_DIR}/${uniqueSlug(section.title, used)}.md`;
|
|
61
|
+
return { section, path };
|
|
62
|
+
});
|
|
63
|
+
const rootContent = renderRoot(file.text, sections, moved);
|
|
64
|
+
const files = [
|
|
65
|
+
{ path: file.path, content: rootContent, role: 'root' },
|
|
66
|
+
...moved.map(({ section, path }) => ({
|
|
67
|
+
path,
|
|
68
|
+
content: renderSection(section),
|
|
69
|
+
role: 'section',
|
|
70
|
+
})),
|
|
71
|
+
];
|
|
72
|
+
return {
|
|
73
|
+
source: file.path,
|
|
74
|
+
beforeBytes: before,
|
|
75
|
+
afterBytes: Buffer.byteLength(rootContent, 'utf8'),
|
|
76
|
+
sections,
|
|
77
|
+
files,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/* ------------------------------------------------------------------------------------
|
|
81
|
+
* Reading sections
|
|
82
|
+
* ---------------------------------------------------------------------------------- */
|
|
83
|
+
/**
|
|
84
|
+
* Every `##` section, fences respected.
|
|
85
|
+
*
|
|
86
|
+
* A `## ` inside a fenced block is a Markdown example or a shell comment, not a
|
|
87
|
+
* heading — splitting on one would cut a code block in half, which is the kind of
|
|
88
|
+
* damage that makes a tool untrustworthy the first time it happens.
|
|
89
|
+
*/
|
|
90
|
+
export function readSections(text) {
|
|
91
|
+
const lines = text.split('\n');
|
|
92
|
+
const starts = [];
|
|
93
|
+
let fence;
|
|
94
|
+
lines.forEach((line, index) => {
|
|
95
|
+
const marker = /^\s{0,3}(`{3,}|~{3,})/.exec(line);
|
|
96
|
+
if (marker !== null) {
|
|
97
|
+
if (fence === undefined)
|
|
98
|
+
fence = marker[1].slice(0, 1);
|
|
99
|
+
else if (marker[1].startsWith(fence))
|
|
100
|
+
fence = undefined;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (fence !== undefined)
|
|
104
|
+
return;
|
|
105
|
+
const heading = /^##\s+(.+?)\s*$/.exec(line);
|
|
106
|
+
if (heading !== null)
|
|
107
|
+
starts.push({ index, title: heading[1] });
|
|
108
|
+
});
|
|
109
|
+
return starts.map(({ index, title }, position) => {
|
|
110
|
+
const end = starts[position + 1]?.index ?? lines.length;
|
|
111
|
+
const body = lines
|
|
112
|
+
.slice(index + 1, end)
|
|
113
|
+
.join('\n')
|
|
114
|
+
.replace(/^\n+/, '')
|
|
115
|
+
.replace(/\n+$/, '');
|
|
116
|
+
return {
|
|
117
|
+
title,
|
|
118
|
+
line: index + 1,
|
|
119
|
+
body,
|
|
120
|
+
bytes: Buffer.byteLength(`${lines[index]}\n${body}\n`, 'utf8'),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/* ------------------------------------------------------------------------------------
|
|
125
|
+
* Rendering
|
|
126
|
+
* ---------------------------------------------------------------------------------- */
|
|
127
|
+
/** The moved section as its own file: the heading promoted to `#`, links rewritten. */
|
|
128
|
+
function renderSection(section) {
|
|
129
|
+
return `# ${section.title}\n\n${rewriteLinks(section.body)}\n`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The root file: everything above the first `##`, then one link per moved section.
|
|
133
|
+
*
|
|
134
|
+
* The link list is the point of the whole exercise — the guide's ideal root file is
|
|
135
|
+
* small and points elsewhere — so it is written plainly, one line per section, with
|
|
136
|
+
* the section's own title as the link text.
|
|
137
|
+
*/
|
|
138
|
+
function renderRoot(text, sections, moved) {
|
|
139
|
+
const lines = text.split('\n');
|
|
140
|
+
const preamble = lines
|
|
141
|
+
.slice(0, (sections[0]?.line ?? lines.length + 1) - 1)
|
|
142
|
+
.join('\n')
|
|
143
|
+
.replace(/\n+$/, '');
|
|
144
|
+
const links = moved.map(({ section, path }) => `- [${section.title}](${path})`).join('\n');
|
|
145
|
+
return `${preamble}\n\n## More\n\n${links}\n`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fix the relative links in a section that is moving one directory down.
|
|
149
|
+
*
|
|
150
|
+
* A section that said `[the gate](CONTRIBUTING.md)` at the root must say
|
|
151
|
+
* `../CONTRIBUTING.md` from `docs/`, or the split turns a working pointer into a
|
|
152
|
+
* `dead-link` — the lint's own flagship finding, minted by the tool that fixes it.
|
|
153
|
+
* External links, anchors and paths that already climb out are left alone.
|
|
154
|
+
*/
|
|
155
|
+
export function rewriteLinks(body) {
|
|
156
|
+
return body.replace(/(\[[^\]]*\]\()([^)\s]+)(\))/g, (whole, open, target, close) => {
|
|
157
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target))
|
|
158
|
+
return whole;
|
|
159
|
+
if (target.startsWith('#') || target.startsWith('/') || target.startsWith('..'))
|
|
160
|
+
return whole;
|
|
161
|
+
const cleaned = target.startsWith('./') ? target.slice(2) : target;
|
|
162
|
+
if (cleaned.startsWith(`${SPLIT_DIR}/`))
|
|
163
|
+
return `${open}${cleaned.slice(SPLIT_DIR.length + 1)}${close}`;
|
|
164
|
+
return `${open}../${cleaned}${close}`;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** `Build & test` → `build-test`, made unique within one plan. */
|
|
168
|
+
function uniqueSlug(title, used) {
|
|
169
|
+
const base = title
|
|
170
|
+
.toLowerCase()
|
|
171
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
172
|
+
.replace(/^-+|-+$/g, '')
|
|
173
|
+
.slice(0, 40) || 'section';
|
|
174
|
+
let slug = base;
|
|
175
|
+
let counter = 2;
|
|
176
|
+
while (used.has(slug)) {
|
|
177
|
+
slug = `${base}-${String(counter)}`;
|
|
178
|
+
counter += 1;
|
|
179
|
+
}
|
|
180
|
+
used.add(slug);
|
|
181
|
+
return slug;
|
|
182
|
+
}
|
|
183
|
+
/* ------------------------------------------------------------------------------------
|
|
184
|
+
* The judgment half, handed over
|
|
185
|
+
* ---------------------------------------------------------------------------------- */
|
|
186
|
+
/**
|
|
187
|
+
* The guide's refactor prompt, filled in with this file's real sections.
|
|
188
|
+
*
|
|
189
|
+
* This is the seam, printed. smelt will not decide which of somebody's sections are
|
|
190
|
+
* essential — that is a reading of their project, and Law 1 keeps smelt offline and
|
|
191
|
+
* modelless — so it hands over the guide's own five-step prompt with the section list
|
|
192
|
+
* already substituted in, ready to paste into whichever agent the user is running.
|
|
193
|
+
*
|
|
194
|
+
* The five steps are the guide's; the wording is smelt's summary of them, with the
|
|
195
|
+
* article cited so the reader can check it. The list of sections is measured, not
|
|
196
|
+
* invented: every heading and every byte count comes from the file on disk.
|
|
197
|
+
*/
|
|
198
|
+
export function refactorPrompt(plan) {
|
|
199
|
+
const inventory = plan.sections
|
|
200
|
+
.map((section) => ` - "${section.title}" (${String(section.bytes)} bytes, line ${String(section.line)})`)
|
|
201
|
+
.join('\n');
|
|
202
|
+
return [
|
|
203
|
+
`Here is my ${plan.source} (${String(plan.beforeBytes)} bytes). Its \`##\` sections are:`,
|
|
204
|
+
'',
|
|
205
|
+
inventory,
|
|
206
|
+
'',
|
|
207
|
+
`Refactor it, following the five steps from ${GUIDE_URL}:`,
|
|
208
|
+
'',
|
|
209
|
+
' 1. Find contradictions. Where two instructions conflict, show me both and ask',
|
|
210
|
+
' which one to keep — do not pick for me.',
|
|
211
|
+
' 2. Identify the essentials for the root file: a one-sentence project description,',
|
|
212
|
+
' the package manager if it is not npm, the build and typecheck commands if they',
|
|
213
|
+
' are non-standard, and anything genuinely relevant to every task.',
|
|
214
|
+
' 3. Group the rest into categories (TypeScript, testing, API design, git workflow),',
|
|
215
|
+
' one file per category.',
|
|
216
|
+
' 4. Create the file structure: a minimal root that links to each file, and the',
|
|
217
|
+
' files themselves under docs/.',
|
|
218
|
+
' 5. Flag for deletion anything redundant, too vague to be actionable, or so obvious',
|
|
219
|
+
' it is not worth a token on every request.',
|
|
220
|
+
'',
|
|
221
|
+
`Why this matters: ${GUIDE.loadsEveryRequest}.`,
|
|
222
|
+
'',
|
|
223
|
+
].join('\n');
|
|
224
|
+
}
|
|
225
|
+
/** The one sentence that says where smelt stops. Printed by the CLI, and in its help. */
|
|
226
|
+
export function splitSeamNotice() {
|
|
227
|
+
return (`smelt did the mechanical half: it partitioned by heading, rewrote the links that ` +
|
|
228
|
+
`moved a directory, and wrote nothing without asking. It did NOT decide which ` +
|
|
229
|
+
`sections are essential — that is a reading of your project, so it needs a model, ` +
|
|
230
|
+
`and smelt has none by law. The prompt above is the judgment half, filled in with ` +
|
|
231
|
+
`your real sections; hand it to your own agent` +
|
|
232
|
+
citing(GUIDE.pointsElsewhere));
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=split.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"split.js","sourceRoot":"","sources":["../../src/agents/split.ts"],"names":[],"mappings":"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","sourcesContent":["import { citing, GUIDE, GUIDE_URL } from './guide.ts';\nimport type { InstructionFile } from './instructions.ts';\n\n/**\n * `smelt agents split` — the **mechanical half** of the guide's refactor, and a plain\n * statement of where the other half is.\n *\n * The guide's advice for a bloated instruction file is to cut it down to a small root\n * that points elsewhere, moving the rest into `docs/`. That advice has two halves, and\n * they are not the same kind of work:\n *\n * - **Mechanical.** Find the sections. Decide what each one's file is called. Move\n * the bytes. Fix every relative link that just moved a directory deeper. Leave a\n * link to each moved file behind. All of this is text manipulation with a right\n * answer, and it is what this module does.\n * - **Judgment.** *Which sections are essential enough to stay in the root?* Which\n * two paragraphs contradict each other? Which rule is too vague to be worth a\n * token? That is a reading of a specific project by someone who knows it, which\n * means a model, which Law 1 forbids smelt from being (ruling R1).\n *\n * So the seam is stated instead of straddled: {@link planSplit} returns a partition\n * anyone can check, and {@link refactorPrompt} returns the guide's own refactor\n * prompt **filled in with this file's actual section headings**, for the user to hand\n * to their own agent. smelt does the part with a right answer and hands over the part\n * that needs a reader — the same shape as the unconfigured rerank stage, and the same\n * reason.\n *\n * Nothing here writes. `cli/agents.ts` owns the confirm-listing discipline\n * (`smelt init`'s, verbatim: every file listed, one final confirm, an existing file\n * never overwritten without a per-file `yes`).\n */\n\n/** Where the moved sections go. The guide's own suggested layout. */\nexport const SPLIT_DIR = 'docs';\n\n/** One `##` section of an instruction file. */\nexport interface InstructionSection {\n /** The heading text, without its `##`. */\n readonly title: string;\n /** 1-based line of the heading. */\n readonly line: number;\n /** The section's body, heading excluded, trailing blank lines trimmed. */\n readonly body: string;\n /** UTF-8 bytes of heading + body — what moving this section saves the root file. */\n readonly bytes: number;\n}\n\n/** One file the split would write, already rendered. */\nexport interface SplitFile {\n /** Root-relative, `/`-separated. */\n readonly path: string;\n readonly content: string;\n /** What this file is, for the confirm listing. */\n readonly role: 'root' | 'section';\n}\n\n/** The whole mechanical proposal. Pure data: nothing on disk has moved. */\nexport interface SplitPlan {\n /** The file that was read. */\n readonly source: string;\n /** Its size before. */\n readonly beforeBytes: number;\n /** What the root file would become. */\n readonly afterBytes: number;\n /** The sections found, in document order. */\n readonly sections: readonly InstructionSection[];\n /** Root file first, then one file per moved section. */\n readonly files: readonly SplitFile[];\n /**\n * Why the plan is empty, when it is: a file with no `##` sections has nothing to\n * partition **by**, and inventing a partition would be the judgment half.\n */\n readonly refusal?: string;\n}\n\n/**\n * Partition one instruction file into a root and one file per `##` section.\n *\n * The partition is **by heading and nothing else**. That is the honest mechanical\n * rule: a split by heading is reproducible, reversible by hand, and obviously not a\n * judgment about what matters — which is exactly what it must not pretend to be.\n *\n * The preamble (everything above the first `##`) stays in the root, because in a\n * file written to the guide's shape the preamble *is* the essentials: the title and\n * the one-sentence description.\n */\nexport function planSplit(file: InstructionFile): SplitPlan {\n const sections = readSections(file.text);\n const before = file.bytes;\n\n if (sections.length === 0) {\n return {\n source: file.path,\n beforeBytes: before,\n afterBytes: before,\n sections: [],\n files: [],\n refusal:\n `${file.path} has no \\`##\\` sections, so there is nothing to partition by. ` +\n `A split invented without headings would be smelt deciding what matters, ` +\n `which is the half of the refactor it deliberately does not do.`,\n };\n }\n\n const used = new Set<string>();\n const moved = sections.map((section) => {\n const path = `${SPLIT_DIR}/${uniqueSlug(section.title, used)}.md`;\n return { section, path };\n });\n\n const rootContent = renderRoot(file.text, sections, moved);\n const files: SplitFile[] = [\n { path: file.path, content: rootContent, role: 'root' },\n ...moved.map(({ section, path }): SplitFile => ({\n path,\n content: renderSection(section),\n role: 'section',\n })),\n ];\n\n return {\n source: file.path,\n beforeBytes: before,\n afterBytes: Buffer.byteLength(rootContent, 'utf8'),\n sections,\n files,\n };\n}\n\n/* ------------------------------------------------------------------------------------\n * Reading sections\n * ---------------------------------------------------------------------------------- */\n\n/**\n * Every `##` section, fences respected.\n *\n * A `## ` inside a fenced block is a Markdown example or a shell comment, not a\n * heading — splitting on one would cut a code block in half, which is the kind of\n * damage that makes a tool untrustworthy the first time it happens.\n */\nexport function readSections(text: string): readonly InstructionSection[] {\n const lines = text.split('\\n');\n const starts: { readonly index: number; readonly title: string }[] = [];\n let fence: string | undefined;\n\n lines.forEach((line, index) => {\n const marker = /^\\s{0,3}(`{3,}|~{3,})/.exec(line);\n if (marker !== null) {\n if (fence === undefined) fence = marker[1]!.slice(0, 1);\n else if (marker[1]!.startsWith(fence)) fence = undefined;\n return;\n }\n if (fence !== undefined) return;\n const heading = /^##\\s+(.+?)\\s*$/.exec(line);\n if (heading !== null) starts.push({ index, title: heading[1]! });\n });\n\n return starts.map(({ index, title }, position) => {\n const end = starts[position + 1]?.index ?? lines.length;\n const body = lines\n .slice(index + 1, end)\n .join('\\n')\n .replace(/^\\n+/, '')\n .replace(/\\n+$/, '');\n return {\n title,\n line: index + 1,\n body,\n bytes: Buffer.byteLength(`${lines[index]!}\\n${body}\\n`, 'utf8'),\n };\n });\n}\n\n/* ------------------------------------------------------------------------------------\n * Rendering\n * ---------------------------------------------------------------------------------- */\n\n/** The moved section as its own file: the heading promoted to `#`, links rewritten. */\nfunction renderSection(section: InstructionSection): string {\n return `# ${section.title}\\n\\n${rewriteLinks(section.body)}\\n`;\n}\n\n/**\n * The root file: everything above the first `##`, then one link per moved section.\n *\n * The link list is the point of the whole exercise — the guide's ideal root file is\n * small and points elsewhere — so it is written plainly, one line per section, with\n * the section's own title as the link text.\n */\nfunction renderRoot(\n text: string,\n sections: readonly InstructionSection[],\n moved: readonly { readonly section: InstructionSection; readonly path: string }[],\n): string {\n const lines = text.split('\\n');\n const preamble = lines\n .slice(0, (sections[0]?.line ?? lines.length + 1) - 1)\n .join('\\n')\n .replace(/\\n+$/, '');\n const links = moved.map(({ section, path }) => `- [${section.title}](${path})`).join('\\n');\n return `${preamble}\\n\\n## More\\n\\n${links}\\n`;\n}\n\n/**\n * Fix the relative links in a section that is moving one directory down.\n *\n * A section that said `[the gate](CONTRIBUTING.md)` at the root must say\n * `../CONTRIBUTING.md` from `docs/`, or the split turns a working pointer into a\n * `dead-link` — the lint's own flagship finding, minted by the tool that fixes it.\n * External links, anchors and paths that already climb out are left alone.\n */\nexport function rewriteLinks(body: string): string {\n return body.replace(\n /(\\[[^\\]]*\\]\\()([^)\\s]+)(\\))/g,\n (whole, open: string, target: string, close: string) => {\n if (/^[a-z][a-z0-9+.-]*:/i.test(target)) return whole;\n if (target.startsWith('#') || target.startsWith('/') || target.startsWith('..')) return whole;\n const cleaned = target.startsWith('./') ? target.slice(2) : target;\n if (cleaned.startsWith(`${SPLIT_DIR}/`))\n return `${open}${cleaned.slice(SPLIT_DIR.length + 1)}${close}`;\n return `${open}../${cleaned}${close}`;\n },\n );\n}\n\n/** `Build & test` → `build-test`, made unique within one plan. */\nfunction uniqueSlug(title: string, used: Set<string>): string {\n const base =\n title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40) || 'section';\n let slug = base;\n let counter = 2;\n while (used.has(slug)) {\n slug = `${base}-${String(counter)}`;\n counter += 1;\n }\n used.add(slug);\n return slug;\n}\n\n/* ------------------------------------------------------------------------------------\n * The judgment half, handed over\n * ---------------------------------------------------------------------------------- */\n\n/**\n * The guide's refactor prompt, filled in with this file's real sections.\n *\n * This is the seam, printed. smelt will not decide which of somebody's sections are\n * essential — that is a reading of their project, and Law 1 keeps smelt offline and\n * modelless — so it hands over the guide's own five-step prompt with the section list\n * already substituted in, ready to paste into whichever agent the user is running.\n *\n * The five steps are the guide's; the wording is smelt's summary of them, with the\n * article cited so the reader can check it. The list of sections is measured, not\n * invented: every heading and every byte count comes from the file on disk.\n */\nexport function refactorPrompt(plan: SplitPlan): string {\n const inventory = plan.sections\n .map(\n (section) =>\n ` - \"${section.title}\" (${String(section.bytes)} bytes, line ${String(section.line)})`,\n )\n .join('\\n');\n\n return [\n `Here is my ${plan.source} (${String(plan.beforeBytes)} bytes). Its \\`##\\` sections are:`,\n '',\n inventory,\n '',\n `Refactor it, following the five steps from ${GUIDE_URL}:`,\n '',\n ' 1. Find contradictions. Where two instructions conflict, show me both and ask',\n ' which one to keep — do not pick for me.',\n ' 2. Identify the essentials for the root file: a one-sentence project description,',\n ' the package manager if it is not npm, the build and typecheck commands if they',\n ' are non-standard, and anything genuinely relevant to every task.',\n ' 3. Group the rest into categories (TypeScript, testing, API design, git workflow),',\n ' one file per category.',\n ' 4. Create the file structure: a minimal root that links to each file, and the',\n ' files themselves under docs/.',\n ' 5. Flag for deletion anything redundant, too vague to be actionable, or so obvious',\n ' it is not worth a token on every request.',\n '',\n `Why this matters: ${GUIDE.loadsEveryRequest}.`,\n '',\n ].join('\\n');\n}\n\n/** The one sentence that says where smelt stops. Printed by the CLI, and in its help. */\nexport function splitSeamNotice(): string {\n return (\n `smelt did the mechanical half: it partitioned by heading, rewrote the links that ` +\n `moved a directory, and wrote nothing without asking. It did NOT decide which ` +\n `sections are essential — that is a reading of your project, so it needs a model, ` +\n `and smelt has none by law. The prompt above is the judgment half, filled in with ` +\n `your real sections; hand it to your own agent` +\n citing(GUIDE.pointsElsewhere)\n );\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AnswerStream } from './shell.ts';
|
|
2
|
+
/**
|
|
3
|
+
* `smelt agents split` — the wizard, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* The partition, the link rewrite and the refactor prompt are `src/agents/split.ts`,
|
|
6
|
+
* which is strings in, strings out and knows nothing about a terminal. What lives here
|
|
7
|
+
* is the half that touches disk, and it is `smelt init`'s discipline verbatim, for the
|
|
8
|
+
* reason stated in `cli/hooks.ts`: this command rewrites a file somebody wrote by hand,
|
|
9
|
+
* and a hand-written AGENTS.md clobbered by a tool is somebody's day gone.
|
|
10
|
+
*
|
|
11
|
+
* - every file listed, with `new` / `exists` / `unchanged`, before anything is written;
|
|
12
|
+
* - one final `yes`;
|
|
13
|
+
* - an existing file overwritten only after its **own** literal `yes` — not `y`, not
|
|
14
|
+
* Enter;
|
|
15
|
+
* - the guide's refactor prompt printed either way, because the judgment half is
|
|
16
|
+
* what the user actually came for and refusing the writes does not change that.
|
|
17
|
+
*/
|
|
18
|
+
/** Where the split wizard's bytes come from and go. Injected, so it tests in-process. */
|
|
19
|
+
export interface AgentsSplitIo {
|
|
20
|
+
/** Scripted answers in, one line at a time. See {@link AnswerStream}. */
|
|
21
|
+
readonly input: AnswerStream;
|
|
22
|
+
readonly output: (text: string) => void;
|
|
23
|
+
/** The working directory; every write is relative to `dir` resolved against it. */
|
|
24
|
+
readonly cwd: string;
|
|
25
|
+
/** The directory to split, as typed. `'.'` by default. */
|
|
26
|
+
readonly dir: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One `smelt agents split` run, start to finish. Returns an exit code, never calls
|
|
30
|
+
* `exit` — the same testability shape as `runInit` and `runHooks`.
|
|
31
|
+
*
|
|
32
|
+
* @throws {CliUsageError} when the directory cannot be read, when there is no
|
|
33
|
+
* instruction file to split, or when input ends mid-wizard. A refusal, never a guess:
|
|
34
|
+
* splitting a file smelt had to invent would be the auto-generation the guide rules
|
|
35
|
+
* out, and a missing directory reported as an internal error would be smelt blaming
|
|
36
|
+
* itself for a typo.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runAgentsSplit(io: AgentsSplitIo): Promise<number>;
|
|
39
|
+
//# sourceMappingURL=agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/cli/agents.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AAEH,yFAAyF;AACzF,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mFAAmF;IACnF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAiEvE","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\n\nimport { GUIDE_URL } from '../agents/guide.ts';\nimport { readInstructionSet } from '../agents/instructions.ts';\nimport type { InstructionFile } from '../agents/instructions.ts';\nimport { planSplit, refactorPrompt, splitSeamNotice } from '../agents/split.ts';\nimport type { SplitPlan } from '../agents/split.ts';\nimport { CliUsageError } from '../errors.ts';\nimport { readTree } from '../ops/inputs.ts';\n\nimport { answerReader, CLI_NAME } from './shell.ts';\nimport type { AnswerStream } from './shell.ts';\n\n/**\n * `smelt agents split` — the wizard, and nothing else.\n *\n * The partition, the link rewrite and the refactor prompt are `src/agents/split.ts`,\n * which is strings in, strings out and knows nothing about a terminal. What lives here\n * is the half that touches disk, and it is `smelt init`'s discipline verbatim, for the\n * reason stated in `cli/hooks.ts`: this command rewrites a file somebody wrote by hand,\n * and a hand-written AGENTS.md clobbered by a tool is somebody's day gone.\n *\n * - every file listed, with `new` / `exists` / `unchanged`, before anything is written;\n * - one final `yes`;\n * - an existing file overwritten only after its **own** literal `yes` — not `y`, not\n * Enter;\n * - the guide's refactor prompt printed either way, because the judgment half is\n * what the user actually came for and refusing the writes does not change that.\n */\n\n/** Where the split wizard's bytes come from and go. Injected, so it tests in-process. */\nexport interface AgentsSplitIo {\n /** Scripted answers in, one line at a time. See {@link AnswerStream}. */\n readonly input: AnswerStream;\n readonly output: (text: string) => void;\n /** The working directory; every write is relative to `dir` resolved against it. */\n readonly cwd: string;\n /** The directory to split, as typed. `'.'` by default. */\n readonly dir: string;\n}\n\n/**\n * One `smelt agents split` run, start to finish. Returns an exit code, never calls\n * `exit` — the same testability shape as `runInit` and `runHooks`.\n *\n * @throws {CliUsageError} when the directory cannot be read, when there is no\n * instruction file to split, or when input ends mid-wizard. A refusal, never a guess:\n * splitting a file smelt had to invent would be the auto-generation the guide rules\n * out, and a missing directory reported as an internal error would be smelt blaming\n * itself for a typo.\n */\nexport async function runAgentsSplit(io: AgentsSplitIo): Promise<number> {\n // `resolve`, not `join`: an absolute `dir` is the user's answer, and concatenating\n // the working directory onto it invents a path nobody named. Then the same tree\n // ruling `agents lint` uses — a directory that is not there is a refusal naming it,\n // not an ENOENT escaping as \"unexpected internal error\" with a stack trace.\n const root = resolve(io.cwd, io.dir);\n const tree = readTree(root, io.dir, { tree: 'agents split', file: `\\`${CLI_NAME} <file>\\`` });\n if (!tree.ok) throw new CliUsageError(`${CLI_NAME}: ${tree.refusal}`);\n\n const set = readInstructionSet({ root });\n const rootLevel = set.levels.find((level) => level.dir === '');\n\n if (rootLevel === undefined) {\n throw new CliUsageError(\n `${CLI_NAME} agents split: no AGENTS.md, CLAUDE.md or GEMINI.md at ${io.dir}. ` +\n `There is nothing to split, and ${CLI_NAME} will not write one for you — ` +\n `the guide it follows says never to auto-generate an AGENTS.md (${GUIDE_URL}).`,\n );\n }\n\n const file: InstructionFile = rootLevel.primary;\n const plan = planSplit(file);\n\n io.output(\n `${CLI_NAME} agents split — the mechanical half of the guide's refactor.\\n` +\n `Nothing is written until you confirm, and an existing file is never overwritten\\n` +\n `without its own yes.\\n\\n`,\n );\n\n if (plan.refusal !== undefined) {\n io.output(` ${plan.refusal}\\n\\n`);\n return 0;\n }\n\n io.output(\n `${plan.source}: ${String(plan.beforeBytes)} bytes, ` +\n `${String(plan.sections.length)} \\`##\\` section${plan.sections.length === 1 ? '' : 's'}.\\n` +\n `The root file would become ${String(plan.afterBytes)} bytes — everything above the\\n` +\n `first heading, plus a link to each moved section.\\n\\n`,\n );\n\n const lines = answerReader(io.input);\n const ask = async (prompt: string): Promise<string> => {\n io.output(prompt);\n const next = await lines.next();\n if (next === undefined) {\n throw new CliUsageError(\n `${CLI_NAME} agents split: input ended before the wizard finished. ` +\n `Files already confirmed and written stay; nothing further was written.`,\n );\n }\n return next.trim();\n };\n\n try {\n await confirmAndWrite(io, ask, root, plan);\n } finally {\n await lines.release();\n }\n\n // Printed whether or not anything was written: the prompt is the half of the\n // refactor smelt refused to do, and refusing the writes does not make it less true.\n io.output(`\\n${refactorPrompt(plan)}\\n`);\n io.output(`${splitSeamNotice()}\\n`);\n return 0;\n}\n\n/** A file the split would write, checked against what is on disk right now. */\ninterface PlannedSplitFile {\n readonly name: string;\n readonly path: string;\n readonly content: string;\n readonly exists: boolean;\n readonly unchanged: boolean;\n}\n\nfunction planned(root: string, name: string, content: string): PlannedSplitFile {\n const path = join(root, name);\n const exists = existsSync(path);\n return {\n name,\n path,\n content,\n exists,\n unchanged: exists && readFileSync(path, 'utf8') === content,\n };\n}\n\nconst fileLabel = (file: PlannedSplitFile): string => {\n if (file.unchanged) return 'unchanged — nothing to write';\n return file.exists ? 'exists — will ask before overwriting' : 'new';\n};\n\nasync function confirmAndWrite(\n io: AgentsSplitIo,\n ask: (prompt: string) => Promise<string>,\n root: string,\n plan: SplitPlan,\n): Promise<void> {\n const files = plan.files.map((file) => planned(root, file.path, file.content));\n\n io.output(\n `About to write, into ${root}:\\n` +\n files.map((file) => ` ${file.name.padEnd(40)} (${fileLabel(file)})\\n`).join('') +\n `Nothing has been written yet.\\n`,\n );\n\n for (;;) {\n const answer = await ask(`confirm (yes / no)> `);\n if (answer === 'no') {\n io.output(`Nothing was written.\\n`);\n return;\n }\n if (answer === 'yes') break;\n io.output(`yes to write, no to leave everything untouched.\\n`);\n }\n\n for (const file of files) {\n if (file.unchanged) {\n io.output(` ${file.name} — unchanged, not rewritten\\n`);\n continue;\n }\n if (file.exists) {\n // The one hard rule, same as `smelt init` and `smelt hooks`: an existing file is\n // never touched without an explicit per-file yes — not `y`, not Enter, a literal\n // `yes`. The root instruction file is *always* on this branch, by construction.\n const answer = await ask(` ${file.name} exists — overwrite it? (yes/no)> `);\n if (answer !== 'yes') {\n io.output(` skipped ${file.name} — the existing file was not touched\\n`);\n continue;\n }\n }\n mkdirSync(dirname(file.path), { recursive: true });\n writeFileSync(file.path, file.content);\n io.output(` wrote ${file.name}\\n`);\n }\n}\n"]}
|