@vibe-agent-toolkit/cli 0.2.0-rc.1 → 0.2.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/dist/bin/vat +66 -15
  2. package/dist/bin/vat.js +66 -15
  3. package/dist/bin/vat.js.map +1 -1
  4. package/dist/bin.js +155 -36
  5. package/dist/bin.js.map +1 -1
  6. package/dist/command-loaders.d.ts +44 -0
  7. package/dist/command-loaders.d.ts.map +1 -0
  8. package/dist/command-loaders.js +58 -0
  9. package/dist/command-loaders.js.map +1 -0
  10. package/dist/commands/agent/index.js +1 -1
  11. package/dist/commands/agent/install.d.ts.map +1 -1
  12. package/dist/commands/agent/install.js +46 -5
  13. package/dist/commands/agent/install.js.map +1 -1
  14. package/dist/commands/audit/distributed-tree.d.ts +1 -1
  15. package/dist/commands/audit/distributed-tree.d.ts.map +1 -1
  16. package/dist/commands/audit/distributed-tree.js.map +1 -1
  17. package/dist/commands/audit/hierarchical-output.d.ts +1 -1
  18. package/dist/commands/audit/hierarchical-output.d.ts.map +1 -1
  19. package/dist/commands/audit/hierarchical-output.js +1 -1
  20. package/dist/commands/audit/hierarchical-output.js.map +1 -1
  21. package/dist/commands/audit.d.ts +22 -1
  22. package/dist/commands/audit.d.ts.map +1 -1
  23. package/dist/commands/audit.js +223 -24
  24. package/dist/commands/audit.js.map +1 -1
  25. package/dist/commands/build.d.ts +1 -1
  26. package/dist/commands/build.d.ts.map +1 -1
  27. package/dist/commands/build.js +2 -2
  28. package/dist/commands/build.js.map +1 -1
  29. package/dist/commands/cache/cache-control.d.ts +73 -0
  30. package/dist/commands/cache/cache-control.d.ts.map +1 -0
  31. package/dist/commands/cache/cache-control.js +77 -0
  32. package/dist/commands/cache/cache-control.js.map +1 -0
  33. package/dist/commands/cache/clear.d.ts +17 -3
  34. package/dist/commands/cache/clear.d.ts.map +1 -1
  35. package/dist/commands/cache/clear.js +68 -3
  36. package/dist/commands/cache/clear.js.map +1 -1
  37. package/dist/commands/cache/index.d.ts +13 -63
  38. package/dist/commands/cache/index.d.ts.map +1 -1
  39. package/dist/commands/cache/index.js +38 -79
  40. package/dist/commands/cache/index.js.map +1 -1
  41. package/dist/commands/claude/budget.d.ts +157 -0
  42. package/dist/commands/claude/budget.d.ts.map +1 -0
  43. package/dist/commands/claude/budget.js +412 -0
  44. package/dist/commands/claude/budget.js.map +1 -0
  45. package/dist/commands/claude/context.d.ts +211 -0
  46. package/dist/commands/claude/context.d.ts.map +1 -0
  47. package/dist/commands/claude/context.js +709 -0
  48. package/dist/commands/claude/context.js.map +1 -0
  49. package/dist/commands/claude/index.d.ts +13 -0
  50. package/dist/commands/claude/index.d.ts.map +1 -1
  51. package/dist/commands/claude/index.js +24 -2
  52. package/dist/commands/claude/index.js.map +1 -1
  53. package/dist/commands/claude/marketplace/git-publish.d.ts.map +1 -1
  54. package/dist/commands/claude/marketplace/git-publish.js +18 -20
  55. package/dist/commands/claude/marketplace/git-publish.js.map +1 -1
  56. package/dist/commands/claude/marketplace/validate.d.ts +1 -1
  57. package/dist/commands/claude/marketplace/validate.d.ts.map +1 -1
  58. package/dist/commands/claude/marketplace/validate.js +1 -1
  59. package/dist/commands/claude/marketplace/validate.js.map +1 -1
  60. package/dist/commands/claude/plugin/build.d.ts +16 -2
  61. package/dist/commands/claude/plugin/build.d.ts.map +1 -1
  62. package/dist/commands/claude/plugin/build.js +64 -11
  63. package/dist/commands/claude/plugin/build.js.map +1 -1
  64. package/dist/commands/claude/plugin/install.d.ts.map +1 -1
  65. package/dist/commands/claude/plugin/install.js +22 -2
  66. package/dist/commands/claude/plugin/install.js.map +1 -1
  67. package/dist/commands/claude/plugin/marketplace-json.d.ts +24 -9
  68. package/dist/commands/claude/plugin/marketplace-json.d.ts.map +1 -1
  69. package/dist/commands/claude/plugin/marketplace-json.js +10 -9
  70. package/dist/commands/claude/plugin/marketplace-json.js.map +1 -1
  71. package/dist/commands/consistency-check.d.ts.map +1 -1
  72. package/dist/commands/consistency-check.js +3 -3
  73. package/dist/commands/consistency-check.js.map +1 -1
  74. package/dist/commands/corpus/report.d.ts +1 -1
  75. package/dist/commands/corpus/report.d.ts.map +1 -1
  76. package/dist/commands/corpus/runner.js +1 -1
  77. package/dist/commands/corpus/runner.js.map +1 -1
  78. package/dist/commands/corpus/scan.d.ts.map +1 -1
  79. package/dist/commands/corpus/scan.js +17 -8
  80. package/dist/commands/corpus/scan.js.map +1 -1
  81. package/dist/commands/doctor.d.ts +36 -0
  82. package/dist/commands/doctor.d.ts.map +1 -1
  83. package/dist/commands/doctor.js +96 -0
  84. package/dist/commands/doctor.js.map +1 -1
  85. package/dist/commands/inventory.d.ts +13 -1
  86. package/dist/commands/inventory.d.ts.map +1 -1
  87. package/dist/commands/inventory.js +118 -13
  88. package/dist/commands/inventory.js.map +1 -1
  89. package/dist/commands/mcp/collections.d.ts +1 -1
  90. package/dist/commands/mcp/collections.d.ts.map +1 -1
  91. package/dist/commands/phase-utils.d.ts +1 -1
  92. package/dist/commands/phase-utils.d.ts.map +1 -1
  93. package/dist/commands/positional-args.js +1 -1
  94. package/dist/commands/positional-args.js.map +1 -1
  95. package/dist/commands/rag/index-command.d.ts +39 -0
  96. package/dist/commands/rag/index-command.d.ts.map +1 -1
  97. package/dist/commands/rag/index-command.js +28 -2
  98. package/dist/commands/rag/index-command.js.map +1 -1
  99. package/dist/commands/rag/index.d.ts.map +1 -1
  100. package/dist/commands/rag/index.js +21 -10
  101. package/dist/commands/rag/index.js.map +1 -1
  102. package/dist/commands/resources/index.d.ts.map +1 -1
  103. package/dist/commands/resources/index.js +10 -2
  104. package/dist/commands/resources/index.js.map +1 -1
  105. package/dist/commands/resources/scan.d.ts +24 -0
  106. package/dist/commands/resources/scan.d.ts.map +1 -1
  107. package/dist/commands/resources/scan.js +18 -5
  108. package/dist/commands/resources/scan.js.map +1 -1
  109. package/dist/commands/resources/validate.d.ts +1 -1
  110. package/dist/commands/resources/validate.d.ts.map +1 -1
  111. package/dist/commands/resources/validate.js +6 -3
  112. package/dist/commands/resources/validate.js.map +1 -1
  113. package/dist/commands/skill/review-checklist.d.ts.map +1 -1
  114. package/dist/commands/skill/review-checklist.js +1 -0
  115. package/dist/commands/skill/review-checklist.js.map +1 -1
  116. package/dist/commands/skill/review.js +2 -2
  117. package/dist/commands/skill/review.js.map +1 -1
  118. package/dist/commands/skill/test/configure.d.ts +19 -0
  119. package/dist/commands/skill/test/configure.d.ts.map +1 -1
  120. package/dist/commands/skill/test/configure.js +18 -3
  121. package/dist/commands/skill/test/configure.js.map +1 -1
  122. package/dist/commands/skill/test/run.d.ts +51 -4
  123. package/dist/commands/skill/test/run.d.ts.map +1 -1
  124. package/dist/commands/skill/test/run.js +112 -10
  125. package/dist/commands/skill/test/run.js.map +1 -1
  126. package/dist/commands/skills/build.d.ts +1 -1
  127. package/dist/commands/skills/build.d.ts.map +1 -1
  128. package/dist/commands/skills/build.js +72 -37
  129. package/dist/commands/skills/build.js.map +1 -1
  130. package/dist/commands/skills/package.d.ts +1 -1
  131. package/dist/commands/skills/package.d.ts.map +1 -1
  132. package/dist/commands/skills/package.js.map +1 -1
  133. package/dist/commands/skills/scope-guard.js +1 -1
  134. package/dist/commands/skills/scope-guard.js.map +1 -1
  135. package/dist/commands/skills/validate.d.ts +1 -1
  136. package/dist/commands/skills/validate.d.ts.map +1 -1
  137. package/dist/commands/skills/validate.js +37 -19
  138. package/dist/commands/skills/validate.js.map +1 -1
  139. package/dist/commands/validate.d.ts.map +1 -1
  140. package/dist/commands/validate.js +2 -1
  141. package/dist/commands/validate.js.map +1 -1
  142. package/dist/commands/verify.d.ts +2 -2
  143. package/dist/commands/verify.d.ts.map +1 -1
  144. package/dist/commands/verify.js +4 -4
  145. package/dist/commands/verify.js.map +1 -1
  146. package/dist/utils/command-error.d.ts +1 -1
  147. package/dist/utils/command-error.d.ts.map +1 -1
  148. package/dist/utils/command-error.js +1 -1
  149. package/dist/utils/command-error.js.map +1 -1
  150. package/dist/utils/config-loader.d.ts.map +1 -1
  151. package/dist/utils/config-loader.js +7 -1
  152. package/dist/utils/config-loader.js.map +1 -1
  153. package/dist/utils/context-budget-issues.d.ts +108 -0
  154. package/dist/utils/context-budget-issues.d.ts.map +1 -0
  155. package/dist/utils/context-budget-issues.js +280 -0
  156. package/dist/utils/context-budget-issues.js.map +1 -0
  157. package/dist/utils/corpus-target.d.ts +48 -0
  158. package/dist/utils/corpus-target.d.ts.map +1 -0
  159. package/dist/utils/corpus-target.js +61 -0
  160. package/dist/utils/corpus-target.js.map +1 -0
  161. package/dist/utils/issue-anchor.d.ts +2 -2
  162. package/dist/utils/issue-anchor.d.ts.map +1 -1
  163. package/dist/utils/issue-anchor.js +1 -1
  164. package/dist/utils/issue-rendering.d.ts +1 -1
  165. package/dist/utils/issue-rendering.d.ts.map +1 -1
  166. package/dist/utils/issue-rendering.js +1 -1
  167. package/dist/utils/issue-rendering.js.map +1 -1
  168. package/dist/utils/issue-severity.d.ts +1 -1
  169. package/dist/utils/issue-severity.d.ts.map +1 -1
  170. package/dist/utils/issue-severity.js +1 -1
  171. package/dist/utils/issue-severity.js.map +1 -1
  172. package/dist/utils/optional-backend.d.ts +77 -0
  173. package/dist/utils/optional-backend.d.ts.map +1 -0
  174. package/dist/utils/optional-backend.js +115 -0
  175. package/dist/utils/optional-backend.js.map +1 -0
  176. package/dist/utils/output.d.ts +17 -0
  177. package/dist/utils/output.d.ts.map +1 -1
  178. package/dist/utils/output.js +19 -0
  179. package/dist/utils/output.js.map +1 -1
  180. package/dist/utils/population-wiring.d.ts +59 -0
  181. package/dist/utils/population-wiring.d.ts.map +1 -0
  182. package/dist/utils/population-wiring.js +55 -0
  183. package/dist/utils/population-wiring.js.map +1 -0
  184. package/dist/utils/projection-store.d.ts +186 -0
  185. package/dist/utils/projection-store.d.ts.map +1 -0
  186. package/dist/utils/projection-store.js +255 -0
  187. package/dist/utils/projection-store.js.map +1 -0
  188. package/dist/utils/resource-loader.d.ts +127 -1
  189. package/dist/utils/resource-loader.d.ts.map +1 -1
  190. package/dist/utils/resource-loader.js +224 -7
  191. package/dist/utils/resource-loader.js.map +1 -1
  192. package/dist/utils/verdict-helpers.d.ts +1 -1
  193. package/dist/utils/verdict-helpers.d.ts.map +1 -1
  194. package/dist/utils/verdict-helpers.js +1 -1
  195. package/dist/utils/verdict-helpers.js.map +1 -1
  196. package/docs/agent.md +2 -2
  197. package/docs/mcp.md +2 -2
  198. package/docs/resources.md +8 -1
  199. package/package.json +15 -12
@@ -0,0 +1,709 @@
1
+ /**
2
+ * `vat claude context [path]` — what loads into an agent's context here, and
3
+ * what it costs.
4
+ *
5
+ * The first ANALYSIS verb under `vat claude`, which until now held only
6
+ * management verbs (plugins, marketplaces, org administration). It answers a
7
+ * question, changes nothing, and never gates: there is no threshold anywhere in
8
+ * this file, and the only non-zero exit is {@link handleCommandError}'s 2 for a
9
+ * system failure. A number that fails a build teaches people to stop reading it.
10
+ *
11
+ * ## Everything below is orchestration and formatting — deliberately
12
+ *
13
+ * Which files load, why, and what they cost are all decided in
14
+ * `@vibe-agent-toolkit/resources` (`whatLoadsAt` and `account`). This module
15
+ * populates, calls them, and renders. It computes nothing about context loading
16
+ * of its own, which is the CLI-stays-dumb rule applied to a lane where the
17
+ * temptation is strong: the totals are right there and a "just one more derived
18
+ * field" would put a second, unowned model of the vendor's behaviour in the
19
+ * package no other package can depend on.
20
+ *
21
+ * ## The limits ride WITH the run — ONCE — in every format
22
+ *
23
+ * Spec §11 puts the stated limits in the command's own output rather than in a
24
+ * doc, because a limit a reader has to go and find is a limit that never reaches
25
+ * the person acting on the number. So {@link CLAUDE_CONTEXT_LIMITS} and
26
+ * {@link CLAUDE_CONTEXT_MODELLED_BEHAVIOURS} are fields of the yaml/json
27
+ * ENVELOPE and a printed section of the text rendering, and
28
+ * {@link CLAUDE_CONTEXT_BOUNDS_STATEMENT} — *neither a floor nor a ceiling* —
29
+ * appears in all three. All three are imported, never restated: the sentence is
30
+ * domain content owned beside the list it frames, so every other consumer of the
31
+ * list reaches it too.
32
+ *
33
+ * ⛔ They belong to the ENVELOPE, never to an answer, and both renderings obey
34
+ * that identically — see {@link ContextEnvelope} for why. Attaching them per
35
+ * answer was not a formatting preference: on a `--all` sweep of this repository
36
+ * (6,224 answers) `--format json` measured 76,877,016 bytes and measures
37
+ * 6,581,468 with the block hoisted — 70.3 MB of ONE byte-identical paragraph,
38
+ * repeated. That is the JSON spelling of the very burial
39
+ * {@link renderEnvelopeText} refuses to do in text.
40
+ *
41
+ * ⛔ Nothing here may read as "complete". Many of the stated limits are signed
42
+ * `over-report`/`under-report` and apply whether or not `unknownTokenRows`,
43
+ * `skippedOversizeRows` and `prunedRows` are all zero, so zeroed counters are not
44
+ * a licence for the words "total cost", "full" or "all context".
45
+ *
46
+ * ⛔ A `null` token or byte count is an UNKNOWN SIZE and is never rendered as 0 —
47
+ * see {@link chargeText}. A confident zero is indistinguishable from a measured
48
+ * one, and the same reasoning is why an unrealized path answers `unknown` rather
49
+ * than an empty table.
50
+ */
51
+ import { account, buildClaudeContextPopulation, CLAUDE_CONTEXT_BOUNDS_STATEMENT, CLAUDE_CONTEXT_LIMITS, CLAUDE_CONTEXT_MODELLED_BEHAVIOURS, CLAUDE_MD_TAG, discoverableFrom, whatLoadsAt, } from '@vibe-agent-toolkit/resources';
52
+ import { findProjectRoot } from '@vibe-agent-toolkit/utils';
53
+ import { Command, Option } from 'commander';
54
+ import { handleCommandError } from '../../utils/command-error.js';
55
+ import { targetPathWithin } from '../../utils/corpus-target.js';
56
+ import { createLogger } from '../../utils/logger.js';
57
+ import { writeJsonOutput, writeStdoutSync, writeYamlOutput } from '../../utils/output.js';
58
+ import { populationWiring } from '../../utils/population-wiring.js';
59
+ import { withPopulationCache } from '../../utils/projection-store.js';
60
+ import { gitTrackerForProjectRoot } from '../audit/distributed-tree.js';
61
+ /** Soft wrap width for the printed limit statements. */
62
+ const WRAP_COLUMNS = 96;
63
+ /** How this command names itself in a refusal. */
64
+ const COMMAND_NAME = 'vat claude context';
65
+ /**
66
+ * Create the `vat claude context [path]` command.
67
+ *
68
+ * @returns The configured Commander command
69
+ */
70
+ export function createContextCommand() {
71
+ const command = new Command('context');
72
+ command
73
+ .description('Report what Claude Code loads into context at a path, why, and what it costs')
74
+ .argument('[paths...]', 'Files or directories to answer for (default: the current directory)')
75
+ .addOption(new Option('--format <format>', 'Output format: text (default), yaml, or json').choices([
76
+ 'text',
77
+ 'yaml',
78
+ 'json',
79
+ ]).default('text'))
80
+ .option('--all', 'Answer for every path the projection realized, instead of named paths')
81
+ .option('--discoverable', 'Also report what the loaded files LINK TO in one hop and the harness does not load')
82
+ .option('--debug', 'Verbose logging to stderr')
83
+ .action(claudeContextCommand)
84
+ .addHelpText('after', `
85
+ Description:
86
+ Report which CLAUDE.md files, .claude/rules files and @-imported files load
87
+ into an agent's context at a path, why each one is there, and what it costs.
88
+ A FILE argument is exact. A directory argument classifies each path-scoped
89
+ rule as ∀ (its glob covers every file there, so it is a second CLAUDE.md for
90
+ that directory) or ∃ (some file there matches, and the answer names it); a
91
+ rule no file there can match is left out rather than charged.
92
+
93
+ Several paths may be named at once. The tree is enumerated ONCE and every
94
+ path answered from that one population, so asking about ten paths together
95
+ costs what asking about one costs — not ten times it. No paths means the
96
+ current directory; --all sweeps every path the projection realized.
97
+
98
+ --discoverable adds a second, DISJOINT set: what the loaded files LINK to in
99
+ one hop and the harness does not load. A markdown link is voluntary — nothing
100
+ loads it — so its tokens are reported separately and never added to the
101
+ estimate. A target already in the loaded set is excluded, so the two sets
102
+ partition and can be read alone or added.
103
+
104
+ Output:
105
+ - root: the corpus root that was enumerated
106
+ - answers: one document per requested path, in the order requested — always
107
+ a list, even for a single path, so consumers never branch on count
108
+ - totals: always/on-demand token estimates, plus counts of rows whose size
109
+ is unknown, skipped by the 4 MiB cliff, or pruned behind one
110
+ - rows: one per resource, with every predicate that admitted it
111
+ - limits: what these answers deliberately do not settle, in both directions.
112
+ On the ENVELOPE beside root, never on an answer — they bound the
113
+ method rather than any one path, so they are stated exactly once
114
+ however many paths were asked about
115
+
116
+ A path the projection never realized answers kind: unknown — never zero.
117
+ Diagnostics and blob-stage refusals go to stderr; stdout is the document.
118
+
119
+ Exit Codes:
120
+ 0 - An answer was produced (there is no threshold and no gate)
121
+ 1 - Invalid usage (unknown option, or an unsupported --format value)
122
+ 2 - System error (a path outside the corpus root, unreadable tree)
123
+
124
+ Example:
125
+ $ vat claude context src/index.ts docs/ README.md # one scan, three answers
126
+ `);
127
+ return command;
128
+ }
129
+ /**
130
+ * Action handler for `vat claude context [path]`.
131
+ *
132
+ * @param pathArg - The path to answer for, or undefined for the current directory
133
+ * @param options - The command's flags
134
+ */
135
+ export async function claudeContextCommand(pathArgs, options) {
136
+ const logger = createLogger(options.debug === true ? { debug: true } : {});
137
+ const startTime = Date.now();
138
+ const sweep = options.all === true;
139
+ try {
140
+ const root = findProjectRoot(process.cwd()) ?? process.cwd();
141
+ // 🔑 Every argument is resolved BEFORE the population, not inside the map.
142
+ // A path outside the corpus is a usage error, and finding it afterwards
143
+ // would charge the caller a full population — minutes on a cold cache — to
144
+ // be told they mistyped. `--all` has no arguments to check.
145
+ const requested = sweep ? [] : targetsWithin(root, pathArgs);
146
+ const projection = await populateContext(root, logger);
147
+ const targets = sweep ? everyRealizedPath(projection) : requested;
148
+ const format = options.format ?? 'text';
149
+ // ONE population, N queries. `whatLoadsAt` is a pure read of materialised
150
+ // tables, so the marginal cost of another path is a map lookup — which is
151
+ // the whole reason this command takes a list rather than being run twice.
152
+ const answers = targets.map((target) => documentFor(target, projection, root, options.discoverable === true));
153
+ emit(contextEnvelope(root, answers), renderEnvelopeText(answers), format);
154
+ // ⛔ Always 0. There is no threshold in this command and there is not going
155
+ // to be one — a number that fails a build is a number people learn to stop
156
+ // reading. The explicit exit matches every other leaf here and guarantees the
157
+ // process ends rather than waiting on whatever the population left behind.
158
+ process.exit(0);
159
+ }
160
+ catch (error) {
161
+ handleCommandError(error, logger, startTime, 'claude context');
162
+ }
163
+ }
164
+ /**
165
+ * The answer document for one query.
166
+ *
167
+ * Split from the handler so the handler stays a straight line: the accounting
168
+ * call needs the `claude-md` identity set, and threading that through the
169
+ * handler body put three statements between the query and its document.
170
+ *
171
+ * Returns the document alone — rendering moved to {@link renderEnvelopeText},
172
+ * which needs every document at once to print the limits exactly once, and the
173
+ * limits themselves are attached by {@link contextEnvelope}, never here.
174
+ *
175
+ * ⛔ Exported so it can be TESTED. What has to be pinned is an ABSENCE — that no
176
+ * limit field is on the returned document — and an absence is only observable by
177
+ * calling the function that would have added it.
178
+ *
179
+ * @param answer - The query's answer
180
+ * @param projection - The populated projection, for the `claude-md` tag rows
181
+ * @param discoverable - Whether to compute the one-hop reachable set
182
+ * @returns The answer document
183
+ */
184
+ export function answerDocument(answer, projection, discoverable) {
185
+ // The cliff and root discovery read ONE vocabulary: these are the ids the
186
+ // shipped `classifyPath` tagged, not a second basename rule invented here.
187
+ const claudeMdIds = new Set(projection.resourceTags
188
+ .filter((tag) => tag.tag === CLAUDE_MD_TAG)
189
+ .map((tag) => tag.resourceId));
190
+ const accounted = account(answer, claudeMdIds);
191
+ // 🔑 `conditions`, `overBudgetRules` and `unattributedImports` live on the
192
+ // ORIGINAL answer — `AccountedContext` is `{rows, totals}` and nothing else.
193
+ // Reading them off the accounting result silently drops every warning.
194
+ const document = {
195
+ kind: 'answer',
196
+ input: answer.input,
197
+ directory: answer.directory,
198
+ file: answer.file,
199
+ totals: accounted.totals,
200
+ rows: accounted.rows,
201
+ conditions: answer.conditions,
202
+ overBudgetRules: answer.overBudgetRules,
203
+ unattributedImports: answer.unattributedImports,
204
+ // The ANSWER is passed, not recomputed: it is the authority for what is
205
+ // already loaded, and the two lenses partitioning depends on both reading
206
+ // one loaded set.
207
+ discoverable: discoverable ? discoverableFrom(projection, answer) : null,
208
+ };
209
+ return document;
210
+ }
211
+ /**
212
+ * Wrap the run's documents in the envelope, with the limits attached ONCE.
213
+ *
214
+ * The one place {@link CLAUDE_CONTEXT_LIMITS} and its two companions enter the
215
+ * machine-readable output. Keeping that a single assignment is what makes the
216
+ * "stated once" property structural rather than a habit: there is no per-answer
217
+ * site left where a copy could be reintroduced.
218
+ *
219
+ * ⛔ Exported so it can be TESTED alongside {@link answerDocument} — the
220
+ * regression this guards (an answer growing its own copy of the block) is a
221
+ * property of the two functions TOGETHER, and a test that can reach only one of
222
+ * them cannot see it.
223
+ *
224
+ * @param root - The corpus root that was enumerated
225
+ * @param answers - One document per requested path, in the order requested
226
+ * @returns The envelope, ready to serialize
227
+ */
228
+ export function contextEnvelope(root, answers) {
229
+ return {
230
+ root,
231
+ answers,
232
+ boundsStatement: CLAUDE_CONTEXT_BOUNDS_STATEMENT,
233
+ limits: CLAUDE_CONTEXT_LIMITS,
234
+ modelledBehaviours: CLAUDE_CONTEXT_MODELLED_BEHAVIOURS,
235
+ };
236
+ }
237
+ /**
238
+ * Resolve every requested path, defaulting to the current directory.
239
+ *
240
+ * No arguments means the current directory — NOT the whole corpus. `--all` is
241
+ * the sweep, spelled explicitly, because a bare `vat claude context` answering
242
+ * for thousands of paths would turn the friendliest invocation into the most
243
+ * expensive one.
244
+ *
245
+ * @param root - The discovered project root
246
+ * @param pathArgs - The path arguments, possibly empty
247
+ * @returns One root-relative target per argument, in argument order
248
+ */
249
+ function targetsWithin(root, pathArgs) {
250
+ if (pathArgs.length === 0)
251
+ return [targetPathWithin(root, undefined, COMMAND_NAME)];
252
+ return pathArgs.map((pathArg) => targetPathWithin(root, pathArg, COMMAND_NAME));
253
+ }
254
+ /**
255
+ * Every path the projection realized, sorted, for `--all`.
256
+ *
257
+ * Deduplicated because one identity may be realized at several paths and the
258
+ * question this command answers is asked of a PATH — two realizations of one
259
+ * identity are two legitimate questions, but the same path twice is not.
260
+ *
261
+ * ⚠️ Sorted by code point rather than left in table order: the table's order is
262
+ * an artefact of population, so a caller diffing two sweeps would see spurious
263
+ * churn. Sorting makes the sweep comparable across runs.
264
+ *
265
+ * @param projection - The populated projection
266
+ * @returns Root-relative paths, deduplicated and sorted
267
+ */
268
+ function everyRealizedPath(projection) {
269
+ return [...new Set(projection.resourceRealizations.map((row) => row.path))].sort(comparePaths);
270
+ }
271
+ /**
272
+ * Order two paths by code point.
273
+ *
274
+ * ⛔ Not `localeCompare`: this ordering is consumed by machines diffing two
275
+ * sweeps, and a locale-sensitive comparator makes the same corpus sort
276
+ * differently on two developers' machines — a diff that reports churn nobody
277
+ * caused.
278
+ *
279
+ * @param left - The first path
280
+ * @param right - The second path
281
+ * @returns Negative, zero or positive, per `Array#sort`
282
+ */
283
+ function comparePaths(left, right) {
284
+ if (left < right)
285
+ return -1;
286
+ return left > right ? 1 : 0;
287
+ }
288
+ /**
289
+ * The document for one target, answer or non-answer.
290
+ *
291
+ * @param target - The root-relative path to answer for
292
+ * @param projection - The populated projection
293
+ * @param root - The corpus root, for the non-answer's explanation
294
+ * @param discoverable - Whether to compute the one-hop reachable set
295
+ * @returns The answer document, or the structurally-different unknown document
296
+ */
297
+ function documentFor(target, projection, root, discoverable) {
298
+ const answer = whatLoadsAt(projection, target);
299
+ if (answer.kind === 'unknown')
300
+ return unknownDocumentFor(answer.input, root);
301
+ return answerDocument(answer, projection, discoverable);
302
+ }
303
+ /**
304
+ * Render every answer for a person, with the limits stated exactly once.
305
+ *
306
+ * ⛔ The limits are printed ONCE, after the last answer, rather than per answer.
307
+ * They are properties of the MEASUREMENT METHOD, not of any one path, so
308
+ * repeating them per path would read as though each answer carried its own
309
+ * caveats — and on a `--all` sweep would bury the answers under thousands of
310
+ * identical paragraphs. A sweep with no answered path prints no limits at all,
311
+ * for the same reason {@link unknownDocumentFor} omits them: nothing was
312
+ * measured, so there is no measurement to bound.
313
+ *
314
+ * @param documents - The answers, in requested order
315
+ * @returns The text rendering, newline-terminated
316
+ */
317
+ function renderEnvelopeText(documents) {
318
+ const bodies = documents.map((document) => document.kind === 'unknown' ? renderUnknownText(document) : renderAnswerText(document));
319
+ const measured = documents.some((document) => document.kind === 'answer');
320
+ if (!measured)
321
+ return bodies.join('\n');
322
+ return `${bodies.join('\n')}${limitSection().join('\n')}\n`;
323
+ }
324
+ /**
325
+ * Populate the Claude-context lane for one tree.
326
+ *
327
+ * @param root - The absolute corpus root
328
+ * @param logger - Where blob-stage refusals go. **stderr**, never stdout: this
329
+ * command's stdout is the document a caller parses, and a diagnostic in the
330
+ * middle of it breaks every consumer
331
+ * @returns The populated projection
332
+ */
333
+ async function populateContext(root, logger) {
334
+ return withPopulationCache({ root }, async (cache) => {
335
+ const gitTracker = await gitTrackerForProjectRoot(root);
336
+ return buildClaudeContextPopulation({ root, ...populationWiring(logger, gitTracker, cache) });
337
+ });
338
+ }
339
+ /**
340
+ * Write the document in the requested format.
341
+ *
342
+ * @param document - The yaml/json payload
343
+ * @param text - The text rendering of the same answer
344
+ * @param format - The selected format
345
+ */
346
+ function emit(document, text, format) {
347
+ if (format === 'json') {
348
+ writeJsonOutput(document);
349
+ return;
350
+ }
351
+ if (format === 'yaml') {
352
+ writeYamlOutput(document);
353
+ return;
354
+ }
355
+ writeStdoutSync(text);
356
+ }
357
+ /**
358
+ * The non-answer, spelled so it cannot be mistaken for an empty answer.
359
+ *
360
+ * ⛔ **A non-answer publishes no measurement, in every format.** No `totals`, no
361
+ * `rows`: nothing was measured here, so anything readable as a figure would
362
+ * assert that a measurement happened, which is the exact confusion `kind:
363
+ * 'unknown'` exists to prevent. The limits are one level up on the ENVELOPE and
364
+ * bound the run's method, not this path — a sweep whose every path was
365
+ * unrealized still measured nothing, and the `explanation` carries the only
366
+ * caveat that applies to a non-answer, which is that there is no answer.
367
+ *
368
+ * @param input - The path that was asked about
369
+ * @param root - The corpus root that was enumerated
370
+ * @returns The unknown document
371
+ */
372
+ function unknownDocumentFor(input, root) {
373
+ return {
374
+ kind: 'unknown',
375
+ input,
376
+ reason: 'path-not-realized',
377
+ explanation: `${displayPath(input)} is not a path this projection realized under ${root}, so VAT did not`
378
+ + ' look at it. This is NOT an answer of "nothing loads here" — no context was measured at'
379
+ + ' all. Check the path exists and is not excluded from enumeration.',
380
+ };
381
+ }
382
+ /**
383
+ * Render the non-answer for a person.
384
+ *
385
+ * @param document - The unknown document
386
+ * @returns The text rendering, newline-terminated
387
+ */
388
+ function renderUnknownText(document) {
389
+ return [
390
+ `NO ANSWER for ${displayPath(document.input)} — reason: ${document.reason}`,
391
+ '',
392
+ ...wrapStatement(document.explanation, ' '),
393
+ '',
394
+ ].join('\n');
395
+ }
396
+ /**
397
+ * Render the answer for a person.
398
+ *
399
+ * @param document - The answer document
400
+ * @returns The text rendering, newline-terminated
401
+ */
402
+ function renderAnswerText(document) {
403
+ const lines = [
404
+ ...headingLines(document),
405
+ ...estimateLines(document.totals),
406
+ ...rowSection('Loaded at launch (always)', document.rows, 'always'),
407
+ ...rowSection('Loaded on demand', document.rows, 'on-demand'),
408
+ ...listSection('Conditions', document.conditions.map(conditionLine)),
409
+ ...listSection('Rules whose paths: list exceeded the vendor pattern budget', document.overBudgetRules),
410
+ ...listSection('Imported files no importer could be attributed to', document.unattributedImports),
411
+ ...discoverySection(document.discoverable),
412
+ ];
413
+ return `${lines.join('\n')}\n`;
414
+ }
415
+ /**
416
+ * What the loaded files point at and the harness does not load.
417
+ *
418
+ * ⛔ Printed under its own heading with its own total, never merged into the
419
+ * token estimate. Nothing loads a markdown link, so this is an upper bound on a
420
+ * VOLUNTARY cost — what following every link once would add — and adding it to a
421
+ * context-budget figure would charge a session for documents it may never open.
422
+ * The heading says "not loaded" for a reader who sees only this section.
423
+ *
424
+ * A `null` argument means the lens was not asked for and prints NOTHING. An
425
+ * empty `rows` prints the heading with a zero, because "this file links nowhere"
426
+ * is a real and useful answer that must not be confused with "nobody looked".
427
+ *
428
+ * @param discoverable - The lens's answer, or null when `--discoverable` was off
429
+ * @returns The section's lines, or none when the lens did not run
430
+ */
431
+ function discoverySection(discoverable) {
432
+ if (discoverable === null)
433
+ return [];
434
+ const { rows, totals } = discoverable;
435
+ const lines = [
436
+ 'Discoverable in one hop — LINKED from the loaded files, NOT loaded',
437
+ ` ${totals.discoverableTokens} tokens if every link were followed once (a ceiling, never a charge)`,
438
+ ` ${totals.unknownTokenRows} size unknown · ${totals.unrealizedRows} not in this tree`
439
+ + ` · ${totals.outsideRootRows} outside the root`,
440
+ ];
441
+ for (const row of rows) {
442
+ lines.push(` ${displayPath(row.path)} — ${discoverableCost(row)}`);
443
+ for (const citation of row.citedBy) {
444
+ const text = citation.text === null ? '' : ` as "${citation.text}"`;
445
+ lines.push(` linked from ${displayPath(citation.fromPath)}:${citation.line}${text}`);
446
+ }
447
+ }
448
+ lines.push('');
449
+ return lines;
450
+ }
451
+ /**
452
+ * What one discoverable row would cost, or why VAT cannot say.
453
+ *
454
+ * ⛔ Never `0` for an unmeasured row, the same rule {@link chargeText} enforces
455
+ * one section up. `unrealized` says what it is — an absence in THIS projection —
456
+ * rather than "broken": adjudicating a link is `vat resources validate`'s lane.
457
+ *
458
+ * @param row - The discoverable row
459
+ * @returns The cost phrase
460
+ */
461
+ function discoverableCost(row) {
462
+ if (row.reach === 'outside-root')
463
+ return 'outside the corpus root, so its size is unknowable here';
464
+ if (row.reach === 'unrealized')
465
+ return 'not realized in this projection, so nothing is known of it';
466
+ if (row.tokens === null)
467
+ return 'size unknown: no measured blob, so it is counted, not summed';
468
+ return `${row.tokens} tokens if opened`;
469
+ }
470
+ /**
471
+ * The heading, which states up front whether the query was exact.
472
+ *
473
+ * @param document - The answer document
474
+ * @returns The heading lines, blank-terminated
475
+ */
476
+ function headingLines(document) {
477
+ const exactness = document.file === null
478
+ ? 'DIRECTORY query — a path-scoped rule is reported as ∀ (covers every file here)'
479
+ + ' or ∃ (some file here matches); only a FILE query is exact'
480
+ : 'FILE query — path-scoped rules are matched exactly';
481
+ return [`Claude Code context at ${displayPath(document.input)}`, ` ${exactness}`, ''];
482
+ }
483
+ /**
484
+ * The token estimate and the three counters that keep it from reading as settled.
485
+ *
486
+ * @param totals - The accounting totals
487
+ * @returns The section's lines, blank-terminated
488
+ */
489
+ function estimateLines(totals) {
490
+ return [
491
+ 'Token estimate',
492
+ ` always-loaded ${totals.alwaysTokens} tokens`,
493
+ ` on-demand ${totals.onDemandTokens} tokens`,
494
+ ` size unknown ${totals.unknownTokenRows} rows (counted, never summed as zero)`,
495
+ ` skipped over 4 MiB ${totals.skippedOversizeRows} rows`,
496
+ ` pruned behind a skip ${totals.prunedRows} rows`,
497
+ '',
498
+ ];
499
+ }
500
+ /**
501
+ * One load class's rows, each with every predicate that admitted it.
502
+ *
503
+ * @param title - The section heading
504
+ * @param rows - Every accounted row
505
+ * @param loadClass - The class this section reports
506
+ * @returns The section's lines, or none when the class is empty
507
+ */
508
+ function rowSection(title, rows, loadClass) {
509
+ const selected = rows.filter((row) => row.loadClass === loadClass);
510
+ if (selected.length === 0)
511
+ return [];
512
+ const lines = [title];
513
+ for (const row of selected) {
514
+ lines.push(` ${row.path} — ${chargeText(row)}`);
515
+ for (const admission of row.admissions) {
516
+ lines.push(` ${describeAdmission(admission)}`);
517
+ }
518
+ }
519
+ lines.push('');
520
+ return lines;
521
+ }
522
+ /**
523
+ * What one row costs, or why it costs nothing.
524
+ *
525
+ * ⛔ A null token count renders as "size unknown", never as `0`. The `charged`
526
+ * branch cannot be reached with a null — `chargeOf` classifies those
527
+ * `unknown-size` first — but the check is written as a value test rather than a
528
+ * `?? 0`, so if that ever changed the output would say what it knows instead of
529
+ * asserting a measurement it does not have.
530
+ *
531
+ * **Exported solely so that rule can be pinned by a test that would FAIL if a
532
+ * `?? 0` were introduced.** The property lives in the RENDERER and nowhere else:
533
+ * an assertion over the json rows only re-states what `account` already
534
+ * guarantees, and on any real tree every row carries a blob, so such an
535
+ * assertion compares `false === false` and passes without ever reaching the code
536
+ * that could print a zero. It is not part of this module's command surface and
537
+ * nothing outside its own test may call it.
538
+ *
539
+ * @param row - The accounted row
540
+ * @returns The cost phrase
541
+ */
542
+ export function chargeText(row) {
543
+ if (row.charge === 'oversize-skipped') {
544
+ return 'skipped: past the 4 MiB CLAUDE.md cliff, so none of it loads';
545
+ }
546
+ if (row.charge === 'pruned-by-oversize') {
547
+ return 'not reached: every import route into it passes through a skipped file';
548
+ }
549
+ if (row.tokens === null)
550
+ return 'size unknown: no measured blob, so it is counted, not summed';
551
+ return `${row.tokens} tokens`;
552
+ }
553
+ /**
554
+ * One admission, in prose.
555
+ *
556
+ * @param admission - Why the row is in the answer
557
+ * @returns The prose form
558
+ */
559
+ function describeAdmission(admission) {
560
+ switch (admission.kind) {
561
+ case 'ancestry':
562
+ return `CLAUDE.md chain, from ${displayPath(admission.dir)}`;
563
+ case 'root-rule':
564
+ return 'root-scope rules file';
565
+ case 'nested-rule':
566
+ return `nested rules file, under ${displayPath(admission.under)}`;
567
+ case 'glob-rule':
568
+ return `rule matched ${admission.pattern}`;
569
+ case 'glob-rule-covers-dir':
570
+ return `∀ rule covers EVERY file here via ${admission.pattern}`
571
+ + ' — a second CLAUDE.md for this directory in all but name';
572
+ case 'glob-rule-may-fire':
573
+ return `∃ rule may fire here — ${admission.pattern} matches ${displayPath(admission.examplePath)}`
574
+ + ', among possibly others; ask about a FILE for an exact answer';
575
+ // Spelled out rather than left to a `default`, so that adding a seventh
576
+ // admission kind upstream is a lint error here instead of an import
577
+ // description silently applied to something that is not an import.
578
+ case 'import':
579
+ return describeImport(admission);
580
+ }
581
+ }
582
+ /**
583
+ * One import admission, in prose.
584
+ *
585
+ * An unattributed member says so rather than naming a plausible importer: the
586
+ * query reports `viaPath: null` precisely when it could not attribute a parent,
587
+ * and inventing one here would launder that into a fact.
588
+ *
589
+ * @param admission - The import admission
590
+ * @returns The prose form
591
+ */
592
+ function describeImport(admission) {
593
+ if (admission.viaPath === null || admission.depth === null) {
594
+ return `@-imported into the closure rooted at ${admission.rootPath}, importer unattributed`;
595
+ }
596
+ return `imported by ${admission.viaPath} at depth ${admission.depth}`
597
+ + ` (closure rooted at ${admission.rootPath})`;
598
+ }
599
+ /**
600
+ * One condition, in the compact `severity code at location: message` shape.
601
+ *
602
+ * @param condition - The graded condition
603
+ * @returns The line
604
+ */
605
+ function conditionLine(condition) {
606
+ const reference = condition.sourceRef === null ? '' : ` [${condition.sourceRef}]`;
607
+ return ` ${condition.severity.padEnd(5)} ${condition.code}`
608
+ + ` at ${conditionLocation(condition)}${reference}: ${condition.message}`;
609
+ }
610
+ /**
611
+ * Where a condition points, using the most specific provenance it carries.
612
+ *
613
+ * @param condition - The graded condition
614
+ * @returns A path, or a `path:line`
615
+ */
616
+ function conditionLocation(condition) {
617
+ if (condition.sourcePath === null)
618
+ return condition.path;
619
+ if (condition.sourceLine === null)
620
+ return condition.sourcePath;
621
+ return `${condition.sourcePath}:${condition.sourceLine}`;
622
+ }
623
+ /**
624
+ * A heading plus its lines, or nothing at all when there are none.
625
+ *
626
+ * @param title - The section heading
627
+ * @param entries - The already-formatted lines, or bare strings to indent
628
+ * @returns The section's lines, blank-terminated, or none
629
+ */
630
+ function listSection(title, entries) {
631
+ if (entries.length === 0)
632
+ return [];
633
+ return [title, ...entries.map((entry) => (entry.startsWith(' ') ? entry : ` ${entry}`)), ''];
634
+ }
635
+ /**
636
+ * The limits section — the one part of this rendering that is never omitted.
637
+ *
638
+ * Printed even when there is nothing else to say, because the limits are a
639
+ * property of the METHOD rather than of this particular tree: an answer with no
640
+ * conditions and no unknown rows is subject to exactly the same signed
641
+ * over/under-report bounds as a messy one.
642
+ *
643
+ * ⛔ Reads the imported constants, NOT an answer document — the same source
644
+ * {@link contextEnvelope} reads. It took no argument's worth of per-answer data
645
+ * even when the fields were on the answer, and threading one through was what
646
+ * made "the limits belong to an answer" look true.
647
+ *
648
+ * @returns The section's lines
649
+ */
650
+ function limitSection() {
651
+ const lines = [
652
+ 'What this answer does not settle',
653
+ ...wrapStatement(CLAUDE_CONTEXT_BOUNDS_STATEMENT, ' '),
654
+ '',
655
+ ];
656
+ for (const limit of CLAUDE_CONTEXT_LIMITS) {
657
+ lines.push(` ${limit.direction}: ${limit.id}`);
658
+ lines.push(...wrapStatement(limit.statement, ' '));
659
+ }
660
+ lines.push('', 'Modelled Claude Code behaviours (vendor versions, each cited)');
661
+ for (const behaviour of CLAUDE_CONTEXT_MODELLED_BEHAVIOURS) {
662
+ lines.push(` ${behaviour.behaviour}`);
663
+ lines.push(` ${behaviour.introducedIn} — ${behaviour.citedFrom}`);
664
+ }
665
+ return lines;
666
+ }
667
+ /**
668
+ * Soft-wrap a statement to {@link WRAP_COLUMNS}, under a fixed indent.
669
+ *
670
+ * The limit statements are two and three sentences long and are the part of this
671
+ * output people actually have to read; unwrapped they arrive as one 400-column
672
+ * line that a terminal breaks mid-word.
673
+ *
674
+ * @param text - The statement
675
+ * @param indent - Leading whitespace for every produced line
676
+ * @returns One or more indented lines
677
+ */
678
+ function wrapStatement(text, indent) {
679
+ const width = Math.max(WRAP_COLUMNS - indent.length, 20);
680
+ const lines = [];
681
+ let pending = [];
682
+ let used = 0;
683
+ for (const word of text.split(' ')) {
684
+ if (pending.length > 0 && used + pending.length + word.length > width) {
685
+ lines.push(indent + pending.join(' '));
686
+ pending = [];
687
+ used = 0;
688
+ }
689
+ pending.push(word);
690
+ used += word.length;
691
+ }
692
+ if (pending.length > 0)
693
+ lines.push(indent + pending.join(' '));
694
+ return lines;
695
+ }
696
+ /**
697
+ * A root-relative path as a reader should see it.
698
+ *
699
+ * The corpus root itself is `''`, which renders as nothing at all and reads like
700
+ * a bug. Only the empty case is special-cased — every other path is printed
701
+ * exactly as the projection holds it, so a reader can paste it straight back in.
702
+ *
703
+ * @param path - The root-relative path
704
+ * @returns The display form
705
+ */
706
+ function displayPath(path) {
707
+ return path === '' ? '<corpus root>' : path;
708
+ }
709
+ //# sourceMappingURL=context.js.map