@smeltjs/core 0.3.0 → 0.4.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/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/hooks.d.ts +25 -16
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +71 -178
- package/dist/cli/hooks.js.map +1 -1
- 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/subcommands/agents.d.ts +72 -0
- package/dist/cli/subcommands/agents.d.ts.map +1 -0
- package/dist/cli/subcommands/agents.js +162 -0
- package/dist/cli/subcommands/agents.js.map +1 -0
- package/dist/cli/subcommands/flags.d.ts +3 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -1
- package/dist/cli/subcommands/flags.js +13 -1
- 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 +19 -3
- package/dist/cli/subcommands/hooks.js.map +1 -1
- package/dist/cli/subcommands/registry.d.ts +2 -1
- package/dist/cli/subcommands/registry.d.ts.map +1 -1
- package/dist/cli/subcommands/registry.js +3 -0
- package/dist/cli/subcommands/registry.js.map +1 -1
- package/dist/cli/subcommands/subcommand.d.ts +1 -1
- package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
- package/dist/cli/subcommands/subcommand.js.map +1 -1
- package/dist/harness/codex.d.ts.map +1 -1
- package/dist/harness/codex.js +1 -0
- package/dist/harness/codex.js.map +1 -1
- package/dist/harness/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 +1 -0
- 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/profile.d.ts +26 -1
- 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/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/text/json-edit.d.ts +71 -0
- package/dist/text/json-edit.d.ts.map +1 -0
- package/dist/text/json-edit.js +219 -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,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guide `smelt agents` lints against, quoted once.
|
|
3
|
+
*
|
|
4
|
+
* Every rule in `./lint.ts` and every sentence `./split.ts` prints cites a source, and
|
|
5
|
+
* the source is one article: *A Complete Guide to AGENTS.md* ({@link GUIDE_URL}). Its
|
|
6
|
+
* thesis is a context-budget argument — the same argument smelt is built on — which is
|
|
7
|
+
* why smelt can lint against it honestly: **an AGENTS.md is a blob that loads on every
|
|
8
|
+
* request, and a blob that loads on every request is exactly what this repository
|
|
9
|
+
* measures.**
|
|
10
|
+
*
|
|
11
|
+
* The quotes live here rather than inside each rule for the reason every other
|
|
12
|
+
* "written twice" in this codebase got collected: an explanation that paraphrases its
|
|
13
|
+
* source drifts from it silently, and a reader who cannot tell smelt's opinion from
|
|
14
|
+
* the guide's cannot judge either. So a rule's explanation is *smelt's* sentence plus
|
|
15
|
+
* a phrase from {@link GUIDE}, attributed — never smelt's opinion wearing the guide's
|
|
16
|
+
* authority, and never the guide's advice restated as smelt's law.
|
|
17
|
+
*
|
|
18
|
+
* **What is not here: a threshold.** {@link GUIDE.instructionCeiling} is the guide's
|
|
19
|
+
* cited figure and is printed as a citation, never applied. The only number that can
|
|
20
|
+
* fail a lint is `agents.budgetBytes` in `smelt.config.json` — the user's own. See
|
|
21
|
+
* ruling R2 in ISSUES.md, which is the same ruling that keeps `--budget` without a
|
|
22
|
+
* default.
|
|
23
|
+
*/
|
|
24
|
+
/** Where every quote below comes from. Printed beside the citations, never guessed. */
|
|
25
|
+
export declare const GUIDE_URL = "https://www.aihero.dev/a-complete-guide-to-agents-md";
|
|
26
|
+
/** How the guide is named in a citation. */
|
|
27
|
+
export declare const GUIDE_TITLE = "the AGENTS.md guide";
|
|
28
|
+
/**
|
|
29
|
+
* The guide's own phrasing, short and attributed.
|
|
30
|
+
*
|
|
31
|
+
* Each entry is a fragment of the article quoted for the rule it grounds. They are
|
|
32
|
+
* deliberately brief: a lint that reprinted its source would be doing to the terminal
|
|
33
|
+
* what a bloated AGENTS.md does to a context window.
|
|
34
|
+
*/
|
|
35
|
+
export declare const GUIDE: {
|
|
36
|
+
/** The thesis. Why an instruction file is a budget problem at all. */
|
|
37
|
+
readonly loadsEveryRequest: string;
|
|
38
|
+
/** The cited figure. **A citation, never a threshold** — see R2. */
|
|
39
|
+
readonly instructionCeiling: 'frontier thinking LLMs can follow ~150-200 instructions with reasonable consistency';
|
|
40
|
+
/** Why a dead path is worse than no path. The flagship rule's grounding (R3). */
|
|
41
|
+
readonly stalenessPoisons: 'stale information actively poisons the context';
|
|
42
|
+
/** Why a directory listing is the wrong thing to spend the budget on. */
|
|
43
|
+
readonly describeCapabilities: 'instead of documenting structure, describe capabilities';
|
|
44
|
+
/** The tone the guide's own example is written in. */
|
|
45
|
+
readonly lightTouch: "notice the light touch, no 'always', no all-caps forcing";
|
|
46
|
+
/** The sentence that rules out `smelt agents init` (R1). */
|
|
47
|
+
readonly neverGenerate: 'never use initialization scripts to auto-generate your AGENTS.md';
|
|
48
|
+
/** The payoff of moving style rules into a linked file. */
|
|
49
|
+
readonly loadWhenRelevant: 'TypeScript rules only load when the agent writes TypeScript';
|
|
50
|
+
/** Why the sum across levels is the honest number (R8). */
|
|
51
|
+
readonly nestedMerge: 'a nested AGENTS.md merges with the root level — do not overload any level';
|
|
52
|
+
/** What the root file should be once the refactor is done. */
|
|
53
|
+
readonly pointsElsewhere: 'the ideal root file is small, focused, and points elsewhere';
|
|
54
|
+
/** The mirror advice, and the reason for it (R4). */
|
|
55
|
+
readonly symlinkMirror: '`ln -s AGENTS.md CLAUDE.md`, to keep all your tools working the same way';
|
|
56
|
+
/** What the guide says a root file actually needs. The dogfood checklist (R9). */
|
|
57
|
+
readonly essentials: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* `… — <the guide>: "<quote>"`, the tail every explanation ends with.
|
|
61
|
+
*
|
|
62
|
+
* One function so the attribution is spelled identically everywhere: a reader
|
|
63
|
+
* scanning a wall of findings must be able to tell, at a glance and without counting
|
|
64
|
+
* quotation marks, which half of a sentence is smelt's claim and which half is the
|
|
65
|
+
* guide's.
|
|
66
|
+
*/
|
|
67
|
+
export declare function citing(quote: string): string;
|
|
68
|
+
//# sourceMappingURL=guide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guide.d.ts","sourceRoot":"","sources":["../../src/agents/guide.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,uFAAuF;AACvF,eAAO,MAAM,SAAS,yDAAyD,CAAC;AAEhF,4CAA4C;AAC5C,eAAO,MAAM,WAAW,wBAAwB,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK;IAChB,sEAAsE;aACtE,iBAAiB;IAGjB,oEAAoE;aACpE,kBAAkB,EAChB,qFAAqF;IACvF,iFAAiF;aACjF,gBAAgB,EAAE,gDAAgD;IAClE,yEAAyE;aACzE,oBAAoB,EAAE,yDAAyD;IAC/E,sDAAsD;aACtD,UAAU,EAAE,0DAA0D;IACtE,4DAA4D;aAC5D,aAAa,EAAE,kEAAkE;IACjF,2DAA2D;aAC3D,gBAAgB,EAAE,6DAA6D;IAC/E,2DAA2D;aAC3D,WAAW,EAAE,2EAA2E;IACxF,8DAA8D;aAC9D,eAAe,EAAE,6DAA6D;IAC9E,qDAAqD;aACrD,aAAa,EAAE,0EAA0E;IACzF,kFAAkF;aAClF,UAAU;CAGF,CAAC;AAEX;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5C","sourcesContent":["/**\n * The guide `smelt agents` lints against, quoted once.\n *\n * Every rule in `./lint.ts` and every sentence `./split.ts` prints cites a source, and\n * the source is one article: *A Complete Guide to AGENTS.md* ({@link GUIDE_URL}). Its\n * thesis is a context-budget argument — the same argument smelt is built on — which is\n * why smelt can lint against it honestly: **an AGENTS.md is a blob that loads on every\n * request, and a blob that loads on every request is exactly what this repository\n * measures.**\n *\n * The quotes live here rather than inside each rule for the reason every other\n * \"written twice\" in this codebase got collected: an explanation that paraphrases its\n * source drifts from it silently, and a reader who cannot tell smelt's opinion from\n * the guide's cannot judge either. So a rule's explanation is *smelt's* sentence plus\n * a phrase from {@link GUIDE}, attributed — never smelt's opinion wearing the guide's\n * authority, and never the guide's advice restated as smelt's law.\n *\n * **What is not here: a threshold.** {@link GUIDE.instructionCeiling} is the guide's\n * cited figure and is printed as a citation, never applied. The only number that can\n * fail a lint is `agents.budgetBytes` in `smelt.config.json` — the user's own. See\n * ruling R2 in ISSUES.md, which is the same ruling that keeps `--budget` without a\n * default.\n */\n\n/** Where every quote below comes from. Printed beside the citations, never guessed. */\nexport const GUIDE_URL = 'https://www.aihero.dev/a-complete-guide-to-agents-md';\n\n/** How the guide is named in a citation. */\nexport const GUIDE_TITLE = 'the AGENTS.md guide';\n\n/**\n * The guide's own phrasing, short and attributed.\n *\n * Each entry is a fragment of the article quoted for the rule it grounds. They are\n * deliberately brief: a lint that reprinted its source would be doing to the terminal\n * what a bloated AGENTS.md does to a context window.\n */\nexport const GUIDE = {\n /** The thesis. Why an instruction file is a budget problem at all. */\n loadsEveryRequest:\n 'every token in your AGENTS.md gets loaded on every single request, ' +\n 'regardless of whether it is relevant',\n /** The cited figure. **A citation, never a threshold** — see R2. */\n instructionCeiling:\n 'frontier thinking LLMs can follow ~150-200 instructions with reasonable consistency',\n /** Why a dead path is worse than no path. The flagship rule's grounding (R3). */\n stalenessPoisons: 'stale information actively poisons the context',\n /** Why a directory listing is the wrong thing to spend the budget on. */\n describeCapabilities: 'instead of documenting structure, describe capabilities',\n /** The tone the guide's own example is written in. */\n lightTouch: \"notice the light touch, no 'always', no all-caps forcing\",\n /** The sentence that rules out `smelt agents init` (R1). */\n neverGenerate: 'never use initialization scripts to auto-generate your AGENTS.md',\n /** The payoff of moving style rules into a linked file. */\n loadWhenRelevant: 'TypeScript rules only load when the agent writes TypeScript',\n /** Why the sum across levels is the honest number (R8). */\n nestedMerge: 'a nested AGENTS.md merges with the root level — do not overload any level',\n /** What the root file should be once the refactor is done. */\n pointsElsewhere: 'the ideal root file is small, focused, and points elsewhere',\n /** The mirror advice, and the reason for it (R4). */\n symlinkMirror: '`ln -s AGENTS.md CLAUDE.md`, to keep all your tools working the same way',\n /** What the guide says a root file actually needs. The dogfood checklist (R9). */\n essentials:\n 'a one-sentence project description, the package manager if it is not npm, and ' +\n 'the build/typecheck commands if they are non-standard — \"that\\'s honestly it\"',\n} as const;\n\n/**\n * `… — <the guide>: \"<quote>\"`, the tail every explanation ends with.\n *\n * One function so the attribution is spelled identically everywhere: a reader\n * scanning a wall of findings must be able to tell, at a glance and without counting\n * quotation marks, which half of a sentence is smelt's claim and which half is the\n * guide's.\n */\nexport function citing(quote: string): string {\n return ` — ${GUIDE_TITLE}: \"${quote}\"`;\n}\n"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guide `smelt agents` lints against, quoted once.
|
|
3
|
+
*
|
|
4
|
+
* Every rule in `./lint.ts` and every sentence `./split.ts` prints cites a source, and
|
|
5
|
+
* the source is one article: *A Complete Guide to AGENTS.md* ({@link GUIDE_URL}). Its
|
|
6
|
+
* thesis is a context-budget argument — the same argument smelt is built on — which is
|
|
7
|
+
* why smelt can lint against it honestly: **an AGENTS.md is a blob that loads on every
|
|
8
|
+
* request, and a blob that loads on every request is exactly what this repository
|
|
9
|
+
* measures.**
|
|
10
|
+
*
|
|
11
|
+
* The quotes live here rather than inside each rule for the reason every other
|
|
12
|
+
* "written twice" in this codebase got collected: an explanation that paraphrases its
|
|
13
|
+
* source drifts from it silently, and a reader who cannot tell smelt's opinion from
|
|
14
|
+
* the guide's cannot judge either. So a rule's explanation is *smelt's* sentence plus
|
|
15
|
+
* a phrase from {@link GUIDE}, attributed — never smelt's opinion wearing the guide's
|
|
16
|
+
* authority, and never the guide's advice restated as smelt's law.
|
|
17
|
+
*
|
|
18
|
+
* **What is not here: a threshold.** {@link GUIDE.instructionCeiling} is the guide's
|
|
19
|
+
* cited figure and is printed as a citation, never applied. The only number that can
|
|
20
|
+
* fail a lint is `agents.budgetBytes` in `smelt.config.json` — the user's own. See
|
|
21
|
+
* ruling R2 in ISSUES.md, which is the same ruling that keeps `--budget` without a
|
|
22
|
+
* default.
|
|
23
|
+
*/
|
|
24
|
+
/** Where every quote below comes from. Printed beside the citations, never guessed. */
|
|
25
|
+
export const GUIDE_URL = 'https://www.aihero.dev/a-complete-guide-to-agents-md';
|
|
26
|
+
/** How the guide is named in a citation. */
|
|
27
|
+
export const GUIDE_TITLE = 'the AGENTS.md guide';
|
|
28
|
+
/**
|
|
29
|
+
* The guide's own phrasing, short and attributed.
|
|
30
|
+
*
|
|
31
|
+
* Each entry is a fragment of the article quoted for the rule it grounds. They are
|
|
32
|
+
* deliberately brief: a lint that reprinted its source would be doing to the terminal
|
|
33
|
+
* what a bloated AGENTS.md does to a context window.
|
|
34
|
+
*/
|
|
35
|
+
export const GUIDE = {
|
|
36
|
+
/** The thesis. Why an instruction file is a budget problem at all. */
|
|
37
|
+
loadsEveryRequest: 'every token in your AGENTS.md gets loaded on every single request, ' +
|
|
38
|
+
'regardless of whether it is relevant',
|
|
39
|
+
/** The cited figure. **A citation, never a threshold** — see R2. */
|
|
40
|
+
instructionCeiling: 'frontier thinking LLMs can follow ~150-200 instructions with reasonable consistency',
|
|
41
|
+
/** Why a dead path is worse than no path. The flagship rule's grounding (R3). */
|
|
42
|
+
stalenessPoisons: 'stale information actively poisons the context',
|
|
43
|
+
/** Why a directory listing is the wrong thing to spend the budget on. */
|
|
44
|
+
describeCapabilities: 'instead of documenting structure, describe capabilities',
|
|
45
|
+
/** The tone the guide's own example is written in. */
|
|
46
|
+
lightTouch: "notice the light touch, no 'always', no all-caps forcing",
|
|
47
|
+
/** The sentence that rules out `smelt agents init` (R1). */
|
|
48
|
+
neverGenerate: 'never use initialization scripts to auto-generate your AGENTS.md',
|
|
49
|
+
/** The payoff of moving style rules into a linked file. */
|
|
50
|
+
loadWhenRelevant: 'TypeScript rules only load when the agent writes TypeScript',
|
|
51
|
+
/** Why the sum across levels is the honest number (R8). */
|
|
52
|
+
nestedMerge: 'a nested AGENTS.md merges with the root level — do not overload any level',
|
|
53
|
+
/** What the root file should be once the refactor is done. */
|
|
54
|
+
pointsElsewhere: 'the ideal root file is small, focused, and points elsewhere',
|
|
55
|
+
/** The mirror advice, and the reason for it (R4). */
|
|
56
|
+
symlinkMirror: '`ln -s AGENTS.md CLAUDE.md`, to keep all your tools working the same way',
|
|
57
|
+
/** What the guide says a root file actually needs. The dogfood checklist (R9). */
|
|
58
|
+
essentials: 'a one-sentence project description, the package manager if it is not npm, and ' +
|
|
59
|
+
'the build/typecheck commands if they are non-standard — "that\'s honestly it"',
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* `… — <the guide>: "<quote>"`, the tail every explanation ends with.
|
|
63
|
+
*
|
|
64
|
+
* One function so the attribution is spelled identically everywhere: a reader
|
|
65
|
+
* scanning a wall of findings must be able to tell, at a glance and without counting
|
|
66
|
+
* quotation marks, which half of a sentence is smelt's claim and which half is the
|
|
67
|
+
* guide's.
|
|
68
|
+
*/
|
|
69
|
+
export function citing(quote) {
|
|
70
|
+
return ` — ${GUIDE_TITLE}: "${quote}"`;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=guide.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guide.js","sourceRoot":"","sources":["../../src/agents/guide.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,uFAAuF;AACvF,MAAM,CAAC,MAAM,SAAS,GAAG,sDAAsD,CAAC;AAEhF,4CAA4C;AAC5C,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,sEAAsE;IACtE,iBAAiB,EACf,qEAAqE;QACrE,sCAAsC;IACxC,oEAAoE;IACpE,kBAAkB,EAChB,qFAAqF;IACvF,iFAAiF;IACjF,gBAAgB,EAAE,gDAAgD;IAClE,yEAAyE;IACzE,oBAAoB,EAAE,yDAAyD;IAC/E,sDAAsD;IACtD,UAAU,EAAE,0DAA0D;IACtE,4DAA4D;IAC5D,aAAa,EAAE,kEAAkE;IACjF,2DAA2D;IAC3D,gBAAgB,EAAE,6DAA6D;IAC/E,2DAA2D;IAC3D,WAAW,EAAE,2EAA2E;IACxF,8DAA8D;IAC9D,eAAe,EAAE,6DAA6D;IAC9E,qDAAqD;IACrD,aAAa,EAAE,0EAA0E;IACzF,kFAAkF;IAClF,UAAU,EACR,gFAAgF;QAChF,+EAA+E;CACzE,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO,MAAM,WAAW,MAAM,KAAK,GAAG,CAAC;AACzC,CAAC","sourcesContent":["/**\n * The guide `smelt agents` lints against, quoted once.\n *\n * Every rule in `./lint.ts` and every sentence `./split.ts` prints cites a source, and\n * the source is one article: *A Complete Guide to AGENTS.md* ({@link GUIDE_URL}). Its\n * thesis is a context-budget argument — the same argument smelt is built on — which is\n * why smelt can lint against it honestly: **an AGENTS.md is a blob that loads on every\n * request, and a blob that loads on every request is exactly what this repository\n * measures.**\n *\n * The quotes live here rather than inside each rule for the reason every other\n * \"written twice\" in this codebase got collected: an explanation that paraphrases its\n * source drifts from it silently, and a reader who cannot tell smelt's opinion from\n * the guide's cannot judge either. So a rule's explanation is *smelt's* sentence plus\n * a phrase from {@link GUIDE}, attributed — never smelt's opinion wearing the guide's\n * authority, and never the guide's advice restated as smelt's law.\n *\n * **What is not here: a threshold.** {@link GUIDE.instructionCeiling} is the guide's\n * cited figure and is printed as a citation, never applied. The only number that can\n * fail a lint is `agents.budgetBytes` in `smelt.config.json` — the user's own. See\n * ruling R2 in ISSUES.md, which is the same ruling that keeps `--budget` without a\n * default.\n */\n\n/** Where every quote below comes from. Printed beside the citations, never guessed. */\nexport const GUIDE_URL = 'https://www.aihero.dev/a-complete-guide-to-agents-md';\n\n/** How the guide is named in a citation. */\nexport const GUIDE_TITLE = 'the AGENTS.md guide';\n\n/**\n * The guide's own phrasing, short and attributed.\n *\n * Each entry is a fragment of the article quoted for the rule it grounds. They are\n * deliberately brief: a lint that reprinted its source would be doing to the terminal\n * what a bloated AGENTS.md does to a context window.\n */\nexport const GUIDE = {\n /** The thesis. Why an instruction file is a budget problem at all. */\n loadsEveryRequest:\n 'every token in your AGENTS.md gets loaded on every single request, ' +\n 'regardless of whether it is relevant',\n /** The cited figure. **A citation, never a threshold** — see R2. */\n instructionCeiling:\n 'frontier thinking LLMs can follow ~150-200 instructions with reasonable consistency',\n /** Why a dead path is worse than no path. The flagship rule's grounding (R3). */\n stalenessPoisons: 'stale information actively poisons the context',\n /** Why a directory listing is the wrong thing to spend the budget on. */\n describeCapabilities: 'instead of documenting structure, describe capabilities',\n /** The tone the guide's own example is written in. */\n lightTouch: \"notice the light touch, no 'always', no all-caps forcing\",\n /** The sentence that rules out `smelt agents init` (R1). */\n neverGenerate: 'never use initialization scripts to auto-generate your AGENTS.md',\n /** The payoff of moving style rules into a linked file. */\n loadWhenRelevant: 'TypeScript rules only load when the agent writes TypeScript',\n /** Why the sum across levels is the honest number (R8). */\n nestedMerge: 'a nested AGENTS.md merges with the root level — do not overload any level',\n /** What the root file should be once the refactor is done. */\n pointsElsewhere: 'the ideal root file is small, focused, and points elsewhere',\n /** The mirror advice, and the reason for it (R4). */\n symlinkMirror: '`ln -s AGENTS.md CLAUDE.md`, to keep all your tools working the same way',\n /** What the guide says a root file actually needs. The dogfood checklist (R9). */\n essentials:\n 'a one-sentence project description, the package manager if it is not npm, and ' +\n 'the build/typecheck commands if they are non-standard — \"that\\'s honestly it\"',\n} as const;\n\n/**\n * `… — <the guide>: \"<quote>\"`, the tail every explanation ends with.\n *\n * One function so the attribution is spelled identically everywhere: a reader\n * scanning a wall of findings must be able to tell, at a glance and without counting\n * quotation marks, which half of a sentence is smelt's claim and which half is the\n * guide's.\n */\nexport function citing(quote: string): string {\n return ` — ${GUIDE_TITLE}: \"${quote}\"`;\n}\n"]}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { RepoReader } from '../repomap/reader.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The instruction files an agent actually loads, found through the read-only
|
|
4
|
+
* {@link RepoReader} seam.
|
|
5
|
+
*
|
|
6
|
+
* **Why the merged set, and not just the root file** (ruling R8): the guide's own rule
|
|
7
|
+
* is that a nested `AGENTS.md` *merges with* the root one rather than replacing it, so
|
|
8
|
+
* a lint that reported only the root file would be reporting a fraction and calling it
|
|
9
|
+
* the total, which is this repository's Law 4 pointed at its own output.
|
|
10
|
+
*
|
|
11
|
+
* **But a merge runs up the tree, not across it.** Two numbers come out of that, and
|
|
12
|
+
* they are different numbers in any monorepo, so both are reported and each is labelled
|
|
13
|
+
* with the question it answers:
|
|
14
|
+
*
|
|
15
|
+
* - {@link InstructionSet.perRequestBytes} — the most any *one* request loads: a level
|
|
16
|
+
* plus its **ancestors**. An agent working in `pkg/a` loads the root file and
|
|
17
|
+
* `pkg/a`'s; it never loads `pkg/b`'s, because siblings do not merge. This is the
|
|
18
|
+
* per-request cost, and it is the number the guide's argument is about.
|
|
19
|
+
* - {@link InstructionSet.totalBytes} — every level's primary summed: the whole
|
|
20
|
+
* repository's instruction surface. Useful as the size of what a team maintains,
|
|
21
|
+
* and **not** a per-request cost. Calling it one would be the same over-count this
|
|
22
|
+
* module refuses for mirrors one paragraph down.
|
|
23
|
+
*
|
|
24
|
+
* **Why a mirror is not a level.** Claude Code reads `CLAUDE.md`, Gemini reads
|
|
25
|
+
* `GEMINI.md`, Codex and the rest read `AGENTS.md`. One agent loads *one* of them, so
|
|
26
|
+
* summing all three would triple a cost nobody pays. At each directory the primary is
|
|
27
|
+
* the file the level contributes — `AGENTS.md` when it exists, otherwise whichever
|
|
28
|
+
* mirror is there alone — and the others are {@link InstructionLevel.mirrors}, counted
|
|
29
|
+
* for drift and not for bytes.
|
|
30
|
+
*
|
|
31
|
+
* **Why the reader is a seam.** Every claim the lint makes is a claim about a *walk*:
|
|
32
|
+
* that a nested file was found, that an ignored directory was never entered, that a
|
|
33
|
+
* path in the prose resolves against the real tree. Those are claims about calls, so
|
|
34
|
+
* they are asserted by counting calls against a stub tree (`test/repo-reader-stub.ts`),
|
|
35
|
+
* exactly as the repo map's walk is. The default is {@link nodeFsReader}, so nothing
|
|
36
|
+
* changes for a caller who does not care.
|
|
37
|
+
*/
|
|
38
|
+
/** The three file names a coding agent reads instructions from, in precedence order. */
|
|
39
|
+
export declare const INSTRUCTION_FILE_NAMES: readonly ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
|
|
40
|
+
/** One of the three names above. */
|
|
41
|
+
export type InstructionFileName = (typeof INSTRUCTION_FILE_NAMES)[number];
|
|
42
|
+
/** True for a name this module treats as an instruction file. */
|
|
43
|
+
export declare function isInstructionFileName(name: string): name is InstructionFileName;
|
|
44
|
+
/** One instruction file on disk, read. */
|
|
45
|
+
export interface InstructionFile {
|
|
46
|
+
/** Root-relative and `/`-separated, e.g. `packages/core/AGENTS.md`. */
|
|
47
|
+
readonly path: string;
|
|
48
|
+
readonly name: InstructionFileName;
|
|
49
|
+
/** The directory it sits in, root-relative; `''` for the repository root. */
|
|
50
|
+
readonly dir: string;
|
|
51
|
+
readonly text: string;
|
|
52
|
+
/** UTF-8 bytes. The unit every smelt budget is in. */
|
|
53
|
+
readonly bytes: number;
|
|
54
|
+
/**
|
|
55
|
+
* True when the entry is a symlink — the arrangement the guide recommends for
|
|
56
|
+
* mirrors, and the one arrangement in which a mirror **cannot** drift.
|
|
57
|
+
*/
|
|
58
|
+
readonly symlink: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* One directory's contribution: the file that level costs, plus the mirrors of it.
|
|
62
|
+
*
|
|
63
|
+
* `primary` is the only one whose bytes are summed. A mirror that differs from it is
|
|
64
|
+
* `mirror-drift`; a mirror that is a symlink cannot differ, which is the whole reason
|
|
65
|
+
* the guide suggests one.
|
|
66
|
+
*/
|
|
67
|
+
export interface InstructionLevel {
|
|
68
|
+
/** Root-relative directory; `''` is the root level. */
|
|
69
|
+
readonly dir: string;
|
|
70
|
+
readonly primary: InstructionFile;
|
|
71
|
+
readonly mirrors: readonly InstructionFile[];
|
|
72
|
+
}
|
|
73
|
+
/** Every instruction file in a tree, arranged by level. */
|
|
74
|
+
export interface InstructionSet {
|
|
75
|
+
/** The directory the walk started from, as the caller spelled it. */
|
|
76
|
+
readonly root: string;
|
|
77
|
+
/** Root level first, then nested levels in walk order. */
|
|
78
|
+
readonly levels: readonly InstructionLevel[];
|
|
79
|
+
/**
|
|
80
|
+
* The repository-wide instruction surface: every level's primary, summed.
|
|
81
|
+
*
|
|
82
|
+
* **Not a per-request cost.** Sibling levels never merge, so no single request ever
|
|
83
|
+
* loads all of these — see {@link perRequestBytes} for the number that answers that
|
|
84
|
+
* question.
|
|
85
|
+
*/
|
|
86
|
+
readonly totalBytes: number;
|
|
87
|
+
/**
|
|
88
|
+
* What the most expensive single request loads: the heaviest level plus every level
|
|
89
|
+
* above it, since a nested file merges with its **ancestors** and with nothing else.
|
|
90
|
+
*
|
|
91
|
+
* Equal to {@link totalBytes} in a repository with one chain of levels, and strictly
|
|
92
|
+
* smaller as soon as two siblings both carry an instruction file.
|
|
93
|
+
*/
|
|
94
|
+
readonly perRequestBytes: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Find and read every instruction file under `root`.
|
|
98
|
+
*
|
|
99
|
+
* The ignore list is the repo map's, and for the same reason: a vendored
|
|
100
|
+
* `node_modules/**\/AGENTS.md` is not this repository's instruction to an agent, and
|
|
101
|
+
* counting it would make the total a number about somebody else's project. A caller
|
|
102
|
+
* may replace the list wholesale — a default nobody can turn off is not a default.
|
|
103
|
+
*
|
|
104
|
+
* Symlinked *directories* are refused outright, exactly as the map refuses them: a
|
|
105
|
+
* walk that follows one can leave the root. A symlinked instruction **file** is read
|
|
106
|
+
* (following it is the point — that is what the harness does too) and flagged.
|
|
107
|
+
*/
|
|
108
|
+
export declare function readInstructionSet(options: {
|
|
109
|
+
readonly root: string;
|
|
110
|
+
readonly reader?: RepoReader;
|
|
111
|
+
readonly ignore?: readonly string[];
|
|
112
|
+
}): InstructionSet;
|
|
113
|
+
/**
|
|
114
|
+
* The directories a level merges *with*, root first and nearest last — and never the
|
|
115
|
+
* level itself.
|
|
116
|
+
*
|
|
117
|
+
* The whole distinction `restated-at-level` and {@link InstructionSet.perRequestBytes}
|
|
118
|
+
* both turn on: `pkg/a` merges with `''` and `pkg`, and with nothing else in the tree.
|
|
119
|
+
*/
|
|
120
|
+
export declare function ancestorDirs(dir: string): readonly string[];
|
|
121
|
+
/**
|
|
122
|
+
* Does `token`, read as a path relative to the repository root, name something that
|
|
123
|
+
* exists? The one question `dead-path` and `dead-link` both ask.
|
|
124
|
+
*
|
|
125
|
+
* Through the reader, so the resolution is part of the same injectable walk the
|
|
126
|
+
* discovery is — a fixture repo can therefore contain a path that resolves and one
|
|
127
|
+
* that does not without either of them existing on the machine running the tests.
|
|
128
|
+
* A reader that throws (a malformed path, a permission error) answers "no": the
|
|
129
|
+
* finding is advisory, and a lint that failed the whole run because one token in
|
|
130
|
+
* somebody's prose was unstattable would be worse than the staleness it looks for.
|
|
131
|
+
*/
|
|
132
|
+
export declare function resolvesInTree(root: string, reader: RepoReader, token: string): boolean;
|
|
133
|
+
//# sourceMappingURL=instructions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/agents/instructions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,wFAAwF;AACxF,eAAO,MAAM,sBAAsB,YAAI,WAAW,EAAE,WAAW,EAAE,WAAW,CAAU,CAAC;AAEvF,oCAAoC;AACpC,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E,iEAAiE;AACjE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,mBAAmB,CAE/E;AAED,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;CAC9C;AAED,2DAA2D;AAC3D,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAKD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,GAAG,cAAc,CAgFjB;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAM3D;AAuBD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAMvF","sourcesContent":["import { join } from 'node:path';\n\nimport { DEFAULT_REPO_IGNORE } from '../repomap/map.ts';\nimport { nodeFsReader } from '../repomap/reader.ts';\nimport type { RepoReader } from '../repomap/reader.ts';\n\n/**\n * The instruction files an agent actually loads, found through the read-only\n * {@link RepoReader} seam.\n *\n * **Why the merged set, and not just the root file** (ruling R8): the guide's own rule\n * is that a nested `AGENTS.md` *merges with* the root one rather than replacing it, so\n * a lint that reported only the root file would be reporting a fraction and calling it\n * the total, which is this repository's Law 4 pointed at its own output.\n *\n * **But a merge runs up the tree, not across it.** Two numbers come out of that, and\n * they are different numbers in any monorepo, so both are reported and each is labelled\n * with the question it answers:\n *\n * - {@link InstructionSet.perRequestBytes} — the most any *one* request loads: a level\n * plus its **ancestors**. An agent working in `pkg/a` loads the root file and\n * `pkg/a`'s; it never loads `pkg/b`'s, because siblings do not merge. This is the\n * per-request cost, and it is the number the guide's argument is about.\n * - {@link InstructionSet.totalBytes} — every level's primary summed: the whole\n * repository's instruction surface. Useful as the size of what a team maintains,\n * and **not** a per-request cost. Calling it one would be the same over-count this\n * module refuses for mirrors one paragraph down.\n *\n * **Why a mirror is not a level.** Claude Code reads `CLAUDE.md`, Gemini reads\n * `GEMINI.md`, Codex and the rest read `AGENTS.md`. One agent loads *one* of them, so\n * summing all three would triple a cost nobody pays. At each directory the primary is\n * the file the level contributes — `AGENTS.md` when it exists, otherwise whichever\n * mirror is there alone — and the others are {@link InstructionLevel.mirrors}, counted\n * for drift and not for bytes.\n *\n * **Why the reader is a seam.** Every claim the lint makes is a claim about a *walk*:\n * that a nested file was found, that an ignored directory was never entered, that a\n * path in the prose resolves against the real tree. Those are claims about calls, so\n * they are asserted by counting calls against a stub tree (`test/repo-reader-stub.ts`),\n * exactly as the repo map's walk is. The default is {@link nodeFsReader}, so nothing\n * changes for a caller who does not care.\n */\n\n/** The three file names a coding agent reads instructions from, in precedence order. */\nexport const INSTRUCTION_FILE_NAMES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'] as const;\n\n/** One of the three names above. */\nexport type InstructionFileName = (typeof INSTRUCTION_FILE_NAMES)[number];\n\n/** True for a name this module treats as an instruction file. */\nexport function isInstructionFileName(name: string): name is InstructionFileName {\n return (INSTRUCTION_FILE_NAMES as readonly string[]).includes(name);\n}\n\n/** One instruction file on disk, read. */\nexport interface InstructionFile {\n /** Root-relative and `/`-separated, e.g. `packages/core/AGENTS.md`. */\n readonly path: string;\n readonly name: InstructionFileName;\n /** The directory it sits in, root-relative; `''` for the repository root. */\n readonly dir: string;\n readonly text: string;\n /** UTF-8 bytes. The unit every smelt budget is in. */\n readonly bytes: number;\n /**\n * True when the entry is a symlink — the arrangement the guide recommends for\n * mirrors, and the one arrangement in which a mirror **cannot** drift.\n */\n readonly symlink: boolean;\n}\n\n/**\n * One directory's contribution: the file that level costs, plus the mirrors of it.\n *\n * `primary` is the only one whose bytes are summed. A mirror that differs from it is\n * `mirror-drift`; a mirror that is a symlink cannot differ, which is the whole reason\n * the guide suggests one.\n */\nexport interface InstructionLevel {\n /** Root-relative directory; `''` is the root level. */\n readonly dir: string;\n readonly primary: InstructionFile;\n readonly mirrors: readonly InstructionFile[];\n}\n\n/** Every instruction file in a tree, arranged by level. */\nexport interface InstructionSet {\n /** The directory the walk started from, as the caller spelled it. */\n readonly root: string;\n /** Root level first, then nested levels in walk order. */\n readonly levels: readonly InstructionLevel[];\n /**\n * The repository-wide instruction surface: every level's primary, summed.\n *\n * **Not a per-request cost.** Sibling levels never merge, so no single request ever\n * loads all of these — see {@link perRequestBytes} for the number that answers that\n * question.\n */\n readonly totalBytes: number;\n /**\n * What the most expensive single request loads: the heaviest level plus every level\n * above it, since a nested file merges with its **ancestors** and with nothing else.\n *\n * Equal to {@link totalBytes} in a repository with one chain of levels, and strictly\n * smaller as soon as two siblings both carry an instruction file.\n */\n readonly perRequestBytes: number;\n}\n\n/** How deep the walk goes. Instruction files near the root are the ones agents read. */\nconst MAX_DEPTH = 6;\n\n/**\n * Find and read every instruction file under `root`.\n *\n * The ignore list is the repo map's, and for the same reason: a vendored\n * `node_modules/**\\/AGENTS.md` is not this repository's instruction to an agent, and\n * counting it would make the total a number about somebody else's project. A caller\n * may replace the list wholesale — a default nobody can turn off is not a default.\n *\n * Symlinked *directories* are refused outright, exactly as the map refuses them: a\n * walk that follows one can leave the root. A symlinked instruction **file** is read\n * (following it is the point — that is what the harness does too) and flagged.\n */\nexport function readInstructionSet(options: {\n readonly root: string;\n readonly reader?: RepoReader;\n readonly ignore?: readonly string[];\n}): InstructionSet {\n const reader = options.reader ?? nodeFsReader();\n const ignore = options.ignore ?? DEFAULT_REPO_IGNORE;\n const found: InstructionFile[] = [];\n\n const walk = (dir: string, depth: number): void => {\n const entries = reader\n .list(dir === '' ? options.root : join(options.root, dir))\n .map((entry) => entry.name)\n .toSorted();\n\n for (const name of entries) {\n const relative = dir === '' ? name : `${dir}/${name}`;\n if (isIgnored(relative, ignore)) continue;\n const stat = reader.stat(join(options.root, relative));\n if (stat === undefined) continue;\n if (stat.isDirectory && !stat.isSymlink) {\n if (depth < MAX_DEPTH) walk(relative, depth + 1);\n continue;\n }\n if (!isInstructionFileName(name)) continue;\n // A symlinked instruction file is **read**, unlike a symlinked directory, which\n // is refused. Following it is the point: it is the arrangement the guide\n // recommends for mirrors, and it is exactly what the harness does when it opens\n // `CLAUDE.md`. Two readers disagree about how to describe one, and both are\n // accommodated here: `nodeFsReader` lstats, so a link is `isSymlink` and *not*\n // `isFile`; a resolving reader answers about the target, so it is both.\n if (!stat.isFile && !stat.isSymlink) continue;\n let raw: Uint8Array;\n try {\n raw = reader.read(join(options.root, relative));\n } catch {\n // A broken symlink, or a file that vanished between the stat and the read.\n // Nothing is loaded on every request, so there is nothing to report.\n continue;\n }\n found.push({\n path: relative,\n name,\n dir,\n text: new TextDecoder('utf-8').decode(raw),\n // The bytes on disk, straight off the read — never `Buffer.byteLength(text)`.\n // Decoding and re-encoding is not the identity: a UTF-8 BOM is dropped on the\n // way in (19 bytes read, 16 reported) and a byte that is not valid UTF-8 comes\n // back as a three-byte U+FFFD. Both skew the one number this verb exists to\n // state, in opposite directions, against a figure the harness measures on the\n // file itself. `stat.size` is not the answer either: on a symlinked mirror\n // `lstat` sizes the *link*, and following it is the point.\n bytes: raw.byteLength,\n symlink: stat.isSymlink,\n });\n }\n };\n\n walk('', 0);\n\n const byDir = new Map<string, InstructionFile[]>();\n for (const file of found) {\n const bucket = byDir.get(file.dir);\n if (bucket === undefined) byDir.set(file.dir, [file]);\n else bucket.push(file);\n }\n\n const levels: InstructionLevel[] = [...byDir.entries()]\n // Root first, then by depth and then alphabetically: the order a reader would\n // walk the tree in, so a report reads top-down.\n .toSorted(([a], [b]) => depthOf(a) - depthOf(b) || (a < b ? -1 : a > b ? 1 : 0))\n .map(([dir, files]) => {\n const ordered = files.toSorted(\n (a, b) => INSTRUCTION_FILE_NAMES.indexOf(a.name) - INSTRUCTION_FILE_NAMES.indexOf(b.name),\n );\n return { dir, primary: ordered[0]!, mirrors: ordered.slice(1) };\n });\n\n return {\n root: options.root,\n levels,\n totalBytes: levels.reduce((sum, level) => sum + level.primary.bytes, 0),\n perRequestBytes: heaviestChainBytes(levels),\n };\n}\n\n/**\n * The heaviest ancestor chain: the most any single request can load.\n *\n * A request made in `pkg/a` merges the root level with `pkg/a`'s, and stops. `pkg/b` is\n * a sibling, and no agent loads a sibling — so the maximum over chains is the honest\n * per-request figure, and the sum over levels is not one.\n */\nfunction heaviestChainBytes(levels: readonly InstructionLevel[]): number {\n const bytesByDir = new Map(levels.map((level) => [level.dir, level.primary.bytes]));\n let heaviest = 0;\n for (const level of levels) {\n const chain = ancestorDirs(level.dir).reduce(\n (sum, dir) => sum + (bytesByDir.get(dir) ?? 0),\n level.primary.bytes,\n );\n heaviest = Math.max(heaviest, chain);\n }\n return heaviest;\n}\n\n/**\n * The directories a level merges *with*, root first and nearest last — and never the\n * level itself.\n *\n * The whole distinction `restated-at-level` and {@link InstructionSet.perRequestBytes}\n * both turn on: `pkg/a` merges with `''` and `pkg`, and with nothing else in the tree.\n */\nexport function ancestorDirs(dir: string): readonly string[] {\n if (dir === '') return [];\n const segments = dir.split('/');\n const out: string[] = [''];\n for (let cut = 1; cut < segments.length; cut += 1) out.push(segments.slice(0, cut).join('/'));\n return out;\n}\n\n/** `''` is depth 0; `a/b` is depth 2. */\nfunction depthOf(dir: string): number {\n return dir === '' ? 0 : dir.split('/').length;\n}\n\n/**\n * The repo map's ignore semantics, restated over root-relative paths: a bare name\n * matches any path segment, an entry containing `/` is a root-relative prefix.\n */\nfunction isIgnored(relative: string, ignore: readonly string[]): boolean {\n const segments = relative.split('/');\n return ignore.some((entry) => {\n const trimmed = entry.endsWith('/') ? entry.slice(0, -1) : entry;\n if (trimmed === '') return false;\n if (entry.includes('/')) {\n return relative === trimmed || relative.startsWith(`${trimmed}/`);\n }\n return segments.includes(trimmed);\n });\n}\n\n/**\n * Does `token`, read as a path relative to the repository root, name something that\n * exists? The one question `dead-path` and `dead-link` both ask.\n *\n * Through the reader, so the resolution is part of the same injectable walk the\n * discovery is — a fixture repo can therefore contain a path that resolves and one\n * that does not without either of them existing on the machine running the tests.\n * A reader that throws (a malformed path, a permission error) answers \"no\": the\n * finding is advisory, and a lint that failed the whole run because one token in\n * somebody's prose was unstattable would be worse than the staleness it looks for.\n */\nexport function resolvesInTree(root: string, reader: RepoReader, token: string): boolean {\n try {\n return reader.stat(join(root, token)) !== undefined;\n } catch {\n return false;\n }\n}\n"]}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { DEFAULT_REPO_IGNORE } from '../repomap/map.js';
|
|
3
|
+
import { nodeFsReader } from '../repomap/reader.js';
|
|
4
|
+
/**
|
|
5
|
+
* The instruction files an agent actually loads, found through the read-only
|
|
6
|
+
* {@link RepoReader} seam.
|
|
7
|
+
*
|
|
8
|
+
* **Why the merged set, and not just the root file** (ruling R8): the guide's own rule
|
|
9
|
+
* is that a nested `AGENTS.md` *merges with* the root one rather than replacing it, so
|
|
10
|
+
* a lint that reported only the root file would be reporting a fraction and calling it
|
|
11
|
+
* the total, which is this repository's Law 4 pointed at its own output.
|
|
12
|
+
*
|
|
13
|
+
* **But a merge runs up the tree, not across it.** Two numbers come out of that, and
|
|
14
|
+
* they are different numbers in any monorepo, so both are reported and each is labelled
|
|
15
|
+
* with the question it answers:
|
|
16
|
+
*
|
|
17
|
+
* - {@link InstructionSet.perRequestBytes} — the most any *one* request loads: a level
|
|
18
|
+
* plus its **ancestors**. An agent working in `pkg/a` loads the root file and
|
|
19
|
+
* `pkg/a`'s; it never loads `pkg/b`'s, because siblings do not merge. This is the
|
|
20
|
+
* per-request cost, and it is the number the guide's argument is about.
|
|
21
|
+
* - {@link InstructionSet.totalBytes} — every level's primary summed: the whole
|
|
22
|
+
* repository's instruction surface. Useful as the size of what a team maintains,
|
|
23
|
+
* and **not** a per-request cost. Calling it one would be the same over-count this
|
|
24
|
+
* module refuses for mirrors one paragraph down.
|
|
25
|
+
*
|
|
26
|
+
* **Why a mirror is not a level.** Claude Code reads `CLAUDE.md`, Gemini reads
|
|
27
|
+
* `GEMINI.md`, Codex and the rest read `AGENTS.md`. One agent loads *one* of them, so
|
|
28
|
+
* summing all three would triple a cost nobody pays. At each directory the primary is
|
|
29
|
+
* the file the level contributes — `AGENTS.md` when it exists, otherwise whichever
|
|
30
|
+
* mirror is there alone — and the others are {@link InstructionLevel.mirrors}, counted
|
|
31
|
+
* for drift and not for bytes.
|
|
32
|
+
*
|
|
33
|
+
* **Why the reader is a seam.** Every claim the lint makes is a claim about a *walk*:
|
|
34
|
+
* that a nested file was found, that an ignored directory was never entered, that a
|
|
35
|
+
* path in the prose resolves against the real tree. Those are claims about calls, so
|
|
36
|
+
* they are asserted by counting calls against a stub tree (`test/repo-reader-stub.ts`),
|
|
37
|
+
* exactly as the repo map's walk is. The default is {@link nodeFsReader}, so nothing
|
|
38
|
+
* changes for a caller who does not care.
|
|
39
|
+
*/
|
|
40
|
+
/** The three file names a coding agent reads instructions from, in precedence order. */
|
|
41
|
+
export const INSTRUCTION_FILE_NAMES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
|
|
42
|
+
/** True for a name this module treats as an instruction file. */
|
|
43
|
+
export function isInstructionFileName(name) {
|
|
44
|
+
return INSTRUCTION_FILE_NAMES.includes(name);
|
|
45
|
+
}
|
|
46
|
+
/** How deep the walk goes. Instruction files near the root are the ones agents read. */
|
|
47
|
+
const MAX_DEPTH = 6;
|
|
48
|
+
/**
|
|
49
|
+
* Find and read every instruction file under `root`.
|
|
50
|
+
*
|
|
51
|
+
* The ignore list is the repo map's, and for the same reason: a vendored
|
|
52
|
+
* `node_modules/**\/AGENTS.md` is not this repository's instruction to an agent, and
|
|
53
|
+
* counting it would make the total a number about somebody else's project. A caller
|
|
54
|
+
* may replace the list wholesale — a default nobody can turn off is not a default.
|
|
55
|
+
*
|
|
56
|
+
* Symlinked *directories* are refused outright, exactly as the map refuses them: a
|
|
57
|
+
* walk that follows one can leave the root. A symlinked instruction **file** is read
|
|
58
|
+
* (following it is the point — that is what the harness does too) and flagged.
|
|
59
|
+
*/
|
|
60
|
+
export function readInstructionSet(options) {
|
|
61
|
+
const reader = options.reader ?? nodeFsReader();
|
|
62
|
+
const ignore = options.ignore ?? DEFAULT_REPO_IGNORE;
|
|
63
|
+
const found = [];
|
|
64
|
+
const walk = (dir, depth) => {
|
|
65
|
+
const entries = reader
|
|
66
|
+
.list(dir === '' ? options.root : join(options.root, dir))
|
|
67
|
+
.map((entry) => entry.name)
|
|
68
|
+
.toSorted();
|
|
69
|
+
for (const name of entries) {
|
|
70
|
+
const relative = dir === '' ? name : `${dir}/${name}`;
|
|
71
|
+
if (isIgnored(relative, ignore))
|
|
72
|
+
continue;
|
|
73
|
+
const stat = reader.stat(join(options.root, relative));
|
|
74
|
+
if (stat === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
if (stat.isDirectory && !stat.isSymlink) {
|
|
77
|
+
if (depth < MAX_DEPTH)
|
|
78
|
+
walk(relative, depth + 1);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!isInstructionFileName(name))
|
|
82
|
+
continue;
|
|
83
|
+
// A symlinked instruction file is **read**, unlike a symlinked directory, which
|
|
84
|
+
// is refused. Following it is the point: it is the arrangement the guide
|
|
85
|
+
// recommends for mirrors, and it is exactly what the harness does when it opens
|
|
86
|
+
// `CLAUDE.md`. Two readers disagree about how to describe one, and both are
|
|
87
|
+
// accommodated here: `nodeFsReader` lstats, so a link is `isSymlink` and *not*
|
|
88
|
+
// `isFile`; a resolving reader answers about the target, so it is both.
|
|
89
|
+
if (!stat.isFile && !stat.isSymlink)
|
|
90
|
+
continue;
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = reader.read(join(options.root, relative));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// A broken symlink, or a file that vanished between the stat and the read.
|
|
97
|
+
// Nothing is loaded on every request, so there is nothing to report.
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
found.push({
|
|
101
|
+
path: relative,
|
|
102
|
+
name,
|
|
103
|
+
dir,
|
|
104
|
+
text: new TextDecoder('utf-8').decode(raw),
|
|
105
|
+
// The bytes on disk, straight off the read — never `Buffer.byteLength(text)`.
|
|
106
|
+
// Decoding and re-encoding is not the identity: a UTF-8 BOM is dropped on the
|
|
107
|
+
// way in (19 bytes read, 16 reported) and a byte that is not valid UTF-8 comes
|
|
108
|
+
// back as a three-byte U+FFFD. Both skew the one number this verb exists to
|
|
109
|
+
// state, in opposite directions, against a figure the harness measures on the
|
|
110
|
+
// file itself. `stat.size` is not the answer either: on a symlinked mirror
|
|
111
|
+
// `lstat` sizes the *link*, and following it is the point.
|
|
112
|
+
bytes: raw.byteLength,
|
|
113
|
+
symlink: stat.isSymlink,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
walk('', 0);
|
|
118
|
+
const byDir = new Map();
|
|
119
|
+
for (const file of found) {
|
|
120
|
+
const bucket = byDir.get(file.dir);
|
|
121
|
+
if (bucket === undefined)
|
|
122
|
+
byDir.set(file.dir, [file]);
|
|
123
|
+
else
|
|
124
|
+
bucket.push(file);
|
|
125
|
+
}
|
|
126
|
+
const levels = [...byDir.entries()]
|
|
127
|
+
// Root first, then by depth and then alphabetically: the order a reader would
|
|
128
|
+
// walk the tree in, so a report reads top-down.
|
|
129
|
+
.toSorted(([a], [b]) => depthOf(a) - depthOf(b) || (a < b ? -1 : a > b ? 1 : 0))
|
|
130
|
+
.map(([dir, files]) => {
|
|
131
|
+
const ordered = files.toSorted((a, b) => INSTRUCTION_FILE_NAMES.indexOf(a.name) - INSTRUCTION_FILE_NAMES.indexOf(b.name));
|
|
132
|
+
return { dir, primary: ordered[0], mirrors: ordered.slice(1) };
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
root: options.root,
|
|
136
|
+
levels,
|
|
137
|
+
totalBytes: levels.reduce((sum, level) => sum + level.primary.bytes, 0),
|
|
138
|
+
perRequestBytes: heaviestChainBytes(levels),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The heaviest ancestor chain: the most any single request can load.
|
|
143
|
+
*
|
|
144
|
+
* A request made in `pkg/a` merges the root level with `pkg/a`'s, and stops. `pkg/b` is
|
|
145
|
+
* a sibling, and no agent loads a sibling — so the maximum over chains is the honest
|
|
146
|
+
* per-request figure, and the sum over levels is not one.
|
|
147
|
+
*/
|
|
148
|
+
function heaviestChainBytes(levels) {
|
|
149
|
+
const bytesByDir = new Map(levels.map((level) => [level.dir, level.primary.bytes]));
|
|
150
|
+
let heaviest = 0;
|
|
151
|
+
for (const level of levels) {
|
|
152
|
+
const chain = ancestorDirs(level.dir).reduce((sum, dir) => sum + (bytesByDir.get(dir) ?? 0), level.primary.bytes);
|
|
153
|
+
heaviest = Math.max(heaviest, chain);
|
|
154
|
+
}
|
|
155
|
+
return heaviest;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The directories a level merges *with*, root first and nearest last — and never the
|
|
159
|
+
* level itself.
|
|
160
|
+
*
|
|
161
|
+
* The whole distinction `restated-at-level` and {@link InstructionSet.perRequestBytes}
|
|
162
|
+
* both turn on: `pkg/a` merges with `''` and `pkg`, and with nothing else in the tree.
|
|
163
|
+
*/
|
|
164
|
+
export function ancestorDirs(dir) {
|
|
165
|
+
if (dir === '')
|
|
166
|
+
return [];
|
|
167
|
+
const segments = dir.split('/');
|
|
168
|
+
const out = [''];
|
|
169
|
+
for (let cut = 1; cut < segments.length; cut += 1)
|
|
170
|
+
out.push(segments.slice(0, cut).join('/'));
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/** `''` is depth 0; `a/b` is depth 2. */
|
|
174
|
+
function depthOf(dir) {
|
|
175
|
+
return dir === '' ? 0 : dir.split('/').length;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The repo map's ignore semantics, restated over root-relative paths: a bare name
|
|
179
|
+
* matches any path segment, an entry containing `/` is a root-relative prefix.
|
|
180
|
+
*/
|
|
181
|
+
function isIgnored(relative, ignore) {
|
|
182
|
+
const segments = relative.split('/');
|
|
183
|
+
return ignore.some((entry) => {
|
|
184
|
+
const trimmed = entry.endsWith('/') ? entry.slice(0, -1) : entry;
|
|
185
|
+
if (trimmed === '')
|
|
186
|
+
return false;
|
|
187
|
+
if (entry.includes('/')) {
|
|
188
|
+
return relative === trimmed || relative.startsWith(`${trimmed}/`);
|
|
189
|
+
}
|
|
190
|
+
return segments.includes(trimmed);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Does `token`, read as a path relative to the repository root, name something that
|
|
195
|
+
* exists? The one question `dead-path` and `dead-link` both ask.
|
|
196
|
+
*
|
|
197
|
+
* Through the reader, so the resolution is part of the same injectable walk the
|
|
198
|
+
* discovery is — a fixture repo can therefore contain a path that resolves and one
|
|
199
|
+
* that does not without either of them existing on the machine running the tests.
|
|
200
|
+
* A reader that throws (a malformed path, a permission error) answers "no": the
|
|
201
|
+
* finding is advisory, and a lint that failed the whole run because one token in
|
|
202
|
+
* somebody's prose was unstattable would be worse than the staleness it looks for.
|
|
203
|
+
*/
|
|
204
|
+
export function resolvesInTree(root, reader, token) {
|
|
205
|
+
try {
|
|
206
|
+
return reader.stat(join(root, token)) !== undefined;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=instructions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instructions.js","sourceRoot":"","sources":["../../src/agents/instructions.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from 'node:path';\n\nimport { DEFAULT_REPO_IGNORE } from '../repomap/map.ts';\nimport { nodeFsReader } from '../repomap/reader.ts';\nimport type { RepoReader } from '../repomap/reader.ts';\n\n/**\n * The instruction files an agent actually loads, found through the read-only\n * {@link RepoReader} seam.\n *\n * **Why the merged set, and not just the root file** (ruling R8): the guide's own rule\n * is that a nested `AGENTS.md` *merges with* the root one rather than replacing it, so\n * a lint that reported only the root file would be reporting a fraction and calling it\n * the total, which is this repository's Law 4 pointed at its own output.\n *\n * **But a merge runs up the tree, not across it.** Two numbers come out of that, and\n * they are different numbers in any monorepo, so both are reported and each is labelled\n * with the question it answers:\n *\n * - {@link InstructionSet.perRequestBytes} — the most any *one* request loads: a level\n * plus its **ancestors**. An agent working in `pkg/a` loads the root file and\n * `pkg/a`'s; it never loads `pkg/b`'s, because siblings do not merge. This is the\n * per-request cost, and it is the number the guide's argument is about.\n * - {@link InstructionSet.totalBytes} — every level's primary summed: the whole\n * repository's instruction surface. Useful as the size of what a team maintains,\n * and **not** a per-request cost. Calling it one would be the same over-count this\n * module refuses for mirrors one paragraph down.\n *\n * **Why a mirror is not a level.** Claude Code reads `CLAUDE.md`, Gemini reads\n * `GEMINI.md`, Codex and the rest read `AGENTS.md`. One agent loads *one* of them, so\n * summing all three would triple a cost nobody pays. At each directory the primary is\n * the file the level contributes — `AGENTS.md` when it exists, otherwise whichever\n * mirror is there alone — and the others are {@link InstructionLevel.mirrors}, counted\n * for drift and not for bytes.\n *\n * **Why the reader is a seam.** Every claim the lint makes is a claim about a *walk*:\n * that a nested file was found, that an ignored directory was never entered, that a\n * path in the prose resolves against the real tree. Those are claims about calls, so\n * they are asserted by counting calls against a stub tree (`test/repo-reader-stub.ts`),\n * exactly as the repo map's walk is. The default is {@link nodeFsReader}, so nothing\n * changes for a caller who does not care.\n */\n\n/** The three file names a coding agent reads instructions from, in precedence order. */\nexport const INSTRUCTION_FILE_NAMES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'] as const;\n\n/** One of the three names above. */\nexport type InstructionFileName = (typeof INSTRUCTION_FILE_NAMES)[number];\n\n/** True for a name this module treats as an instruction file. */\nexport function isInstructionFileName(name: string): name is InstructionFileName {\n return (INSTRUCTION_FILE_NAMES as readonly string[]).includes(name);\n}\n\n/** One instruction file on disk, read. */\nexport interface InstructionFile {\n /** Root-relative and `/`-separated, e.g. `packages/core/AGENTS.md`. */\n readonly path: string;\n readonly name: InstructionFileName;\n /** The directory it sits in, root-relative; `''` for the repository root. */\n readonly dir: string;\n readonly text: string;\n /** UTF-8 bytes. The unit every smelt budget is in. */\n readonly bytes: number;\n /**\n * True when the entry is a symlink — the arrangement the guide recommends for\n * mirrors, and the one arrangement in which a mirror **cannot** drift.\n */\n readonly symlink: boolean;\n}\n\n/**\n * One directory's contribution: the file that level costs, plus the mirrors of it.\n *\n * `primary` is the only one whose bytes are summed. A mirror that differs from it is\n * `mirror-drift`; a mirror that is a symlink cannot differ, which is the whole reason\n * the guide suggests one.\n */\nexport interface InstructionLevel {\n /** Root-relative directory; `''` is the root level. */\n readonly dir: string;\n readonly primary: InstructionFile;\n readonly mirrors: readonly InstructionFile[];\n}\n\n/** Every instruction file in a tree, arranged by level. */\nexport interface InstructionSet {\n /** The directory the walk started from, as the caller spelled it. */\n readonly root: string;\n /** Root level first, then nested levels in walk order. */\n readonly levels: readonly InstructionLevel[];\n /**\n * The repository-wide instruction surface: every level's primary, summed.\n *\n * **Not a per-request cost.** Sibling levels never merge, so no single request ever\n * loads all of these — see {@link perRequestBytes} for the number that answers that\n * question.\n */\n readonly totalBytes: number;\n /**\n * What the most expensive single request loads: the heaviest level plus every level\n * above it, since a nested file merges with its **ancestors** and with nothing else.\n *\n * Equal to {@link totalBytes} in a repository with one chain of levels, and strictly\n * smaller as soon as two siblings both carry an instruction file.\n */\n readonly perRequestBytes: number;\n}\n\n/** How deep the walk goes. Instruction files near the root are the ones agents read. */\nconst MAX_DEPTH = 6;\n\n/**\n * Find and read every instruction file under `root`.\n *\n * The ignore list is the repo map's, and for the same reason: a vendored\n * `node_modules/**\\/AGENTS.md` is not this repository's instruction to an agent, and\n * counting it would make the total a number about somebody else's project. A caller\n * may replace the list wholesale — a default nobody can turn off is not a default.\n *\n * Symlinked *directories* are refused outright, exactly as the map refuses them: a\n * walk that follows one can leave the root. A symlinked instruction **file** is read\n * (following it is the point — that is what the harness does too) and flagged.\n */\nexport function readInstructionSet(options: {\n readonly root: string;\n readonly reader?: RepoReader;\n readonly ignore?: readonly string[];\n}): InstructionSet {\n const reader = options.reader ?? nodeFsReader();\n const ignore = options.ignore ?? DEFAULT_REPO_IGNORE;\n const found: InstructionFile[] = [];\n\n const walk = (dir: string, depth: number): void => {\n const entries = reader\n .list(dir === '' ? options.root : join(options.root, dir))\n .map((entry) => entry.name)\n .toSorted();\n\n for (const name of entries) {\n const relative = dir === '' ? name : `${dir}/${name}`;\n if (isIgnored(relative, ignore)) continue;\n const stat = reader.stat(join(options.root, relative));\n if (stat === undefined) continue;\n if (stat.isDirectory && !stat.isSymlink) {\n if (depth < MAX_DEPTH) walk(relative, depth + 1);\n continue;\n }\n if (!isInstructionFileName(name)) continue;\n // A symlinked instruction file is **read**, unlike a symlinked directory, which\n // is refused. Following it is the point: it is the arrangement the guide\n // recommends for mirrors, and it is exactly what the harness does when it opens\n // `CLAUDE.md`. Two readers disagree about how to describe one, and both are\n // accommodated here: `nodeFsReader` lstats, so a link is `isSymlink` and *not*\n // `isFile`; a resolving reader answers about the target, so it is both.\n if (!stat.isFile && !stat.isSymlink) continue;\n let raw: Uint8Array;\n try {\n raw = reader.read(join(options.root, relative));\n } catch {\n // A broken symlink, or a file that vanished between the stat and the read.\n // Nothing is loaded on every request, so there is nothing to report.\n continue;\n }\n found.push({\n path: relative,\n name,\n dir,\n text: new TextDecoder('utf-8').decode(raw),\n // The bytes on disk, straight off the read — never `Buffer.byteLength(text)`.\n // Decoding and re-encoding is not the identity: a UTF-8 BOM is dropped on the\n // way in (19 bytes read, 16 reported) and a byte that is not valid UTF-8 comes\n // back as a three-byte U+FFFD. Both skew the one number this verb exists to\n // state, in opposite directions, against a figure the harness measures on the\n // file itself. `stat.size` is not the answer either: on a symlinked mirror\n // `lstat` sizes the *link*, and following it is the point.\n bytes: raw.byteLength,\n symlink: stat.isSymlink,\n });\n }\n };\n\n walk('', 0);\n\n const byDir = new Map<string, InstructionFile[]>();\n for (const file of found) {\n const bucket = byDir.get(file.dir);\n if (bucket === undefined) byDir.set(file.dir, [file]);\n else bucket.push(file);\n }\n\n const levels: InstructionLevel[] = [...byDir.entries()]\n // Root first, then by depth and then alphabetically: the order a reader would\n // walk the tree in, so a report reads top-down.\n .toSorted(([a], [b]) => depthOf(a) - depthOf(b) || (a < b ? -1 : a > b ? 1 : 0))\n .map(([dir, files]) => {\n const ordered = files.toSorted(\n (a, b) => INSTRUCTION_FILE_NAMES.indexOf(a.name) - INSTRUCTION_FILE_NAMES.indexOf(b.name),\n );\n return { dir, primary: ordered[0]!, mirrors: ordered.slice(1) };\n });\n\n return {\n root: options.root,\n levels,\n totalBytes: levels.reduce((sum, level) => sum + level.primary.bytes, 0),\n perRequestBytes: heaviestChainBytes(levels),\n };\n}\n\n/**\n * The heaviest ancestor chain: the most any single request can load.\n *\n * A request made in `pkg/a` merges the root level with `pkg/a`'s, and stops. `pkg/b` is\n * a sibling, and no agent loads a sibling — so the maximum over chains is the honest\n * per-request figure, and the sum over levels is not one.\n */\nfunction heaviestChainBytes(levels: readonly InstructionLevel[]): number {\n const bytesByDir = new Map(levels.map((level) => [level.dir, level.primary.bytes]));\n let heaviest = 0;\n for (const level of levels) {\n const chain = ancestorDirs(level.dir).reduce(\n (sum, dir) => sum + (bytesByDir.get(dir) ?? 0),\n level.primary.bytes,\n );\n heaviest = Math.max(heaviest, chain);\n }\n return heaviest;\n}\n\n/**\n * The directories a level merges *with*, root first and nearest last — and never the\n * level itself.\n *\n * The whole distinction `restated-at-level` and {@link InstructionSet.perRequestBytes}\n * both turn on: `pkg/a` merges with `''` and `pkg`, and with nothing else in the tree.\n */\nexport function ancestorDirs(dir: string): readonly string[] {\n if (dir === '') return [];\n const segments = dir.split('/');\n const out: string[] = [''];\n for (let cut = 1; cut < segments.length; cut += 1) out.push(segments.slice(0, cut).join('/'));\n return out;\n}\n\n/** `''` is depth 0; `a/b` is depth 2. */\nfunction depthOf(dir: string): number {\n return dir === '' ? 0 : dir.split('/').length;\n}\n\n/**\n * The repo map's ignore semantics, restated over root-relative paths: a bare name\n * matches any path segment, an entry containing `/` is a root-relative prefix.\n */\nfunction isIgnored(relative: string, ignore: readonly string[]): boolean {\n const segments = relative.split('/');\n return ignore.some((entry) => {\n const trimmed = entry.endsWith('/') ? entry.slice(0, -1) : entry;\n if (trimmed === '') return false;\n if (entry.includes('/')) {\n return relative === trimmed || relative.startsWith(`${trimmed}/`);\n }\n return segments.includes(trimmed);\n });\n}\n\n/**\n * Does `token`, read as a path relative to the repository root, name something that\n * exists? The one question `dead-path` and `dead-link` both ask.\n *\n * Through the reader, so the resolution is part of the same injectable walk the\n * discovery is — a fixture repo can therefore contain a path that resolves and one\n * that does not without either of them existing on the machine running the tests.\n * A reader that throws (a malformed path, a permission error) answers \"no\": the\n * finding is advisory, and a lint that failed the whole run because one token in\n * somebody's prose was unstattable would be worse than the staleness it looks for.\n */\nexport function resolvesInTree(root: string, reader: RepoReader, token: string): boolean {\n try {\n return reader.stat(join(root, token)) !== undefined;\n } catch {\n return false;\n }\n}\n"]}
|