@vibe-agent-toolkit/cli 0.2.0-rc.3 → 0.2.0-rc.4
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/bin/vat +2 -1
- package/dist/bin/vat.js +2 -1
- package/dist/bin/vat.js.map +1 -1
- package/dist/bin.js +2 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/audit/distributed-tree.d.ts +1 -1
- package/dist/commands/audit/distributed-tree.d.ts.map +1 -1
- package/dist/commands/audit/distributed-tree.js +2 -1
- package/dist/commands/audit/distributed-tree.js.map +1 -1
- package/dist/commands/audit/git-url-clone.d.ts +1 -1
- package/dist/commands/audit/git-url-clone.d.ts.map +1 -1
- package/dist/commands/audit/git-url-clone.js.map +1 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +2 -1
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +14 -11
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/cache/cache-control.d.ts +16 -7
- package/dist/commands/cache/cache-control.d.ts.map +1 -1
- package/dist/commands/cache/cache-control.js +17 -8
- package/dist/commands/cache/cache-control.js.map +1 -1
- package/dist/commands/claude/budget.js +1 -1
- package/dist/commands/claude/budget.js.map +1 -1
- package/dist/commands/claude/context.d.ts +138 -9
- package/dist/commands/claude/context.d.ts.map +1 -1
- package/dist/commands/claude/context.js +450 -80
- package/dist/commands/claude/context.js.map +1 -1
- package/dist/commands/claude/marketplace/git-publish.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/git-publish.js +2 -1
- package/dist/commands/claude/marketplace/git-publish.js.map +1 -1
- package/dist/commands/claude/marketplace/validate.d.ts +18 -0
- package/dist/commands/claude/marketplace/validate.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/validate.js +40 -20
- package/dist/commands/claude/marketplace/validate.js.map +1 -1
- package/dist/commands/claude/plugin/build.d.ts +17 -1
- package/dist/commands/claude/plugin/build.d.ts.map +1 -1
- package/dist/commands/claude/plugin/build.js +45 -16
- package/dist/commands/claude/plugin/build.js.map +1 -1
- package/dist/commands/claude/plugin/helpers.d.ts.map +1 -1
- package/dist/commands/claude/plugin/helpers.js +2 -1
- package/dist/commands/claude/plugin/helpers.js.map +1 -1
- package/dist/commands/claude/plugin/install.d.ts.map +1 -1
- package/dist/commands/claude/plugin/install.js +2 -1
- package/dist/commands/claude/plugin/install.js.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.d.ts.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.js +2 -1
- package/dist/commands/claude/plugin/tree-copy.js.map +1 -1
- package/dist/commands/consistency-check.d.ts.map +1 -1
- package/dist/commands/consistency-check.js +2 -1
- package/dist/commands/consistency-check.js.map +1 -1
- package/dist/commands/corpus/runner.d.ts.map +1 -1
- package/dist/commands/corpus/runner.js +2 -1
- package/dist/commands/corpus/runner.js.map +1 -1
- package/dist/commands/corpus/scan.d.ts.map +1 -1
- package/dist/commands/corpus/scan.js +2 -1
- package/dist/commands/corpus/scan.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/inventory.js +1 -1
- package/dist/commands/inventory.js.map +1 -1
- package/dist/commands/phase-utils.d.ts +154 -103
- package/dist/commands/phase-utils.d.ts.map +1 -1
- package/dist/commands/phase-utils.js +109 -155
- package/dist/commands/phase-utils.js.map +1 -1
- package/dist/commands/resources/scan.d.ts.map +1 -1
- package/dist/commands/resources/scan.js +1 -1
- package/dist/commands/resources/scan.js.map +1 -1
- package/dist/commands/resources/validate.d.ts +11 -0
- package/dist/commands/resources/validate.d.ts.map +1 -1
- package/dist/commands/resources/validate.js +96 -48
- package/dist/commands/resources/validate.js.map +1 -1
- package/dist/commands/skill/test/run.d.ts.map +1 -1
- package/dist/commands/skill/test/run.js +11 -2
- package/dist/commands/skill/test/run.js.map +1 -1
- package/dist/commands/skills/build.d.ts +10 -0
- package/dist/commands/skills/build.d.ts.map +1 -1
- package/dist/commands/skills/build.js +83 -25
- package/dist/commands/skills/build.js.map +1 -1
- package/dist/commands/skills/skill-discovery.d.ts.map +1 -1
- package/dist/commands/skills/skill-discovery.js +2 -1
- package/dist/commands/skills/skill-discovery.js.map +1 -1
- package/dist/commands/skills/validate.d.ts +78 -1
- package/dist/commands/skills/validate.d.ts.map +1 -1
- package/dist/commands/skills/validate.js +67 -32
- package/dist/commands/skills/validate.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +26 -16
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/verify.d.ts +9 -8
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +46 -35
- package/dist/commands/verify.js.map +1 -1
- package/dist/utils/command-error.d.ts +32 -1
- package/dist/utils/command-error.d.ts.map +1 -1
- package/dist/utils/command-error.js +33 -9
- package/dist/utils/command-error.js.map +1 -1
- package/dist/utils/population-wiring.d.ts +47 -6
- package/dist/utils/population-wiring.d.ts.map +1 -1
- package/dist/utils/population-wiring.js +45 -2
- package/dist/utils/population-wiring.js.map +1 -1
- package/dist/utils/projection-store.d.ts +6 -4
- package/dist/utils/projection-store.d.ts.map +1 -1
- package/dist/utils/projection-store.js +7 -5
- package/dist/utils/projection-store.js.map +1 -1
- package/dist/utils/resource-loader.d.ts +1 -1
- package/dist/utils/resource-loader.d.ts.map +1 -1
- package/dist/utils/resource-loader.js +11 -1
- package/dist/utils/resource-loader.js.map +1 -1
- package/package.json +13 -13
- package/dist/commands/resources/command-helpers.d.ts +0 -5
- package/dist/commands/resources/command-helpers.d.ts.map +0 -1
- package/dist/commands/resources/command-helpers.js +0 -6
- package/dist/commands/resources/command-helpers.js.map +0 -1
|
@@ -30,13 +30,24 @@
|
|
|
30
30
|
* domain content owned beside the list it frames, so every other consumer of the
|
|
31
31
|
* list reaches it too.
|
|
32
32
|
*
|
|
33
|
-
* ⛔ They belong to the ENVELOPE, never to an answer, and
|
|
34
|
-
* that identically — see {@link ContextEnvelope} for why.
|
|
35
|
-
* answer was not a formatting preference: on
|
|
36
|
-
* (6,224 answers) `--format json`
|
|
37
|
-
* 6,581,468 with the block hoisted — 70.3
|
|
38
|
-
* repeated. That is the JSON spelling of the
|
|
39
|
-
* {@link renderEnvelopeText} refuses to do in text
|
|
33
|
+
* ⛔ They belong to the ENVELOPE, never to an answer or a region, and every
|
|
34
|
+
* rendering obeys that identically — see {@link ContextEnvelope} for why.
|
|
35
|
+
* Attaching them per answer was not a formatting preference: on the whole-tree
|
|
36
|
+
* sweep `--all` used to be (6,224 answers on this repository) `--format json`
|
|
37
|
+
* measured 76,877,016 bytes and measures 6,581,468 with the block hoisted — 70.3
|
|
38
|
+
* MB of ONE byte-identical paragraph, repeated. That is the JSON spelling of the
|
|
39
|
+
* very burial {@link renderEnvelopeText} refuses to do in text, and it is why
|
|
40
|
+
* {@link ContextCostMapEnvelope} hoists the same block rather than growing one
|
|
41
|
+
* per region.
|
|
42
|
+
*
|
|
43
|
+
* ## `--all` is a MAP, not a sweep
|
|
44
|
+
*
|
|
45
|
+
* ⛔ `--all` emits no per-path documents in any format. It used to answer for
|
|
46
|
+
* every realized path — 10,438 documents and 205,918 lines on a large adopter
|
|
47
|
+
* monorepo, ~491 s — which is not a report anyone reads. It now emits the
|
|
48
|
+
* whole-tree cost map {@link buildContextCostMap} builds, which answers the
|
|
49
|
+
* question the flag exists for: where in this tree is it expensive to work.
|
|
50
|
+
* Naming paths is the drill-down, and is the only way to get a per-path answer.
|
|
40
51
|
*
|
|
41
52
|
* ⛔ Nothing here may read as "complete". Many of the stated limits are signed
|
|
42
53
|
* `over-report`/`under-report` and apply whether or not `unknownTokenRows`,
|
|
@@ -48,7 +59,7 @@
|
|
|
48
59
|
* one, and the same reasoning is why an unrealized path answers `unknown` rather
|
|
49
60
|
* than an empty table.
|
|
50
61
|
*/
|
|
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';
|
|
62
|
+
import { account, buildClaudeContextPopulation, buildContextCostMap, CLAUDE_CONTEXT_BOUNDS_STATEMENT, CLAUDE_CONTEXT_LIMITS, CLAUDE_CONTEXT_MODELLED_BEHAVIOURS, CLAUDE_MD_TAG, discoverableFrom, whatLoadsAt, } from '@vibe-agent-toolkit/resources';
|
|
52
63
|
import { findProjectRoot } from '@vibe-agent-toolkit/utils';
|
|
53
64
|
import { Command, Option } from 'commander';
|
|
54
65
|
import { handleCommandError } from '../../utils/command-error.js';
|
|
@@ -62,6 +73,47 @@ import { gitTrackerForProjectRoot } from '../audit/distributed-tree.js';
|
|
|
62
73
|
const WRAP_COLUMNS = 96;
|
|
63
74
|
/** How this command names itself in a refusal. */
|
|
64
75
|
const COMMAND_NAME = 'vat claude context';
|
|
76
|
+
/**
|
|
77
|
+
* How many rows each RANKED section of the `--all` cost map prints.
|
|
78
|
+
*
|
|
79
|
+
* ⛔ A cap exists because the previous shape of `--all` had none: it answered for
|
|
80
|
+
* every realized path, which on a large adopter monorepo was 10,438 documents and
|
|
81
|
+
* 205,918 lines — an output nobody, human or agent, can act on. The map collapses
|
|
82
|
+
* that to two ranked lists, and a ranked list is only useful if its head is
|
|
83
|
+
* readable in one screen. 20 is that head: enough that a real hot spot is
|
|
84
|
+
* somewhere in it, few enough that the whole report stays scannable.
|
|
85
|
+
*
|
|
86
|
+
* ⛔ It bounds the TEXT rendering ONLY. `--format json` carries the map whole —
|
|
87
|
+
* see {@link ContextCostMapEnvelope} — because a program asked for the map, not
|
|
88
|
+
* for a summary of it, and a silently-truncated array is a defect a consumer
|
|
89
|
+
* cannot see.
|
|
90
|
+
*
|
|
91
|
+
* ⛔ Whenever it fires, {@link omittedNotice} says how many rows were left out. A
|
|
92
|
+
* silent cap reads as "this is everything", which is the one thing this report
|
|
93
|
+
* must never say. Exported so a test pins the notice's arithmetic against the
|
|
94
|
+
* same number the renderer slices at, rather than against a literal that is free
|
|
95
|
+
* to disagree with it.
|
|
96
|
+
*/
|
|
97
|
+
export const COST_MAP_ROW_LIMIT = 20;
|
|
98
|
+
/**
|
|
99
|
+
* What the two halves of the cost map mean, and which one is exact.
|
|
100
|
+
*
|
|
101
|
+
* Printed under the heading because the numbers are unreadable without it: a
|
|
102
|
+
* reader who takes the per-region figure for an average, or borrows a
|
|
103
|
+
* region-mate's on-demand figure, has read the report backwards. This paraphrases
|
|
104
|
+
* the contracts `RegionCost.alwaysTokens` and `DirectoryCost.onDemandTokens`
|
|
105
|
+
* state in `@vibe-agent-toolkit/resources` — it asserts nothing this module
|
|
106
|
+
* decided, and computes nothing.
|
|
107
|
+
*/
|
|
108
|
+
const COST_MAP_METHOD_STATEMENT = 'Directories are ranked by what it costs to work in them: the at-launch floor PLUS what fires'
|
|
109
|
+
+ ' on demand there, because a cheap rule under a heavy instruction chain is still an expensive'
|
|
110
|
+
+ ' place to work.'
|
|
111
|
+
+ ' Every working location in a region loads the same files at launch, so the at-launch figure is'
|
|
112
|
+
+ ' EXACT for the whole region rather than an average over it. On-demand cost is reported per'
|
|
113
|
+
+ ' DIRECTORY instead, because a path-scoped rule is admitted where some file under that'
|
|
114
|
+
+ " directory matches its globs and nowhere else — a region-mate's on-demand figure is never a"
|
|
115
|
+
+ " substitute for a directory's own. Only a FILE query is exact for path-scoped rules: name a"
|
|
116
|
+
+ ' path (vat claude context src/index.ts) to get one.';
|
|
65
117
|
/**
|
|
66
118
|
* Create the `vat claude context [path]` command.
|
|
67
119
|
*
|
|
@@ -77,7 +129,8 @@ export function createContextCommand() {
|
|
|
77
129
|
'yaml',
|
|
78
130
|
'json',
|
|
79
131
|
]).default('text'))
|
|
80
|
-
.option('--all', '
|
|
132
|
+
.option('--all', 'Report the whole-tree cost map — launch cost per region, total cost per directory —'
|
|
133
|
+
+ ' instead of answering for named paths')
|
|
81
134
|
.option('--discoverable', 'Also report what the loaded files LINK TO in one hop and the harness does not load')
|
|
82
135
|
.option('--debug', 'Verbose logging to stderr')
|
|
83
136
|
.action(claudeContextCommand)
|
|
@@ -93,25 +146,37 @@ Description:
|
|
|
93
146
|
Several paths may be named at once. The tree is enumerated ONCE and every
|
|
94
147
|
path answered from that one population, so asking about ten paths together
|
|
95
148
|
costs what asking about one costs — not ten times it. No paths means the
|
|
96
|
-
current directory
|
|
149
|
+
current directory.
|
|
97
150
|
|
|
98
|
-
--
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
151
|
+
--all answers a different question: where in this tree is it expensive to
|
|
152
|
+
work. It reports launch cost once per REGION (every directory inheriting one
|
|
153
|
+
CLAUDE.md chain — exact for all of them, not an average) and, per DIRECTORY,
|
|
154
|
+
the TOTAL of that launch floor and what fires on demand there (a path-scoped
|
|
155
|
+
rule fires for some directories and not others, so the on-demand half is
|
|
156
|
+
never borrowed from a region-mate). Directories are ranked by that total. It
|
|
157
|
+
emits no per-path documents; name paths for those. --discoverable applies to
|
|
158
|
+
named paths only.
|
|
103
159
|
|
|
104
160
|
Output:
|
|
161
|
+
- kind: context-answers for named paths, context-cost-map for --all —
|
|
162
|
+
the two are different documents, so never guess which you hold
|
|
105
163
|
- root: the corpus root that was enumerated
|
|
106
|
-
- answers: one document per requested path, in the order
|
|
107
|
-
a list, even for
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
164
|
+
- answers: (context-answers) one document per requested path, in the order
|
|
165
|
+
requested — always a list, even for one path, so consumers never
|
|
166
|
+
branch on count; with totals, rows, and every admitting predicate
|
|
167
|
+
- costMap: (context-cost-map) regions worst-first by launch cost, directories
|
|
168
|
+
worst-first by total cost (launch + on demand), the tree-level
|
|
169
|
+
roll-up of rows nothing could be measured for, plus the counts of
|
|
170
|
+
locations evaluated, queried, and left out for want of an answer
|
|
171
|
+
- limits: what this run deliberately does not settle, in both directions.
|
|
172
|
+
On the ENVELOPE beside root, never on an answer or a region — they
|
|
173
|
+
bound the method rather than any one path, so they are stated
|
|
174
|
+
exactly once however much was measured
|
|
175
|
+
|
|
176
|
+
Counts of rows whose size is unknown, skipped by the 4 MiB cliff, or pruned
|
|
177
|
+
behind one are reported beside every estimate and NEVER summed into it. The
|
|
178
|
+
text rendering prints the top ${COST_MAP_ROW_LIMIT} of each ranked list and says how many it
|
|
179
|
+
left out; --format json carries the map whole.
|
|
115
180
|
|
|
116
181
|
A path the projection never realized answers kind: unknown — never zero.
|
|
117
182
|
Diagnostics and blob-stage refusals go to stderr; stdout is the document.
|
|
@@ -129,7 +194,13 @@ Example:
|
|
|
129
194
|
/**
|
|
130
195
|
* Action handler for `vat claude context [path]`.
|
|
131
196
|
*
|
|
132
|
-
*
|
|
197
|
+
* ⛔ The two branches produce DIFFERENT documents and there is no third mode: a
|
|
198
|
+
* `--per-path` escape hatch back to the old sweep is exactly the backward
|
|
199
|
+
* compatibility shim this project forbids pre-1.0, and naming paths explicitly
|
|
200
|
+
* already gives the per-path answer — now from a population the caller pays for
|
|
201
|
+
* once instead of ten thousand times.
|
|
202
|
+
*
|
|
203
|
+
* @param pathArgs - The paths to answer for; empty means the current directory
|
|
133
204
|
* @param options - The command's flags
|
|
134
205
|
*/
|
|
135
206
|
export async function claudeContextCommand(pathArgs, options) {
|
|
@@ -144,13 +215,20 @@ export async function claudeContextCommand(pathArgs, options) {
|
|
|
144
215
|
// be told they mistyped. `--all` has no arguments to check.
|
|
145
216
|
const requested = sweep ? [] : targetsWithin(root, pathArgs);
|
|
146
217
|
const projection = await populateContext(root, logger);
|
|
147
|
-
const targets = sweep ? everyRealizedPath(projection) : requested;
|
|
148
218
|
const format = options.format ?? 'text';
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
219
|
+
if (sweep) {
|
|
220
|
+
// Every number in the map is decided in `@vibe-agent-toolkit/resources`.
|
|
221
|
+
// This branch calls one function and formats what it returns.
|
|
222
|
+
const map = buildContextCostMap(projection);
|
|
223
|
+
emit(costMapEnvelope(root, map), renderCostMapText(map), format);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// ONE population, N queries. `whatLoadsAt` is a pure read of materialised
|
|
227
|
+
// tables, so the marginal cost of another path is a map lookup — which is
|
|
228
|
+
// the whole reason this command takes a list rather than being run twice.
|
|
229
|
+
const answers = requested.map((target) => documentFor(target, projection, root, options.discoverable === true));
|
|
230
|
+
emit(contextEnvelope(root, answers), renderEnvelopeText(answers), format);
|
|
231
|
+
}
|
|
154
232
|
// ⛔ Always 0. There is no threshold in this command and there is not going
|
|
155
233
|
// to be one — a number that fails a build is a number people learn to stop
|
|
156
234
|
// reading. The explicit exit matches every other leaf here and guarantees the
|
|
@@ -227,6 +305,7 @@ export function answerDocument(answer, projection, discoverable) {
|
|
|
227
305
|
*/
|
|
228
306
|
export function contextEnvelope(root, answers) {
|
|
229
307
|
return {
|
|
308
|
+
kind: 'context-answers',
|
|
230
309
|
root,
|
|
231
310
|
answers,
|
|
232
311
|
boundsStatement: CLAUDE_CONTEXT_BOUNDS_STATEMENT,
|
|
@@ -234,6 +313,32 @@ export function contextEnvelope(root, answers) {
|
|
|
234
313
|
modelledBehaviours: CLAUDE_CONTEXT_MODELLED_BEHAVIOURS,
|
|
235
314
|
};
|
|
236
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Wrap the `--all` cost map in its envelope, with the limits attached ONCE.
|
|
318
|
+
*
|
|
319
|
+
* The mirror of {@link contextEnvelope}, and deliberately its own function rather
|
|
320
|
+
* than a generic over both: the two envelopes differ in more than one field, and
|
|
321
|
+
* a shared builder taking a payload would make `kind` a parameter — which is how
|
|
322
|
+
* a discriminator stops discriminating.
|
|
323
|
+
*
|
|
324
|
+
* ⛔ Exported so a test can pin, over a many-region map, that the limit block
|
|
325
|
+
* appears exactly ONCE in the serialized document. Presence passes on a copy per
|
|
326
|
+
* region, which is the 70.3 MB defect this command already shipped once.
|
|
327
|
+
*
|
|
328
|
+
* @param root - The corpus root that was enumerated
|
|
329
|
+
* @param costMap - The whole-tree cost map, carried entire
|
|
330
|
+
* @returns The envelope, ready to serialize
|
|
331
|
+
*/
|
|
332
|
+
export function costMapEnvelope(root, costMap) {
|
|
333
|
+
return {
|
|
334
|
+
kind: 'context-cost-map',
|
|
335
|
+
root,
|
|
336
|
+
costMap,
|
|
337
|
+
boundsStatement: CLAUDE_CONTEXT_BOUNDS_STATEMENT,
|
|
338
|
+
limits: CLAUDE_CONTEXT_LIMITS,
|
|
339
|
+
modelledBehaviours: CLAUDE_CONTEXT_MODELLED_BEHAVIOURS,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
237
342
|
/**
|
|
238
343
|
* Resolve every requested path, defaulting to the current directory.
|
|
239
344
|
*
|
|
@@ -251,40 +356,6 @@ function targetsWithin(root, pathArgs) {
|
|
|
251
356
|
return [targetPathWithin(root, undefined, COMMAND_NAME)];
|
|
252
357
|
return pathArgs.map((pathArg) => targetPathWithin(root, pathArg, COMMAND_NAME));
|
|
253
358
|
}
|
|
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
359
|
/**
|
|
289
360
|
* The document for one target, answer or non-answer.
|
|
290
361
|
*
|
|
@@ -306,10 +377,10 @@ function documentFor(target, projection, root, discoverable) {
|
|
|
306
377
|
* ⛔ The limits are printed ONCE, after the last answer, rather than per answer.
|
|
307
378
|
* They are properties of the MEASUREMENT METHOD, not of any one path, so
|
|
308
379
|
* repeating them per path would read as though each answer carried its own
|
|
309
|
-
* caveats — and
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
380
|
+
* caveats — and across a many-path run would bury the answers under identical
|
|
381
|
+
* paragraphs. A run with no answered path prints no limits at all, for the same
|
|
382
|
+
* reason {@link unknownDocumentFor} omits them: nothing was measured, so there is
|
|
383
|
+
* no measurement to bound.
|
|
313
384
|
*
|
|
314
385
|
* @param documents - The answers, in requested order
|
|
315
386
|
* @returns The text rendering, newline-terminated
|
|
@@ -321,6 +392,297 @@ function renderEnvelopeText(documents) {
|
|
|
321
392
|
return bodies.join('\n');
|
|
322
393
|
return `${bodies.join('\n')}${limitSection().join('\n')}\n`;
|
|
323
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Render the `--all` cost map for a person: where in this tree it is expensive
|
|
397
|
+
* to work.
|
|
398
|
+
*
|
|
399
|
+
* Four sections, in the order a reader needs them — the region table (what each
|
|
400
|
+
* part of the tree costs at launch), the regions' own files (what they are paying
|
|
401
|
+
* for), the directories that cost the most to work in overall, and what the map
|
|
402
|
+
* looked at — then the limits, ONCE.
|
|
403
|
+
*
|
|
404
|
+
* ⛔ Every ranked section is capped at {@link COST_MAP_ROW_LIMIT} and every cap
|
|
405
|
+
* that fires announces itself. ⛔ Nothing here sums an unknown into a total or
|
|
406
|
+
* calls any figure complete: the counted-not-summed rows are printed beside each
|
|
407
|
+
* estimate, and the stated limits apply whether or not those counters are zero.
|
|
408
|
+
*
|
|
409
|
+
* ⛔ Exported so the properties above can be TESTED with inputs a real tree never
|
|
410
|
+
* produces — a null token count, more directories than the cap. On this repository
|
|
411
|
+
* every file has a measured blob, so an assertion driven by a real run compares
|
|
412
|
+
* `false === false` and would keep passing against a `?? 0`.
|
|
413
|
+
*
|
|
414
|
+
* @param map - The whole-tree cost map from `buildContextCostMap`
|
|
415
|
+
* @returns The text rendering, newline-terminated
|
|
416
|
+
*/
|
|
417
|
+
export function renderCostMapText(map) {
|
|
418
|
+
const lines = [
|
|
419
|
+
'Context cost by region — what it costs to work in each part of this tree',
|
|
420
|
+
'',
|
|
421
|
+
...wrapStatement(COST_MAP_METHOD_STATEMENT, ' '),
|
|
422
|
+
'',
|
|
423
|
+
...regionTable(map.regions),
|
|
424
|
+
...map.regions.slice(0, COST_MAP_ROW_LIMIT).flatMap(regionDetail),
|
|
425
|
+
...directoryTable(map.directories),
|
|
426
|
+
...coverageLines(map),
|
|
427
|
+
];
|
|
428
|
+
return `${lines.join('\n')}\n${limitSection().join('\n')}\n`;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The ranked region table: what a session pays at launch in each part of the tree.
|
|
432
|
+
*
|
|
433
|
+
* @param regions - The regions, already worst-first by launch cost
|
|
434
|
+
* @returns The section's lines, blank-terminated
|
|
435
|
+
*/
|
|
436
|
+
function regionTable(regions) {
|
|
437
|
+
const shown = regions.slice(0, COST_MAP_ROW_LIMIT);
|
|
438
|
+
const lines = [' at launch locations region'];
|
|
439
|
+
for (const region of shown) {
|
|
440
|
+
lines.push(` ${padCount(region.alwaysTokens, 9)} ${padCount(region.locationCount, 9)}`
|
|
441
|
+
+ ` ${displayPath(region.representative)}`);
|
|
442
|
+
}
|
|
443
|
+
lines.push(...omittedNotice(shown.length, regions.length, 'region', 'regions'), '');
|
|
444
|
+
return lines;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* One region's launch-time bill, itemised.
|
|
448
|
+
*
|
|
449
|
+
* 🔑 The rows are rendered by {@link rowSection}, the same function the per-path
|
|
450
|
+
* answer uses, so a file reads identically in both reports and there is exactly
|
|
451
|
+
* one admission-describer. Its `loadClass` filter is a no-op here — `alwaysRows`
|
|
452
|
+
* is already that class — and passing it anyway keeps the call honest rather than
|
|
453
|
+
* relying on the caller's guarantee.
|
|
454
|
+
*
|
|
455
|
+
* @param region - The region to itemise
|
|
456
|
+
* @returns The section's lines, blank-terminated
|
|
457
|
+
*/
|
|
458
|
+
function regionDetail(region) {
|
|
459
|
+
return [
|
|
460
|
+
`Region ${displayPath(region.representative)} — ${groupDigits(region.alwaysTokens)} tokens`
|
|
461
|
+
+ ` at launch, ${countOf(region.locationCount, 'location', 'locations')}`,
|
|
462
|
+
...quietCounterLines(region),
|
|
463
|
+
...nestBlock(rowSection('Loaded at launch', region.alwaysRows, 'always')),
|
|
464
|
+
'',
|
|
465
|
+
];
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* A region's three counters, printed only when one of them has something to say.
|
|
469
|
+
*
|
|
470
|
+
* ⛔ **This suppression is safe ONLY because the tree-level roll-up in
|
|
471
|
+
* {@link coverageLines} prints UNCONDITIONALLY, zeros included.** The honesty
|
|
472
|
+
* rule in this lane is that a reader must never be able to mistake "nothing here
|
|
473
|
+
* was unmeasurable" for "nobody counted", and after this suppression the roll-up
|
|
474
|
+
* is the single thing in the report carrying that. Silence a region's zeros only
|
|
475
|
+
* while a tree-level zero is still printed somewhere; drop the roll-up and this
|
|
476
|
+
* function has to go back to unconditional.
|
|
477
|
+
*
|
|
478
|
+
* ⛔ Deliberately NOT applied to {@link estimateLines}. A single-path answer has
|
|
479
|
+
* no roll-up to fall back on, so its counters are the only statement that the
|
|
480
|
+
* rows were counted at all and must keep printing whatever their values.
|
|
481
|
+
*
|
|
482
|
+
* The measured motivation: nine regions on this repository printed 27 lines of
|
|
483
|
+
* pure zeros, burying the launch bills the section exists to show.
|
|
484
|
+
*
|
|
485
|
+
* @param region - The region whose counters to print
|
|
486
|
+
* @returns The three lines, or none when all three are zero
|
|
487
|
+
*/
|
|
488
|
+
function quietCounterLines(region) {
|
|
489
|
+
const anything = region.unknownTokenRows + region.skippedOversizeRows + region.prunedRows;
|
|
490
|
+
if (anything === 0)
|
|
491
|
+
return [];
|
|
492
|
+
// Whole block or nothing: a reader comparing two regions must be comparing the
|
|
493
|
+
// same three lines, not one region's selected non-zeros.
|
|
494
|
+
return counterLines(region);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Indent a borrowed section one level, and drop the blank line it terminates with.
|
|
498
|
+
*
|
|
499
|
+
* {@link rowSection} is written for the per-path answer, where it sits at the top
|
|
500
|
+
* level and separates itself from what follows. Nested under a region heading it
|
|
501
|
+
* needs a level of indent, and its terminator would double the region's own — so
|
|
502
|
+
* this drops every blank rather than the last one specifically, which is exact
|
|
503
|
+
* because that section emits no interior blanks.
|
|
504
|
+
*
|
|
505
|
+
* @param lines - The borrowed section's lines
|
|
506
|
+
* @returns The same lines, indented, with blanks removed
|
|
507
|
+
*/
|
|
508
|
+
function nestBlock(lines) {
|
|
509
|
+
return lines.filter((line) => line !== '').map((line) => ` ${line}`);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* The ranked directory table: where in this tree it is most expensive to work.
|
|
513
|
+
*
|
|
514
|
+
* ⛔ Ordered and headed by TOTAL cost — the launch floor plus the on-demand
|
|
515
|
+
* burden — which is the key `buildContextCostMap` sorts by. Ranking on a key the
|
|
516
|
+
* producer did not sort on would make the column descend non-monotonically and
|
|
517
|
+
* read as a bug; ranking on the on-demand half alone would answer "where does a
|
|
518
|
+
* rule fire", which is not the question the map is for.
|
|
519
|
+
*
|
|
520
|
+
* ⛔ Every one of the three figures is READ. The CLI does not add
|
|
521
|
+
* `alwaysTokens` to `onDemandTokens` here — that sum is `DirectoryCost.
|
|
522
|
+
* totalTokens`, computed where both halves are known to be charged-only totals.
|
|
523
|
+
* A second sum in this module would be a second, unowned model of what "cost"
|
|
524
|
+
* means, free to disagree with the ranking beside it. Both halves stay in the
|
|
525
|
+
* table because they are acted on differently: the floor by moving instructions,
|
|
526
|
+
* the burden by scoping rules.
|
|
527
|
+
*
|
|
528
|
+
* @param directories - The directories, already worst-first by total cost
|
|
529
|
+
* @returns The section's lines, blank-terminated, or none when there are none
|
|
530
|
+
*/
|
|
531
|
+
function directoryTable(directories) {
|
|
532
|
+
if (directories.length === 0)
|
|
533
|
+
return [];
|
|
534
|
+
const shown = directories.slice(0, COST_MAP_ROW_LIMIT);
|
|
535
|
+
const lines = [
|
|
536
|
+
'Most expensive directories to work in — at launch PLUS what fires on demand there',
|
|
537
|
+
' total at launch on demand directory',
|
|
538
|
+
];
|
|
539
|
+
for (const directory of shown) {
|
|
540
|
+
lines.push(` ${padCount(directory.totalTokens, 9)} ${padCount(directory.alwaysTokens, 9)}`
|
|
541
|
+
+ ` ${padCount(directory.onDemandTokens, 9)} ${displayPath(directory.directory)}`
|
|
542
|
+
// ⛔ The unknown-row suffix appears only when there is one, for the same
|
|
543
|
+
// reason and under the same condition as {@link quietCounterLines}: the
|
|
544
|
+
// tree-level roll-up in {@link coverageLines} states unconditionally that
|
|
545
|
+
// these rows were counted, so twenty repetitions of "0 rows of unknown
|
|
546
|
+
// size" buy nothing and crowd out the paths the table is ranking. Remove
|
|
547
|
+
// the roll-up and this suffix has to become unconditional again.
|
|
548
|
+
+ unknownRowSuffix(directory.unknownTokenRows));
|
|
549
|
+
}
|
|
550
|
+
lines.push(...omittedNotice(shown.length, directories.length, 'directory', 'directories'), '');
|
|
551
|
+
return lines;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The note that a directory's on-demand cost is an under-report, when it is.
|
|
555
|
+
*
|
|
556
|
+
* @param unknownTokenRows - On-demand rows at that directory whose size is unknown
|
|
557
|
+
* @returns The suffix, or nothing at all when every row was measured
|
|
558
|
+
*/
|
|
559
|
+
function unknownRowSuffix(unknownTokenRows) {
|
|
560
|
+
if (unknownTokenRows === 0)
|
|
561
|
+
return '';
|
|
562
|
+
return ` · ${countOf(unknownTokenRows, 'row', 'rows')} of unknown size, counted not summed`;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* What the map looked at, including the locations it could not answer for.
|
|
566
|
+
*
|
|
567
|
+
* ⛔ `skippedUnknownLocations` is PRINTED, not folded away. A location left out
|
|
568
|
+
* because a query it needed answered `unknown` is a hole in the map, and a report
|
|
569
|
+
* that quietly dropped it would be indistinguishable from one where every
|
|
570
|
+
* location answered.
|
|
571
|
+
*
|
|
572
|
+
* ⛔ The tree-level roll-up prints UNCONDITIONALLY, all-zero included, and that
|
|
573
|
+
* is load-bearing: {@link quietCounterLines} suppresses the per-region zeros, so
|
|
574
|
+
* this is the only place left saying the rows were counted at all. A zero here is
|
|
575
|
+
* a measurement; an absent line would be indistinguishable from nobody looking.
|
|
576
|
+
*
|
|
577
|
+
* @param map - The cost map
|
|
578
|
+
* @returns The section's lines, blank-terminated
|
|
579
|
+
*/
|
|
580
|
+
function coverageLines(map) {
|
|
581
|
+
return [
|
|
582
|
+
'What this map looked at',
|
|
583
|
+
` working locations evaluated ${groupDigits(map.evaluatedDirectories)}`,
|
|
584
|
+
` queries issued ${groupDigits(map.queriedDirectories)}`,
|
|
585
|
+
` no answer of their own ${countOf(map.skippedUnknownLocations, 'location', 'locations')}`
|
|
586
|
+
+ ' (left out of the table above, never counted as zero)',
|
|
587
|
+
' rows this map could not measure, over every region and directory above:',
|
|
588
|
+
...counterLines(map.unmeasuredRows),
|
|
589
|
+
'',
|
|
590
|
+
];
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Say how many ranked rows were left unprinted, or say nothing when none were.
|
|
594
|
+
*
|
|
595
|
+
* ⛔ The COUNT is the point. "Some rows were omitted" leaves a reader unable to
|
|
596
|
+
* tell a table that dropped three rows from one that dropped three thousand, and
|
|
597
|
+
* a cap with no notice at all reads as "this is everything" — which is the claim
|
|
598
|
+
* this whole report is built not to make.
|
|
599
|
+
*
|
|
600
|
+
* @param shown - How many rows were printed
|
|
601
|
+
* @param total - How many there were
|
|
602
|
+
* @param singular - What one omitted row is, for the `total - shown === 1` case
|
|
603
|
+
* @param plural - What several of them are
|
|
604
|
+
* @returns The notice line, or none when nothing was truncated
|
|
605
|
+
*/
|
|
606
|
+
function omittedNotice(shown, total, singular, plural) {
|
|
607
|
+
if (total <= shown)
|
|
608
|
+
return [];
|
|
609
|
+
const omitted = total - shown;
|
|
610
|
+
const noun = omitted === 1 ? singular : plural;
|
|
611
|
+
return [
|
|
612
|
+
` ... and ${groupDigits(omitted)} more ${noun} not shown —`
|
|
613
|
+
+ ` this is the ${groupDigits(shown)} most expensive, not the whole tree`,
|
|
614
|
+
];
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The three row counters that keep an estimate from reading as settled.
|
|
618
|
+
*
|
|
619
|
+
* Shared by the per-path answer and by every region of the cost map, because it
|
|
620
|
+
* is one rule stated in one place: a row whose size is unknown, whose file the
|
|
621
|
+
* 4 MiB cliff skipped, or which sits behind such a file is COUNTED here and
|
|
622
|
+
* summed into nothing. Two copies of these three lines would be two places for a
|
|
623
|
+
* `?? 0` to appear in.
|
|
624
|
+
*
|
|
625
|
+
* @param counters - Anything carrying the three counts — `ContextTotals`, or a
|
|
626
|
+
* region's own always-row counts
|
|
627
|
+
* @returns The three lines, unterminated
|
|
628
|
+
*/
|
|
629
|
+
function counterLines(counters) {
|
|
630
|
+
return [
|
|
631
|
+
` size unknown ${countOf(counters.unknownTokenRows, 'row', 'rows')}`
|
|
632
|
+
+ ' (counted, never summed as zero)',
|
|
633
|
+
` skipped over 4 MiB ${countOf(counters.skippedOversizeRows, 'row', 'rows')}`,
|
|
634
|
+
` pruned behind a skip ${countOf(counters.prunedRows, 'row', 'rows')}`,
|
|
635
|
+
];
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* A count with its noun, digit-grouped and agreeing in number.
|
|
639
|
+
*
|
|
640
|
+
* The alternative — a bare `${n} ${plural}` — prints "1 locations" whenever a
|
|
641
|
+
* region has exactly one, which is the common case on a tree with many small
|
|
642
|
+
* packages and reads as a rendering bug the moment anyone sees it. Both forms are
|
|
643
|
+
* PARAMETERS rather than a suffixed `s`, because the nouns this report uses are
|
|
644
|
+
* not all regular ("directory" is the counter-example living two functions away).
|
|
645
|
+
*
|
|
646
|
+
* @param value - The count
|
|
647
|
+
* @param singular - The noun for exactly one
|
|
648
|
+
* @param plural - The noun for any other count, zero included
|
|
649
|
+
* @returns The count and its noun
|
|
650
|
+
*/
|
|
651
|
+
function countOf(value, singular, plural) {
|
|
652
|
+
return `${groupDigits(value)} ${value === 1 ? singular : plural}`;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* A count, digit-grouped and right-aligned to a column width.
|
|
656
|
+
*
|
|
657
|
+
* @param value - The count
|
|
658
|
+
* @param width - The column width; a longer number overflows rather than truncates
|
|
659
|
+
* @returns The padded text
|
|
660
|
+
*/
|
|
661
|
+
function padCount(value, width) {
|
|
662
|
+
return groupDigits(value).padStart(width);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* A non-negative integer with thousands separators.
|
|
666
|
+
*
|
|
667
|
+
* ⚠️ Written out by hand rather than through `toLocaleString` or `Intl`: those
|
|
668
|
+
* are ICU- and locale-dependent, so the same tree would render `38,412` on one
|
|
669
|
+
* machine and `38 412` on another, and this report is exactly the kind of output
|
|
670
|
+
* people diff between machines. Every number reaching it is a token estimate or a
|
|
671
|
+
* row count, so the negative case cannot arise and is not invented for.
|
|
672
|
+
*
|
|
673
|
+
* @param value - The count
|
|
674
|
+
* @returns The grouped digits
|
|
675
|
+
*/
|
|
676
|
+
function groupDigits(value) {
|
|
677
|
+
const digits = String(value);
|
|
678
|
+
let grouped = '';
|
|
679
|
+
for (const [index, digit] of [...digits].entries()) {
|
|
680
|
+
if (index > 0 && (digits.length - index) % 3 === 0)
|
|
681
|
+
grouped += ',';
|
|
682
|
+
grouped += digit;
|
|
683
|
+
}
|
|
684
|
+
return grouped;
|
|
685
|
+
}
|
|
324
686
|
/**
|
|
325
687
|
* Populate the Claude-context lane for one tree.
|
|
326
688
|
*
|
|
@@ -333,7 +695,7 @@ function renderEnvelopeText(documents) {
|
|
|
333
695
|
async function populateContext(root, logger) {
|
|
334
696
|
return withPopulationCache({ root }, async (cache) => {
|
|
335
697
|
const gitTracker = await gitTrackerForProjectRoot(root);
|
|
336
|
-
return buildClaudeContextPopulation({ root, ...populationWiring(logger, gitTracker, cache) });
|
|
698
|
+
return buildClaudeContextPopulation({ root, ...populationWiring(logger, gitTracker, cache, root) });
|
|
337
699
|
});
|
|
338
700
|
}
|
|
339
701
|
/**
|
|
@@ -434,9 +796,11 @@ function discoverySection(discoverable) {
|
|
|
434
796
|
const { rows, totals } = discoverable;
|
|
435
797
|
const lines = [
|
|
436
798
|
'Discoverable in one hop — LINKED from the loaded files, NOT loaded',
|
|
437
|
-
` ${totals.discoverableTokens} tokens if every link were followed once
|
|
438
|
-
|
|
439
|
-
|
|
799
|
+
` ${groupDigits(totals.discoverableTokens)} tokens if every link were followed once`
|
|
800
|
+
+ ' (a ceiling, never a charge)',
|
|
801
|
+
` ${groupDigits(totals.unknownTokenRows)} size unknown`
|
|
802
|
+
+ ` · ${groupDigits(totals.unrealizedRows)} not in this tree`
|
|
803
|
+
+ ` · ${groupDigits(totals.outsideRootRows)} outside the root`,
|
|
440
804
|
];
|
|
441
805
|
for (const row of rows) {
|
|
442
806
|
lines.push(` ${displayPath(row.path)} — ${discoverableCost(row)}`);
|
|
@@ -465,7 +829,7 @@ function discoverableCost(row) {
|
|
|
465
829
|
return 'not realized in this projection, so nothing is known of it';
|
|
466
830
|
if (row.tokens === null)
|
|
467
831
|
return 'size unknown: no measured blob, so it is counted, not summed';
|
|
468
|
-
return `${row.tokens} tokens if opened`;
|
|
832
|
+
return `${groupDigits(row.tokens)} tokens if opened`;
|
|
469
833
|
}
|
|
470
834
|
/**
|
|
471
835
|
* The heading, which states up front whether the query was exact.
|
|
@@ -489,11 +853,12 @@ function headingLines(document) {
|
|
|
489
853
|
function estimateLines(totals) {
|
|
490
854
|
return [
|
|
491
855
|
'Token estimate',
|
|
492
|
-
` always-loaded ${totals.alwaysTokens} tokens`,
|
|
493
|
-
` on-demand ${totals.onDemandTokens} tokens`,
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
856
|
+
` always-loaded ${groupDigits(totals.alwaysTokens)} tokens`,
|
|
857
|
+
` on-demand ${groupDigits(totals.onDemandTokens)} tokens`,
|
|
858
|
+
// ⛔ UNCONDITIONAL here, unlike a region's — see {@link quietCounterLines}.
|
|
859
|
+
// A single-path answer has no tree-level roll-up to fall back on, so these
|
|
860
|
+
// three lines are its only statement that the rows were counted at all.
|
|
861
|
+
...counterLines(totals),
|
|
497
862
|
'',
|
|
498
863
|
];
|
|
499
864
|
}
|
|
@@ -548,7 +913,12 @@ export function chargeText(row) {
|
|
|
548
913
|
}
|
|
549
914
|
if (row.tokens === null)
|
|
550
915
|
return 'size unknown: no measured blob, so it is counted, not summed';
|
|
551
|
-
|
|
916
|
+
// ⚠️ Grouped through the SAME helper the tables use. Two spellings of one
|
|
917
|
+
// quantity — `14,396` in a column and `8385` on the row ten lines below it —
|
|
918
|
+
// read as a bug in the measurement rather than as a formatting choice. This
|
|
919
|
+
// changes only how the number is written, never whether one is written: the
|
|
920
|
+
// null branch above is what keeps an unknown size out of the digits entirely.
|
|
921
|
+
return `${groupDigits(row.tokens)} tokens`;
|
|
552
922
|
}
|
|
553
923
|
/**
|
|
554
924
|
* One admission, in prose.
|