vigiles 29.0.0 → 30.0.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/adapter-conformance.d.ts +1 -1
- package/dist/adapter-conformance.js +106 -25
- package/dist/adapter-registry.d.ts +61 -14
- package/dist/adapter-registry.js +78 -10
- package/dist/adapter.d.ts +23 -2
- package/dist/adapter.js +13 -1
- package/dist/adapters/claude-code/adapter.d.ts +32 -2
- package/dist/adapters/claude-code/adapter.js +44 -23
- package/dist/adapters/claude-code/dialect.js +87 -21
- package/dist/adapters/claude-code/hook-protocol.js +16 -0
- package/dist/adapters/claude-code/instruction-chain.d.ts +25 -0
- package/dist/adapters/claude-code/instruction-chain.js +626 -0
- package/dist/adapters/claude-code/layout.d.ts +2 -2
- package/dist/adapters/claude-code/layout.js +42 -8
- package/dist/adapters/claude-code/model-access.d.ts +41 -0
- package/dist/adapters/claude-code/model-access.js +46 -0
- package/dist/adapters/claude-code/skill-reachability.d.ts +125 -0
- package/dist/adapters/claude-code/skill-reachability.js +111 -0
- package/dist/adapters/codex/adapter.d.ts +39 -2
- package/dist/adapters/codex/adapter.js +29 -29
- package/dist/adapters/codex/dialect.js +11 -6
- package/dist/adapters/codex/eval.d.ts +10 -0
- package/dist/adapters/codex/eval.js +48 -1
- package/dist/adapters/codex/hook-protocol.d.ts +2 -1
- package/dist/adapters/codex/hook-protocol.js +10 -0
- package/dist/adapters/codex/instruction-chain.d.ts +40 -0
- package/dist/adapters/codex/instruction-chain.js +105 -0
- package/dist/adapters/codex/layout.d.ts +1 -1
- package/dist/adapters/codex/layout.js +41 -14
- package/dist/adapters/opencode/adapter.d.ts +33 -2
- package/dist/adapters/opencode/adapter.js +36 -36
- package/dist/adapters/opencode/dialect.js +2 -2
- package/dist/adapters/opencode/instruction-chain.d.ts +37 -0
- package/dist/adapters/opencode/instruction-chain.js +70 -0
- package/dist/adapters/opencode/layout.d.ts +19 -0
- package/dist/adapters/opencode/layout.js +34 -15
- package/dist/adoptability.d.ts +31 -1
- package/dist/adoptability.js +57 -0
- package/dist/cli-main.js +180 -102
- package/dist/core/adapter.d.ts +213 -61
- package/dist/core/compile.d.ts +2 -2
- package/dist/core/compile.js +57 -38
- package/dist/core/compose.d.ts +5 -3
- package/dist/core/compose.js +5 -3
- package/dist/core/config-schema.d.ts +14 -2
- package/dist/core/config-schema.js +24 -3
- package/dist/core/dialect.d.ts +54 -12
- package/dist/core/dialect.js +56 -0
- package/dist/core/eval-driver.d.ts +194 -0
- package/dist/core/eval-driver.js +3 -0
- package/dist/core/frontmatter-read.d.ts +10 -0
- package/dist/core/frontmatter-read.js +30 -3
- package/dist/core/hook-program.d.ts +27 -2
- package/dist/core/hook-program.js +29 -24
- package/dist/core/hook-protocol.d.ts +54 -0
- package/dist/core/install-reader.d.ts +18 -0
- package/dist/core/install-reader.js +88 -0
- package/dist/core/instruction-chain.d.ts +444 -0
- package/dist/core/instruction-chain.js +292 -0
- package/dist/core/instruction-weight.d.ts +96 -14
- package/dist/core/instruction-weight.js +65 -30
- package/dist/core/layout.d.ts +220 -33
- package/dist/core/layout.js +115 -1
- package/dist/core/lethal-trifecta.d.ts +12 -7
- package/dist/core/lethal-trifecta.js +13 -8
- package/dist/core/live-driver.d.ts +137 -0
- package/dist/core/live-driver.js +14 -0
- package/dist/core/markdown.d.ts +23 -0
- package/dist/core/markdown.js +77 -28
- package/dist/core/orphans.js +9 -7
- package/dist/core/settings-codec.d.ts +17 -0
- package/dist/core/settings-codec.js +56 -0
- package/dist/core/surface-discovery.d.ts +2 -2
- package/dist/core/surface-discovery.js +24 -8
- package/dist/core/surface-scopes.d.ts +26 -6
- package/dist/core/surface-scopes.js +52 -11
- package/dist/core/validate.js +16 -3
- package/dist/eval.d.ts +16 -108
- package/dist/eval.js +34 -1
- package/dist/harness-test.d.ts +3 -63
- package/dist/hook-install.d.ts +12 -1
- package/dist/hook-install.js +12 -1
- package/dist/plugin-loader.d.ts +1 -1
- package/dist/plugin-loader.js +43 -36
- package/dist/scan-behavioral.d.ts +34 -25
- package/dist/scan-behavioral.js +122 -58
- package/dist/scan-core.js +37 -18
- package/dist/scan-files.d.ts +1 -1
- package/dist/scan-files.js +53 -33
- package/dist/scan-trigger-suggest.d.ts +0 -21
- package/dist/scan-trigger-suggest.js +0 -23
- package/dist/scan.d.ts +4 -4
- package/dist/scan.js +120 -73
- package/dist/skill-harness.d.ts +21 -5
- package/dist/skill-harness.js +29 -11
- package/dist/surface-discovery-fs.d.ts +2 -0
- package/dist/surface-discovery-fs.js +108 -6
- package/dist/test-coverage-files.js +24 -17
- package/dist/test-coverage.d.ts +9 -3
- package/dist/test-coverage.js +32 -17
- package/dist/verify-plugin-guards.js +1 -1
- package/package.json +1 -1
- package/dist/skill-reachability.d.ts +0 -68
- package/dist/skill-reachability.js +0 -205
- /package/dist/{dialect-drift.d.ts → adapters/claude-code/dialect-drift.d.ts} +0 -0
- /package/dist/{dialect-drift.js → adapters/claude-code/dialect-drift.js} +0 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INSTRUCTION_SHAPES = exports.EMPTY_CHAIN = void 0;
|
|
4
|
+
exports.siblingNamed = siblingNamed;
|
|
5
|
+
exports.settingsSources = settingsSources;
|
|
6
|
+
exports.settingsSourcePaths = settingsSourcePaths;
|
|
7
|
+
exports.isInstructionShaped = isInstructionShaped;
|
|
8
|
+
exports.instructionCandidatePaths = instructionCandidatePaths;
|
|
9
|
+
exports.isRepoRootedImport = isRepoRootedImport;
|
|
10
|
+
exports.resolveImportPath = resolveImportPath;
|
|
11
|
+
exports.resolveImports = resolveImports;
|
|
12
|
+
const layout_js_1 = require("./layout.js");
|
|
13
|
+
/** An empty chain — the answer for a harness with no instruction surface. */
|
|
14
|
+
exports.EMPTY_CHAIN = {
|
|
15
|
+
loaded: [],
|
|
16
|
+
unloaded: [],
|
|
17
|
+
imports: [],
|
|
18
|
+
patterns: [],
|
|
19
|
+
redirects: [],
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The SHAPE of an instruction candidate — the domain's bound, stated the same
|
|
23
|
+
* way `SURFACE_SHAPES` states the surface one, and for the same reason: if an
|
|
24
|
+
* adapter could add a root we would be back to "registering an adapter widens
|
|
25
|
+
* the read in everyone's repository".
|
|
26
|
+
*
|
|
27
|
+
* These are CROSS-VENDOR shapes, not one harness's paths. `rules` is the name
|
|
28
|
+
* Claude Code (`.claude/rules`), Cursor (`.cursor/rules`) and Windsurf
|
|
29
|
+
* (`.windsurf/rules`) all use; the dot-directory is the variable, the shape name
|
|
30
|
+
* is not. Nothing here spells a harness's own directory.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ WHAT THIS CANNOT SEE, stated rather than assumed: a nested
|
|
33
|
+
* `packages/x/AGENTS.md` (neither vendor loads it at a root session — the chain
|
|
34
|
+
* classifies one as on-demand if it is handed one, and never goes looking);
|
|
35
|
+
* `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` and every other home-directory
|
|
36
|
+
* file (reading `~` for a grade is wrong on its face); and a repo-configured
|
|
37
|
+
* fallback instruction name that is not markdown, because the root entry below
|
|
38
|
+
* is bounded to `.md` rather than to "every file in the repo root". That last
|
|
39
|
+
* one under-reports, which is the wrong direction, and it is the price of not
|
|
40
|
+
* reading a lockfile to grade a harness.
|
|
41
|
+
*/
|
|
42
|
+
exports.INSTRUCTION_SHAPES = [
|
|
43
|
+
{ what: "root markdown", re: /^[^/]+\.md$/ },
|
|
44
|
+
{ what: "dot-directory markdown", re: /^\.[^/]+\/[^/]+\.md$/ },
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* 🔴 THE RULES TREE IS NOT IN THE TABLE ABOVE, and that is the fix for #271.
|
|
48
|
+
*
|
|
49
|
+
* It used to be, as `/^\.[^/]+\/rules\/…/` — a shape that spelled the word
|
|
50
|
+
* `rules` itself and demanded a leading dot. Measured against three layouts
|
|
51
|
+
* built from the TYPE rather than taken from the registry, it answered the same
|
|
52
|
+
* thing for all of them:
|
|
53
|
+
*
|
|
54
|
+
* rulesDir "rules", no userSurfaceRoot -> `rules/a.md` NOT a candidate
|
|
55
|
+
* rulesDir "guidelines" under `.x` -> `.x/guidelines/a.md` NOT a candidate
|
|
56
|
+
* no rulesDir at all -> `.github/rules/a.md` IS a candidate
|
|
57
|
+
*
|
|
58
|
+
* Three wrong answers of two kinds: a home the layout DECLARED going unread,
|
|
59
|
+
* and somebody else's tree being read for a layout that declared none. The
|
|
60
|
+
* second is the same defect `ruleFileRe` was created for one commit earlier —
|
|
61
|
+
* fixed there for the CLASSIFIER and left standing here in the BOUND, which is
|
|
62
|
+
* exactly the "two readers, one fact" split this port exists to remove.
|
|
63
|
+
*
|
|
64
|
+
* So the bound asks the layout, through the same function the classifier uses.
|
|
65
|
+
* This does not let an adapter widen the bound: `rulesDir` is a directory NAME,
|
|
66
|
+
* the regexp is anchored at the repository root, and a layout that declares no
|
|
67
|
+
* rules home gets `null` and matches nothing.
|
|
68
|
+
*/
|
|
69
|
+
function isDeclaredRuleFile(path, layout) {
|
|
70
|
+
const re = (0, layout_js_1.ruleFileRe)(layout);
|
|
71
|
+
return re !== null && re.test(path);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The files that carry SETTINGS for one layout — the parse target and the
|
|
75
|
+
* per-machine override beside it.
|
|
76
|
+
*
|
|
77
|
+
* 🔴 A SETTINGS SOURCE IS NOT AN INSTRUCTION, and keeping the two roles apart
|
|
78
|
+
* is the whole point of this function having its own name. These files are
|
|
79
|
+
* handed to `instructionChain` and are never weighed, never appear in `loaded`
|
|
80
|
+
* and never get a `role`: they are not read TO the model, they decide WHICH
|
|
81
|
+
* files are. Claude Code's `claudeMdExcludes` and Codex's
|
|
82
|
+
* `project_doc_fallback_filenames` both live in one, which is why they are in
|
|
83
|
+
* the bound at all.
|
|
84
|
+
*
|
|
85
|
+
* The `.local` sibling is DERIVED from `settingsPath` rather than listed,
|
|
86
|
+
* because a second list is the defect this whole redesign removes. It is
|
|
87
|
+
* advisory for the same reason `scope: "local"` is: it is gitignored by
|
|
88
|
+
* convention, so the browser twin can never see it, and anything that DEPENDED
|
|
89
|
+
* on it would make the two engines disagree. It can only narrow what loads.
|
|
90
|
+
*/
|
|
91
|
+
/**
|
|
92
|
+
* `AGENTS.md` + `override` → `AGENTS.override.md`; `settings.json` + `local` →
|
|
93
|
+
* `settings.local.json`. The ONE place the "sibling file" spelling lives.
|
|
94
|
+
*
|
|
95
|
+
* Both vendors name a per-machine file by inserting a word before the
|
|
96
|
+
* extension, and they choose DIFFERENT words — Claude Code `local`, Codex
|
|
97
|
+
* `override` — so the word is the argument and the spelling is not. A file with
|
|
98
|
+
* no extension gets the word appended, which is the only reading that does not
|
|
99
|
+
* invent a dot.
|
|
100
|
+
*/
|
|
101
|
+
function siblingNamed(path, infix) {
|
|
102
|
+
const dot = path.lastIndexOf(".");
|
|
103
|
+
const slash = path.lastIndexOf("/");
|
|
104
|
+
return dot > slash && dot > slash + 1
|
|
105
|
+
? `${path.slice(0, dot)}.${infix}${path.slice(dot)}`
|
|
106
|
+
: `${path}.${infix}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The settings files in precedence order, each carrying its SCOPE.
|
|
110
|
+
*
|
|
111
|
+
* The scope is the whole point: a pattern read out of a gitignored sibling may
|
|
112
|
+
* not change what a teammate on this commit is scored for. Callers that only
|
|
113
|
+
* need the paths use {@link settingsSourcePaths}, which is this list flattened.
|
|
114
|
+
*/
|
|
115
|
+
function settingsSources(layout) {
|
|
116
|
+
const infix = layout.settingsLocalInfix;
|
|
117
|
+
const committed = {
|
|
118
|
+
path: layout.settingsPath,
|
|
119
|
+
scope: "repo",
|
|
120
|
+
};
|
|
121
|
+
return infix === undefined
|
|
122
|
+
? [committed]
|
|
123
|
+
: [
|
|
124
|
+
committed,
|
|
125
|
+
{ path: siblingNamed(layout.settingsPath, infix), scope: "local" },
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
function settingsSourcePaths(layout) {
|
|
129
|
+
// DECLARED, not derived — see `PluginLayout.settingsLocalInfix` for the
|
|
130
|
+
// measurement. A harness that names no infix has no per-machine settings
|
|
131
|
+
// layer, and inventing one for it manufactures a file to read.
|
|
132
|
+
return settingsSources(layout).map((s) => s.path);
|
|
133
|
+
}
|
|
134
|
+
/** Does this repo-relative path match one of the {@link INSTRUCTION_SHAPES}? */
|
|
135
|
+
function isInstructionShaped(path) {
|
|
136
|
+
return exports.INSTRUCTION_SHAPES.some((s) => s.re.test(path));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The bounded candidate set: every path a harness may be ASKED about.
|
|
140
|
+
*
|
|
141
|
+
* Pure and storage-blind on purpose, exactly as `discoverSurfaces` is — the
|
|
142
|
+
* disk walk (`src/surface-discovery-fs.ts`) and the browser file-map twin
|
|
143
|
+
* (`src/scan-files.ts`) each enumerate from their own storage and call THIS for
|
|
144
|
+
* the decision, so the pair cannot disagree about what is a candidate.
|
|
145
|
+
*/
|
|
146
|
+
function instructionCandidatePaths(paths, layout) {
|
|
147
|
+
const named = new Set([
|
|
148
|
+
layout.instructionFile,
|
|
149
|
+
...settingsSourcePaths(layout),
|
|
150
|
+
]);
|
|
151
|
+
return paths.filter((p) => named.has(p) || isInstructionShaped(p) || isDeclaredRuleFile(p, layout));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Is `token` a path this repo could hold, and safe to resolve against its root?
|
|
155
|
+
*
|
|
156
|
+
* Refuses an absolute path, a `~` home reference, and anything with a `..`
|
|
157
|
+
* segment — an instruction file that points outside the repository is not a
|
|
158
|
+
* fact about the repository, and following it would let a file decide what
|
|
159
|
+
* vigiles opens on the machine running it.
|
|
160
|
+
*/
|
|
161
|
+
function isRepoRootedImport(token) {
|
|
162
|
+
if (token === "" || token.startsWith("/") || token.startsWith("~")) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (/^[A-Za-z]:/.test(token) || token.includes("\\"))
|
|
166
|
+
return false;
|
|
167
|
+
return !token.split("/").includes("..");
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Where an `@import` token inside `from` actually points.
|
|
171
|
+
*
|
|
172
|
+
* 🔴 RELATIVE TO THE IMPORTING FILE, NOT TO THE REPOSITORY ROOT, and that is a
|
|
173
|
+
* MEASUREMENT rather than a reading of the docs — Claude Code 2.1.278, fixture
|
|
174
|
+
* `q3-relative` in `test/fixtures/instruction-chain-vendor/`. The case is built
|
|
175
|
+
* so the answer cannot be "it found nothing": `.claude/CLAUDE.md` holds
|
|
176
|
+
* `@notes.md` and BOTH candidates exist, each with its own codeword.
|
|
177
|
+
*
|
|
178
|
+
* recited: SAIGA-8181 (`.claude/notes.md`) not TAPIR-6262 (`notes.md`)
|
|
179
|
+
* hook: .claude/notes.md load_reason: include parent: .claude/CLAUDE.md
|
|
180
|
+
*
|
|
181
|
+
* Resolving against the root instead reports that file unread and charges the
|
|
182
|
+
* weight of a DIFFERENT file that happens to share its name — wrong in both
|
|
183
|
+
* directions at once, and silent.
|
|
184
|
+
*
|
|
185
|
+
* ⚠️ A TOKEN WITH `..` STAYS REFUSED even though this resolution would make
|
|
186
|
+
* some of them land inside the repository (`@../notes.md` from `.claude/`).
|
|
187
|
+
* {@link isRepoRootedImport} rejects them before this is called, and lifting
|
|
188
|
+
* that is a separate decision needing its own fixture: the refusal is what
|
|
189
|
+
* stops an instruction file deciding what vigiles opens on the machine running
|
|
190
|
+
* it, and "it happens to stay inside" is a property of one path, not a rule.
|
|
191
|
+
*/
|
|
192
|
+
function resolveImportPath(from, token) {
|
|
193
|
+
const slash = from.lastIndexOf("/");
|
|
194
|
+
const dir = slash === -1 ? "" : from.slice(0, slash + 1);
|
|
195
|
+
// 🔴 EVERY `.` SEGMENT, NOT JUST THE LEADING ONE. A first fix stripped
|
|
196
|
+
// `^(\./)+` only, which made `@./notes.md` and `@notes.md` one key and left
|
|
197
|
+
// `@docs/./style.md` as `docs/./style.md`. That is invisible on disk — `join`
|
|
198
|
+
// normalises it for the reader — and wrong in the browser, whose file map is
|
|
199
|
+
// keyed `docs/style.md`: the file is reported unread and its bytes vanish
|
|
200
|
+
// from the weight. Two engines, one path, and only one of them normalising is
|
|
201
|
+
// the disagreement the shared candidate set exists to prevent.
|
|
202
|
+
//
|
|
203
|
+
// `..` is NOT handled here and must not be: `isRepoRootedImport` refuses those
|
|
204
|
+
// tokens before this is called, and collapsing one would quietly turn a
|
|
205
|
+
// refused path into an accepted one.
|
|
206
|
+
return `${dir}${token}`
|
|
207
|
+
.split("/")
|
|
208
|
+
.filter((seg, i, all) => seg !== "." && (seg !== "" || i === all.length - 1))
|
|
209
|
+
.join("/");
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Read the `@import` paths the loaded files NAME — ONE LEVEL, no recursion.
|
|
213
|
+
* Returns a NEW map: the candidates handed in, plus whatever they named.
|
|
214
|
+
*
|
|
215
|
+
* 🔴 THIS READS OUTSIDE THE DOT-DIRECTORY BOUND, DELIBERATELY, AND THE REASON IS
|
|
216
|
+
* WHO CHOSE THE PATH. The bound exists so that REGISTERING AN ADAPTER cannot
|
|
217
|
+
* widen what vigiles reads in someone else's repository. An `@import` token is a
|
|
218
|
+
* concrete path written by the REPOSITORY OWNER in their own instruction file;
|
|
219
|
+
* the adapter only finds it, and `adapter-properties.test.ts` asserts exactly
|
|
220
|
+
* that — every reported import literally occurs in the file that reports it.
|
|
221
|
+
*
|
|
222
|
+
* 🔴 ONE LEVEL IS A MEASUREMENT, NOT A SHORTCUT — DO NOT "IMPROVE" IT INTO A
|
|
223
|
+
* RECURSIVE PASS. Across a corpus of 198 real `CLAUDE.md` files scraped from
|
|
224
|
+
* public repositories (July sample; the grep finds `@name.md` shapes only),
|
|
225
|
+
* exactly SIX files carried an import at all — 3%:
|
|
226
|
+
*
|
|
227
|
+
* 4 @AGENTS.md
|
|
228
|
+
* 1 @docs/architecture.md
|
|
229
|
+
* 1 @.maister/docs/INDEX.md
|
|
230
|
+
*
|
|
231
|
+
* Every one is a single concrete path at depth 1. Nothing in that corpus needs
|
|
232
|
+
* recursion, a depth budget or an exclude pass, and a recursive walk driven by
|
|
233
|
+
* strings found in files is the exact defect zernie/vigiles#262 is about.
|
|
234
|
+
*
|
|
235
|
+
* 🔴 AND THE SAME IS NOW MEASURED FOR `AGENTS.md`, WHICH USED TO BE THE HOLE IN
|
|
236
|
+
* THIS BOUND. The corpus above is `CLAUDE.md` BY CONSTRUCTION, so it said
|
|
237
|
+
* nothing about the family Claude Code reads natively since v2.1.277 — and a
|
|
238
|
+
* one-level bound justified by a corpus that could not contain the file is not
|
|
239
|
+
* justified, it is extrapolated. Measured over the same sample, by the same
|
|
240
|
+
* method, carrying the same two caveats (July sample of public repositories;
|
|
241
|
+
* the grep finds `@name.md` shapes only): 214 real `AGENTS.md` files, THREE
|
|
242
|
+
* carry an import at all — 1.4%:
|
|
243
|
+
*
|
|
244
|
+
* 1 @tasks/BASED.md
|
|
245
|
+
* 1 @ai-rules/rule-loading.md
|
|
246
|
+
* 1 @AGENTS.local.md
|
|
247
|
+
*
|
|
248
|
+
* Every one is a single concrete path at depth 1 — the same SHAPE and the same
|
|
249
|
+
* RARITY as the six on the `CLAUDE.md` side (3%). So one level is measured on
|
|
250
|
+
* both families rather than assumed to carry over from one.
|
|
251
|
+
*
|
|
252
|
+
* ⏳ THE THIRD OF THOSE THREE IS NOT AN ORDINARY IMPORT, and it is an OPEN
|
|
253
|
+
* QUESTION rather than a decided one: `AGENTS.local.md` is a name Claude Code
|
|
254
|
+
* lists under "Not read", so an explicit `@` token names a file the loader may
|
|
255
|
+
* never open. Both readings and the observation that settles them are at
|
|
256
|
+
* `isNeverRead` in `adapters/claude-code/instruction-chain.ts` — one harness's
|
|
257
|
+
* list belongs in one harness's adapter, not in the domain.
|
|
258
|
+
*
|
|
259
|
+
* ⚠️ AND THE VENDOR PUTS A NUMBER ON THE THING THIS BOUND APPROXIMATES, which
|
|
260
|
+
* the measurement above does not repeal: "Imported files can recursively import
|
|
261
|
+
* other files, with a maximum depth of FOUR HOPS" (same page, read 2026-09-21).
|
|
262
|
+
* So one level is a bound on what this reads, chosen because neither corpus has
|
|
263
|
+
* a second hop — not a claim that a second hop cannot exist. A repository that
|
|
264
|
+
* uses them is under-reported by the nested size, and the honest form of that
|
|
265
|
+
* is the sentence below rather than a depth counter nothing exercises.
|
|
266
|
+
*
|
|
267
|
+
* 🔴 AND THE SIX ARE WHERE THE NUMBER IS MOST WRONG WITHOUT THIS PASS. Four of
|
|
268
|
+
* them are `@AGENTS.md` — the workaround for Claude Code not yet reading
|
|
269
|
+
* `AGENTS.md` natively (anthropics/claude-code#34235; reversed in v2.1.277, see
|
|
270
|
+
* `adapters/claude-code/dialect.ts`). Skipping imports would still miss that
|
|
271
|
+
* file's whole size in exactly those repositories, because the vendor's rule is
|
|
272
|
+
* that a `CLAUDE.md` SUPPRESSES `AGENTS.md` — so the import is the only way in,
|
|
273
|
+
* and dropping it is an under-report, which reads as "you are fine".
|
|
274
|
+
*
|
|
275
|
+
* ⚠️ WHAT ONE LEVEL COSTS, stated rather than implied: a transitive import (an
|
|
276
|
+
* imported file that imports again) is a real Claude Code feature, and its
|
|
277
|
+
* nested size is NOT counted. NEITHER corpus — 198 `CLAUDE.md`, 214
|
|
278
|
+
* `AGENTS.md`, 412 files, nine imports between them — holds one; if a real case
|
|
279
|
+
* shows up, those measurements are the thing to redo, not this loop.
|
|
280
|
+
*/
|
|
281
|
+
function resolveImports(layout, files, read) {
|
|
282
|
+
const out = { ...files };
|
|
283
|
+
for (const { path } of layout.instructionChain(files).imports) {
|
|
284
|
+
if (out[path] !== undefined || !isRepoRootedImport(path))
|
|
285
|
+
continue;
|
|
286
|
+
const text = read(path);
|
|
287
|
+
if (text !== undefined)
|
|
288
|
+
out[path] = text;
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=instruction-chain.js.map
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* first consumer is `audit`, as a REPORT. It earns a severity when a corpus
|
|
36
36
|
* exists that it would not immediately fail.
|
|
37
37
|
*/
|
|
38
|
+
import type { InstructionChain, InstructionRole, InstructionScope } from "./instruction-chain.js";
|
|
38
39
|
/** What the harness counts, and what it does when the count is exceeded. */
|
|
39
40
|
export interface InstructionBudget {
|
|
40
41
|
/** Claude Code counts characters; Codex counts bytes. Never tokens. */
|
|
@@ -50,17 +51,53 @@ export interface InstructionBudget {
|
|
|
50
51
|
readonly onExceed: "warns" | "truncates";
|
|
51
52
|
/** The vendor artifact this was read from, version included. */
|
|
52
53
|
readonly capturedFrom: string;
|
|
53
|
-
/**
|
|
54
|
-
* Globs the harness loads WITHOUT the user asking — the set the SUM is taken
|
|
55
|
-
* over. A file reachable only by an explicit read does not belong here; that
|
|
56
|
-
* is exactly the distinction the relocation trick exploits.
|
|
57
|
-
*/
|
|
58
|
-
readonly alwaysLoaded: readonly string[];
|
|
59
54
|
}
|
|
60
55
|
/** One file's contribution, so a report can say WHERE the weight is. */
|
|
61
56
|
export interface WeighedFile {
|
|
62
57
|
readonly path: string;
|
|
63
58
|
readonly size: number;
|
|
59
|
+
/** What it is to the harness — a root file, a rule, an import. */
|
|
60
|
+
readonly role: InstructionRole;
|
|
61
|
+
/** `"local"` files are shown and never scored; see {@link InstructionScope}. */
|
|
62
|
+
readonly scope: InstructionScope;
|
|
63
|
+
/**
|
|
64
|
+
* Set when this file got into the count through an IMPORT — who named it, and
|
|
65
|
+
* with what text. The report prints it on the file's own line, because
|
|
66
|
+
* `AGENTS.md` appearing in a Claude Code weight reads as a bug until the line
|
|
67
|
+
* says `via @AGENTS.md in CLAUDE.md`. A total a reader cannot decompose is the
|
|
68
|
+
* failure this report exists to prevent.
|
|
69
|
+
*/
|
|
70
|
+
readonly via?: {
|
|
71
|
+
readonly from: string;
|
|
72
|
+
readonly token: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Set when this file pays into {@link InstructionWeight.committedTotal} but
|
|
76
|
+
* NOT into {@link InstructionWeight.effectiveTotal}: a PER-MACHINE file in
|
|
77
|
+
* this working copy supersedes it, so a teammate on the same commit loads it
|
|
78
|
+
* and you do not. Names the file that did it.
|
|
79
|
+
*
|
|
80
|
+
* 🔴 THE ONE CASE WHERE THE TWO TOTALS MOVE IN OPPOSITE DIRECTIONS, and the
|
|
81
|
+
* reason this is a field rather than a filter at the print site. Every other
|
|
82
|
+
* per-machine effect is ADDITIVE — a `CLAUDE.local.md` appends its own bytes,
|
|
83
|
+
* so `effective = committed + locals` — which is why it was safe for
|
|
84
|
+
* `effectiveTotal` to be "the sum of everything loaded". Claude Code's
|
|
85
|
+
* supersede rule breaks that: "Because `CLAUDE.local.md` counts, adding one
|
|
86
|
+
* to keep your own uncommitted instructions in a project that relies on
|
|
87
|
+
* `AGENTS.md` stops Claude from reading `AGENTS.md` for you." The gitignored
|
|
88
|
+
* file changes the MEMBERSHIP of the load, not its size, and the committed
|
|
89
|
+
* number has to keep a file this working copy never opens.
|
|
90
|
+
*
|
|
91
|
+
* A reader who could not see this on the file's own line would meet a
|
|
92
|
+
* `committedTotal` larger than the `effectiveTotal` beside it with nothing
|
|
93
|
+
* accounting for the gap — the undecomposable total this whole report exists
|
|
94
|
+
* to prevent.
|
|
95
|
+
*/
|
|
96
|
+
readonly notLoadedHere?: {
|
|
97
|
+
/** The per-machine file that did it — a superseder, or a settings file. */
|
|
98
|
+
readonly by: string;
|
|
99
|
+
readonly why: "superseded" | "excluded";
|
|
100
|
+
};
|
|
64
101
|
}
|
|
65
102
|
export interface InstructionWeight {
|
|
66
103
|
readonly unit: "chars" | "bytes";
|
|
@@ -68,19 +105,64 @@ export interface InstructionWeight {
|
|
|
68
105
|
readonly onExceed: "warns" | "truncates";
|
|
69
106
|
/** Heaviest first — a report's first line should name the biggest payer. */
|
|
70
107
|
readonly files: readonly WeighedFile[];
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
108
|
+
/**
|
|
109
|
+
* The SCORED number: everything loaded without a decision that a TEAMMATE or
|
|
110
|
+
* CI would also load. Per-machine files are excluded, which is what makes the
|
|
111
|
+
* figure reproducible from a commit alone.
|
|
112
|
+
*/
|
|
113
|
+
readonly committedTotal: number;
|
|
114
|
+
/**
|
|
115
|
+
* What THIS working copy actually loads. Never compared against the budget;
|
|
116
|
+
* printed beside it so the difference is visible rather than silently either
|
|
117
|
+
* counted or dropped.
|
|
118
|
+
*
|
|
119
|
+
* ⚠️ IT IS NOT "`committedTotal` PLUS THE PER-MACHINE FILES", and it used to
|
|
120
|
+
* say so. A per-machine file can also SUBTRACT: Claude Code stops reading
|
|
121
|
+
* `AGENTS.md` at all once a `CLAUDE.local.md` exists, so this number can come
|
|
122
|
+
* out BELOW `committedTotal`. See {@link WeighedFile.supersededLocallyBy}.
|
|
123
|
+
*/
|
|
124
|
+
readonly effectiveTotal: number;
|
|
125
|
+
/** `null` when {@link committedTotal} is within budget; else how far over. */
|
|
74
126
|
readonly overBy: number | null;
|
|
127
|
+
/**
|
|
128
|
+
* Files the chain NAMED and this run did not read — an import that does not
|
|
129
|
+
* exist, points outside the repo, or sits past the import depth. Printed, not
|
|
130
|
+
* dropped: a number missing a file it knows about would be the under-report
|
|
131
|
+
* this whole module exists to prevent.
|
|
132
|
+
*/
|
|
133
|
+
readonly unreadImports: readonly string[];
|
|
134
|
+
/**
|
|
135
|
+
* Globs and URLs the harness would expand at launch and vigiles will not walk
|
|
136
|
+
* (OpenCode `instructions`). Reported for the same reason.
|
|
137
|
+
*/
|
|
138
|
+
readonly unweighedPatterns: readonly string[];
|
|
139
|
+
/**
|
|
140
|
+
* A loaded file whose ENTIRE content is import tokens. Printed as a FINDING,
|
|
141
|
+
* not as a size: such a `CLAUDE.md` is fourteen bytes and the repository it
|
|
142
|
+
* describes loads tens of kilobytes, so the number on its own is a confident
|
|
143
|
+
* wrong answer. See `InstructionChain.redirects` for why the shape is common.
|
|
144
|
+
*/
|
|
145
|
+
readonly redirects: readonly {
|
|
146
|
+
readonly path: string;
|
|
147
|
+
readonly to: readonly string[];
|
|
148
|
+
}[];
|
|
75
149
|
}
|
|
76
150
|
/** Size in the harness's own unit. Bytes and chars differ on any non-ASCII text. */
|
|
77
151
|
export declare function sizeIn(text: string, unit: "chars" | "bytes"): number;
|
|
78
152
|
/**
|
|
79
|
-
* Weigh
|
|
153
|
+
* Weigh a harness's LOADED chain against its own budget.
|
|
154
|
+
*
|
|
155
|
+
* 🔴 IT TAKES A CHAIN, NOT A GLOB LIST, AND THAT IS THE WHOLE FIX. This used to
|
|
156
|
+
* filter the file map with `matchesGlob` over `budget.alwaysLoaded` — an adapter
|
|
157
|
+
* string the core interpreted — and so it counted files the harness does not
|
|
158
|
+
* load at launch (`paths:`-scoped rules, a sibling package's instruction file)
|
|
159
|
+
* and a file no teammate has (`CLAUDE.local.md`). Which files load is the
|
|
160
|
+
* harness's answer now (`PluginLayout.instructionChain`); this function only
|
|
161
|
+
* adds up what it was told and says what it could not weigh.
|
|
80
162
|
*
|
|
81
|
-
* Takes
|
|
82
|
-
* the
|
|
83
|
-
*
|
|
163
|
+
* Takes the map as well as the chain because a chain is a CLASSIFICATION, not
|
|
164
|
+
* a measurement: the sizes live in the bytes, and the same map serves the CLI
|
|
165
|
+
* and the browser engine.
|
|
84
166
|
*/
|
|
85
|
-
export declare function weighInstructions(files: Readonly<Record<string, string>>, budget: InstructionBudget): InstructionWeight;
|
|
167
|
+
export declare function weighInstructions(chain: InstructionChain, files: Readonly<Record<string, string>>, budget: InstructionBudget): InstructionWeight;
|
|
86
168
|
//# sourceMappingURL=instruction-weight.d.ts.map
|
|
@@ -44,43 +44,78 @@ function sizeIn(text, unit) {
|
|
|
44
44
|
return unit === "chars" ? text.length : Buffer.byteLength(text, "utf8");
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
* `alwaysLoaded` entries an ADAPTER writes, not user input — `CLAUDE.md`,
|
|
49
|
-
* `.claude/rules/**`. `*` stops at a separator, `**` crosses them.
|
|
50
|
-
*/
|
|
51
|
-
function matchesGlob(path, glob) {
|
|
52
|
-
const rx = glob
|
|
53
|
-
.split(/(\*\*\/|\*\*|\*)/)
|
|
54
|
-
.map((part) => part === "**/"
|
|
55
|
-
? "(?:.*/)?"
|
|
56
|
-
: part === "**"
|
|
57
|
-
? ".*"
|
|
58
|
-
: part === "*"
|
|
59
|
-
? "[^/]*"
|
|
60
|
-
: part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
61
|
-
.join("");
|
|
62
|
-
return new RegExp(`^${rx}$`).test(path);
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Weigh every unconditionally-loaded file in a file map.
|
|
47
|
+
* Weigh a harness's LOADED chain against its own budget.
|
|
66
48
|
*
|
|
67
|
-
*
|
|
68
|
-
* the
|
|
69
|
-
*
|
|
49
|
+
* 🔴 IT TAKES A CHAIN, NOT A GLOB LIST, AND THAT IS THE WHOLE FIX. This used to
|
|
50
|
+
* filter the file map with `matchesGlob` over `budget.alwaysLoaded` — an adapter
|
|
51
|
+
* string the core interpreted — and so it counted files the harness does not
|
|
52
|
+
* load at launch (`paths:`-scoped rules, a sibling package's instruction file)
|
|
53
|
+
* and a file no teammate has (`CLAUDE.local.md`). Which files load is the
|
|
54
|
+
* harness's answer now (`PluginLayout.instructionChain`); this function only
|
|
55
|
+
* adds up what it was told and says what it could not weigh.
|
|
56
|
+
*
|
|
57
|
+
* Takes the map as well as the chain because a chain is a CLASSIFICATION, not
|
|
58
|
+
* a measurement: the sizes live in the bytes, and the same map serves the CLI
|
|
59
|
+
* and the browser engine.
|
|
70
60
|
*/
|
|
71
|
-
function weighInstructions(files, budget) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
|
|
61
|
+
function weighInstructions(chain, files, budget) {
|
|
62
|
+
const notLoadedHere = chain.unloaded.flatMap((e) => {
|
|
63
|
+
if (e.scope !== "repo")
|
|
64
|
+
return [];
|
|
65
|
+
if (e.reason.kind === "superseded" && e.reason.byScope === "local") {
|
|
66
|
+
return [{ entry: e, by: e.reason.by, why: "superseded" }];
|
|
67
|
+
}
|
|
68
|
+
if (e.reason.kind === "excluded-by-settings" &&
|
|
69
|
+
e.reason.byScope === "local") {
|
|
70
|
+
return [{ entry: e, by: e.reason.by, why: "excluded" }];
|
|
71
|
+
}
|
|
72
|
+
return [];
|
|
73
|
+
});
|
|
74
|
+
const weighOne = (entry, notLoadedHereBy) => {
|
|
75
|
+
const text = files[entry.path];
|
|
76
|
+
return text === undefined
|
|
77
|
+
? []
|
|
78
|
+
: [
|
|
79
|
+
{
|
|
80
|
+
path: entry.path,
|
|
81
|
+
size: sizeIn(text, budget.unit),
|
|
82
|
+
role: entry.role,
|
|
83
|
+
scope: entry.scope,
|
|
84
|
+
...(entry.via === undefined ? {} : { via: entry.via }),
|
|
85
|
+
...(notLoadedHereBy === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: { notLoadedHere: notLoadedHereBy }),
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
};
|
|
91
|
+
const weighed = [
|
|
92
|
+
...chain.loaded.flatMap((e) => weighOne(e)),
|
|
93
|
+
...notLoadedHere.flatMap((s) => weighOne(s.entry, { by: s.by, why: s.why })),
|
|
94
|
+
].sort((a, b) => b.size - a.size || a.path.localeCompare(b.path));
|
|
95
|
+
const sum = (of) => of.reduce((total, f) => total + f.size, 0);
|
|
96
|
+
// COMMITTED is still "every `repo`-scoped file in the list", unchanged — the
|
|
97
|
+
// superseded entry is a committed file and joins the list with `scope:
|
|
98
|
+
// "repo"`, so the formula did not have to learn a second rule. EFFECTIVE is
|
|
99
|
+
// the one that changed: it was `sum(weighed)`, which was only ever right
|
|
100
|
+
// while the list held nothing this working copy fails to load.
|
|
101
|
+
const committedTotal = sum(weighed.filter((f) => f.scope === "repo"));
|
|
77
102
|
return {
|
|
78
103
|
unit: budget.unit,
|
|
79
104
|
limit: budget.limit,
|
|
80
105
|
onExceed: budget.onExceed,
|
|
81
106
|
files: weighed,
|
|
82
|
-
|
|
83
|
-
|
|
107
|
+
committedTotal,
|
|
108
|
+
effectiveTotal: sum(weighed.filter((f) => f.notLoadedHere === undefined)),
|
|
109
|
+
overBy: committedTotal > budget.limit ? committedTotal - budget.limit : null,
|
|
110
|
+
unreadImports: [
|
|
111
|
+
...new Set(chain.imports
|
|
112
|
+
.map((i) => i.path)
|
|
113
|
+
.filter((path) => files[path] === undefined)),
|
|
114
|
+
].sort(),
|
|
115
|
+
unweighedPatterns: [
|
|
116
|
+
...new Set(chain.patterns.map((p) => p.pattern)),
|
|
117
|
+
].sort(),
|
|
118
|
+
redirects: chain.redirects,
|
|
84
119
|
};
|
|
85
120
|
}
|
|
86
121
|
//# sourceMappingURL=instruction-weight.js.map
|