@platformos/platformos-mcp-supervisor 0.0.2 → 0.1.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/README.md +90 -0
- package/dist/adapter-input.d.ts +113 -0
- package/dist/adapter-input.js +210 -0
- package/dist/adapter-input.spec.d.ts +1 -0
- package/dist/adapter-input.spec.js +275 -0
- package/dist/bin/args.spec.d.ts +1 -0
- package/dist/bin/args.spec.js +35 -0
- package/dist/check-docs.d.ts +21 -0
- package/dist/check-docs.js +38 -0
- package/dist/check-docs.spec.d.ts +1 -0
- package/dist/check-docs.spec.js +87 -0
- package/dist/context.d.ts +26 -0
- package/dist/context.js +11 -0
- package/dist/cost-model.d.ts +128 -0
- package/dist/cost-model.js +137 -0
- package/dist/cost-model.spec.d.ts +1 -0
- package/dist/cost-model.spec.js +91 -0
- package/dist/deadline.d.ts +27 -0
- package/dist/deadline.js +40 -0
- package/dist/deadline.spec.d.ts +1 -0
- package/dist/deadline.spec.js +83 -0
- package/dist/enrich/enrich.d.ts +87 -0
- package/dist/enrich/enrich.js +97 -0
- package/dist/enrich/enrich.spec.d.ts +1 -0
- package/dist/enrich/enrich.spec.js +184 -0
- package/dist/enrich/symbol.d.ts +16 -0
- package/dist/enrich/symbol.js +58 -0
- package/dist/enrich/symbol.spec.d.ts +1 -0
- package/dist/enrich/symbol.spec.js +89 -0
- package/dist/impact/impact.d.ts +9 -0
- package/dist/impact/impact.js +189 -0
- package/dist/impact/impact.spec.d.ts +1 -0
- package/dist/impact/impact.spec.js +357 -0
- package/dist/impact/project-scan.d.ts +37 -0
- package/dist/impact/project-scan.js +60 -0
- package/dist/impact/project-scan.spec.d.ts +1 -0
- package/dist/impact/project-scan.spec.js +121 -0
- package/dist/index.d.ts +1 -1
- package/dist/lint/fixes.d.ts +28 -0
- package/dist/lint/fixes.js +30 -0
- package/dist/lint/lint-batch.d.ts +100 -0
- package/dist/lint/lint-batch.js +98 -0
- package/dist/lint/lint-batch.spec.d.ts +1 -0
- package/dist/lint/lint-batch.spec.js +503 -0
- package/dist/lint/lint.d.ts +11 -1
- package/dist/lint/lint.js +9 -3
- package/dist/result/assemble.d.ts +12 -9
- package/dist/result/assemble.js +64 -16
- package/dist/result/assemble.spec.d.ts +1 -0
- package/dist/result/assemble.spec.js +319 -0
- package/dist/result/blocking.d.ts +86 -0
- package/dist/result/blocking.js +96 -0
- package/dist/result/blocking.spec.d.ts +1 -0
- package/dist/result/blocking.spec.js +201 -0
- package/dist/result/impact-states.d.ts +16 -0
- package/dist/result/impact-states.js +18 -0
- package/dist/result/response-budget.d.ts +9 -0
- package/dist/result/response-budget.js +131 -0
- package/dist/result/response-budget.spec.d.ts +1 -0
- package/dist/result/response-budget.spec.js +222 -0
- package/dist/result/types.d.ts +210 -104
- package/dist/result/types.js +4 -13
- package/dist/transport/instructions-coverage.spec.d.ts +1 -0
- package/dist/transport/instructions-coverage.spec.js +176 -0
- package/dist/transport/instructions.d.ts +1 -0
- package/dist/transport/instructions.js +132 -0
- package/dist/transport/process-guards.d.ts +43 -0
- package/dist/transport/process-guards.js +63 -0
- package/dist/transport/process-guards.spec.d.ts +1 -0
- package/dist/transport/process-guards.spec.js +200 -0
- package/dist/transport/server.d.ts +1 -1
- package/dist/transport/server.js +16 -9
- package/dist/transport/validate-code.d.ts +36 -18
- package/dist/transport/validate-code.js +200 -33
- package/dist/transport/validate-code.spec.d.ts +1 -0
- package/dist/transport/validate-code.spec.js +2303 -0
- package/dist/validate/batch-bounds.d.ts +34 -0
- package/dist/validate/batch-bounds.js +60 -0
- package/dist/validate/batch-coherence.d.ts +21 -0
- package/dist/validate/batch-coherence.js +54 -0
- package/dist/validate/batch-coherence.spec.d.ts +1 -0
- package/dist/validate/batch-coherence.spec.js +111 -0
- package/dist/validate/validate-buffers.d.ts +27 -0
- package/dist/validate/validate-buffers.js +265 -0
- package/package.json +8 -7
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { APP_SOURCE_SUBTREES } from '@platformos/platformos-common';
|
|
2
|
+
import { join, parse } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { bufferTooLarge, fileApplicability, MAX_BUFFER_BYTES, misplacedSource, notPlatformOSFile, toAbsoluteFilePath, } from './adapter-input.js';
|
|
5
|
+
/**
|
|
6
|
+
* Without this gate `validate_code` lints ANY path, and because check-common's
|
|
7
|
+
* `toSourceCode` types an unrecognized extension as JSON, `/etc/passwd` came back as
|
|
8
|
+
* `ValidJSON` with `must_fix_before_write: true` while `/etc/shadow` containing `{}` came
|
|
9
|
+
* back `ok` — a false approval for a path outside the project.
|
|
10
|
+
*
|
|
11
|
+
* These pin the pure decision; the end-to-end consequence is pinned in
|
|
12
|
+
* `transport/validate-code.spec.ts`.
|
|
13
|
+
*/
|
|
14
|
+
const PROJECT = join('/srv', 'app');
|
|
15
|
+
const applicable = (filePath) => fileApplicability(PROJECT, filePath);
|
|
16
|
+
/**
|
|
17
|
+
* The two refusal messages, duplicated from the implementation on purpose: the prose is the
|
|
18
|
+
* entire payload of a declined call. Both must state that nothing was checked — an agent
|
|
19
|
+
* must never read a refusal as a pass.
|
|
20
|
+
*/
|
|
21
|
+
const outside = (filePath) => ({
|
|
22
|
+
applicable: false,
|
|
23
|
+
code: 'outside_project',
|
|
24
|
+
reason: `\`${filePath}\` resolves outside the project root (${PROJECT}), so there is no project ` +
|
|
25
|
+
`context to validate it against. This server validates only files inside the project it was ` +
|
|
26
|
+
`started for. Nothing was checked — treat this as "unknown", not as approval.`,
|
|
27
|
+
});
|
|
28
|
+
const unsupported = (relativePath) => ({
|
|
29
|
+
applicable: false,
|
|
30
|
+
code: 'unsupported_type',
|
|
31
|
+
reason: `\`${relativePath}\` is not a platformOS source file, so there is nothing to check. The ` +
|
|
32
|
+
`platform deploys app/, marketplace_builder/, modules/*/public/, modules/*/private/ only. ` +
|
|
33
|
+
`Nothing was checked — writing this file is your call, not a validated pass. Directory ` +
|
|
34
|
+
`structure: https://documentation.platformos.com/developer-guide/platformos-workflow/directory-structure`,
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* An ASSET is refused for a different reason and says so. It IS part of the app and IS
|
|
38
|
+
* deployed — there is simply no source in it to check — so the "not a platformOS source
|
|
39
|
+
* file, the platform deploys app/ only" message above would be false about it.
|
|
40
|
+
*/
|
|
41
|
+
const asset = (relativePath) => ({
|
|
42
|
+
applicable: false,
|
|
43
|
+
code: 'unsupported_type',
|
|
44
|
+
reason: `\`${relativePath}\` is an asset, not a source file the linter understands — it checks ` +
|
|
45
|
+
`Liquid, GraphQL and YAML — so no check ran against it. The file is still deployed and ` +
|
|
46
|
+
`served; there is simply nothing here to validate. Nothing was checked, which is not the ` +
|
|
47
|
+
`same as a pass.`,
|
|
48
|
+
});
|
|
49
|
+
describe('Unit: fileApplicability', () => {
|
|
50
|
+
describe('accepts every file type the linter would actually visit', () => {
|
|
51
|
+
it.each([
|
|
52
|
+
'app/views/pages/index.liquid',
|
|
53
|
+
'app/views/partials/card.liquid',
|
|
54
|
+
'app/views/layouts/application.liquid',
|
|
55
|
+
'app/lib/helper.liquid',
|
|
56
|
+
// A page that emits JSON is Liquid — classified by the `.liquid` extension, NOT by
|
|
57
|
+
// the `.json` in its name, which a naive extension check would misroute.
|
|
58
|
+
'app/views/pages/feed.json.liquid',
|
|
59
|
+
'app/graphql/things/search.graphql',
|
|
60
|
+
'app/translations/en.yml',
|
|
61
|
+
'app/schema/thing.yml',
|
|
62
|
+
'modules/mcp/public/views/partials/tool.liquid',
|
|
63
|
+
])('%s', (filePath) => {
|
|
64
|
+
expect(applicable(filePath)).toEqual({ applicable: true });
|
|
65
|
+
});
|
|
66
|
+
it('accepts an absolute path inside the project', () => {
|
|
67
|
+
expect(applicable(join(PROJECT, 'app/views/pages/index.liquid'))).toEqual({
|
|
68
|
+
applicable: true,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it('accepts a path whose `..` segments normalize back inside the project', () => {
|
|
72
|
+
expect(applicable('app/views/pages/../partials/card.liquid')).toEqual({ applicable: true });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('declines paths outside the project root', () => {
|
|
76
|
+
// `''` is refused by the zod schema instead — see validate-code.spec.ts.
|
|
77
|
+
it.each([
|
|
78
|
+
['an absolute system path', '/etc/passwd'],
|
|
79
|
+
['a relative traversal', '../../../etc/passwd'],
|
|
80
|
+
['a traversal onto a plausible-looking file', '../other/app/views/pages/x.liquid'],
|
|
81
|
+
['an absolute path outside the root', '/tmp/scratch/app/views/pages/x.liquid'],
|
|
82
|
+
// A `startsWith` containment test would ACCEPT this, since
|
|
83
|
+
// '/srv/app-backup'.startsWith('/srv/app'); only a path-segment comparison rejects it.
|
|
84
|
+
['a sibling root extending the project name', '/srv/app-backup/app/views/pages/x.liquid'],
|
|
85
|
+
])('%s', (_label, filePath) => {
|
|
86
|
+
expect(applicable(filePath)).toEqual(outside(filePath));
|
|
87
|
+
});
|
|
88
|
+
it.each([
|
|
89
|
+
['the project root itself', PROJECT],
|
|
90
|
+
['the project root via a round trip', 'app/..'],
|
|
91
|
+
])('%s is a directory, not a file under edit', (_label, filePath) => {
|
|
92
|
+
expect(applicable(filePath)).toEqual(outside(filePath));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('declines in-project files that are not platformOS sources', () => {
|
|
96
|
+
it.each([
|
|
97
|
+
['markdown', 'README.md'],
|
|
98
|
+
['plain text', 'app/notes.txt'],
|
|
99
|
+
// The extension whose fallback used to swallow everything else.
|
|
100
|
+
['standalone json', 'app/pos-modules.json'],
|
|
101
|
+
['ruby', 'script/deploy.rb'],
|
|
102
|
+
['no extension at all', 'Makefile'],
|
|
103
|
+
])('%s', (_label, filePath) => {
|
|
104
|
+
expect(applicable(filePath)).toEqual(unsupported(filePath));
|
|
105
|
+
});
|
|
106
|
+
/**
|
|
107
|
+
* REFUSED BY TYPE, not by extension, and this branch fixes a measured FALSE BLOCK: a
|
|
108
|
+
* bare `.liquid` has no response format, so `sourceCodeTypeOf` falls back to
|
|
109
|
+
* `html.liquid` and handed `app/assets/x.liquid` to the Liquid parser. Backwards
|
|
110
|
+
* besides, since `theme.css.liquid` — the asset form the platform DOES process — was
|
|
111
|
+
* exempt all along.
|
|
112
|
+
*
|
|
113
|
+
* The rule is `platformos-common`'s `isParsedFileType`, and this gate ASKS it rather
|
|
114
|
+
* than comparing to `PlatformOSFileType.Asset` itself, so it cannot drift from the
|
|
115
|
+
* lint's answer.
|
|
116
|
+
*/
|
|
117
|
+
it.each([
|
|
118
|
+
['an image', 'app/assets/logo.png'],
|
|
119
|
+
['a stylesheet', 'app/assets/site.css'],
|
|
120
|
+
['a css asset partial', 'app/assets/site.css.liquid'],
|
|
121
|
+
['a js asset partial', 'app/assets/app.js.liquid'],
|
|
122
|
+
['an asset manifest', 'app/assets/manifest.json'],
|
|
123
|
+
// THE CASE THAT WAS BLOCKING WRITES. A parser accepts it; the platform does not
|
|
124
|
+
// evaluate it. Sabotage check: revert the `Asset` branch in `fileApplicability` and
|
|
125
|
+
// only this row fails, because it is the only one a parser claims.
|
|
126
|
+
['a bare liquid file the parser would accept', 'app/assets/x.liquid'],
|
|
127
|
+
['a nested bare liquid file', 'app/assets/nested/deep/w.liquid'],
|
|
128
|
+
// Legacy spelling of the same directory, so the rule cannot be half-applied.
|
|
129
|
+
['a marketplace_builder asset', 'marketplace_builder/assets/x.liquid'],
|
|
130
|
+
])('refuses %s as an asset, never as a source', (_label, filePath) => {
|
|
131
|
+
expect(applicable(filePath)).toEqual(asset(filePath));
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* THE LINE THIS GATE DRAWS. It asks ONE question — "is this a type we parse at all?" —
|
|
135
|
+
* from the extension and nothing else, and deliberately not whether the path is
|
|
136
|
+
* somewhere the platform deploys, which `isSupportedSourceFile(uri, root)` would answer.
|
|
137
|
+
*
|
|
138
|
+
* Using that here would collapse two situations into one wrong answer.
|
|
139
|
+
* `scripts/helper.liquid` IS a platformOS source in the wrong place, and its author
|
|
140
|
+
* needs to hear "nothing will ever load this", not "this is not a platformOS file".
|
|
141
|
+
* check-node draws that distinction where classification happens, so the gate admits
|
|
142
|
+
* anything parseable and lets the lint say which it is.
|
|
143
|
+
*
|
|
144
|
+
* So being ADMITTED here means "worth asking about", not "will be linted".
|
|
145
|
+
*/
|
|
146
|
+
it.each([
|
|
147
|
+
// Liquid outside every deployed subtree: admitted here, then reported as
|
|
148
|
+
// `misplaced_source` by the lint, which is the answer that helps.
|
|
149
|
+
['unclassified liquid', 'scripts/helper.liquid'],
|
|
150
|
+
// A REAL platformOS source under master's classification: `PlatformOSFileType` has
|
|
151
|
+
// `InstanceConfig` (`app/config.yml`) and `UserSchema` (`app/user.yml`) as fixed-path
|
|
152
|
+
// singletons, and the file-type-coverage group pins that something examines them.
|
|
153
|
+
['the app config singleton', 'app/config.yml'],
|
|
154
|
+
['the user schema singleton', 'app/user.yml'],
|
|
155
|
+
// KNOWN WRONG ADVICE, admitted here on purpose rather than special-cased. A `.yml` at
|
|
156
|
+
// the project root is parseable, so the lint calls it `misplaced-source` — telling the
|
|
157
|
+
// author of this toolchain's OWN config file that it is "likely misplaced". It never
|
|
158
|
+
// blocks a write, and narrowing it belongs at the point of classification, in
|
|
159
|
+
// check-node. Asserted so the behaviour is recorded and the fix has a test to flip.
|
|
160
|
+
['the check config itself', '.platformos-check.yml'],
|
|
161
|
+
])('admits %s, leaving the verdict to the lint', (_label, filePath) => {
|
|
162
|
+
expect(applicable(filePath)).toEqual({ applicable: true });
|
|
163
|
+
});
|
|
164
|
+
it('reports the PROJECT-RELATIVE path even when given an absolute one', () => {
|
|
165
|
+
// Forward slashes, spelled literally rather than built with `join`: the reason string
|
|
166
|
+
// is agent-facing, and an agent that sent `app/notes.txt` must get that spelling back
|
|
167
|
+
// on every platform. Building the expectation with `join` made it agree with whatever
|
|
168
|
+
// the host does, so it passed on POSIX while asserting `app\notes.txt` on Windows.
|
|
169
|
+
//
|
|
170
|
+
// This cannot FAIL on POSIX if the separator handling regresses — check-common's
|
|
171
|
+
// `path.spec.ts` is what guards that, over Windows-shaped input on any host.
|
|
172
|
+
expect(applicable(join(PROJECT, 'app/notes.txt'))).toEqual(unsupported('app/notes.txt'));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
it('checks containment BEFORE type, so an unsupported outside path cites the root', () => {
|
|
176
|
+
// Both rules reject `/etc/passwd`; the order determines which reason the agent
|
|
177
|
+
// gets, and "outside the project" is the more actionable of the two.
|
|
178
|
+
expect(applicable('/etc/passwd')).toEqual(outside('/etc/passwd'));
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
/**
|
|
182
|
+
* The size bound is the ONLY guard that acts before a pathological buffer reaches the
|
|
183
|
+
* parser, and it is sized against the FULL lint (~61 ms/KiB), not the parse alone.
|
|
184
|
+
*
|
|
185
|
+
* A deadline cannot substitute for it: the parse is synchronous, so the timer cannot even
|
|
186
|
+
* FIRE during one, and further out the parser stops completing at all — 2 MiB throws inside
|
|
187
|
+
* ohm's CST→AST recursion, 4 MiB produced a native V8 abort that no handler can catch.
|
|
188
|
+
*/
|
|
189
|
+
describe('Unit: bufferTooLarge', () => {
|
|
190
|
+
const bytes = (n) => 'a'.repeat(n);
|
|
191
|
+
it('accepts a buffer at exactly the limit', () => {
|
|
192
|
+
// Boundary is inclusive — an exactly-limit buffer is legal, so `<=` not `<`.
|
|
193
|
+
expect(bufferTooLarge(bytes(MAX_BUFFER_BYTES))).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
it('refuses a buffer one byte over the limit', () => {
|
|
196
|
+
const declined = bufferTooLarge(bytes(MAX_BUFFER_BYTES + 1));
|
|
197
|
+
expect(declined?.code).toEqual('too_large');
|
|
198
|
+
expect(declined?.reason).toEqual('The buffer is 128 KiB, above the 128 KiB limit this server will validate, so it was ' +
|
|
199
|
+
'refused before parsing. Liquid validation costs about 75 ms per KiB and the parser stops ' +
|
|
200
|
+
'completing at all a few MiB out, so checking this would risk hanging the server rather ' +
|
|
201
|
+
'than answering. Nothing was checked — consider splitting the file, which is almost ' +
|
|
202
|
+
'certainly worth doing at this size regardless.');
|
|
203
|
+
});
|
|
204
|
+
it('accepts every realistic file size', () => {
|
|
205
|
+
// The largest real source file found across local projects is 76 KiB.
|
|
206
|
+
expect([
|
|
207
|
+
bufferTooLarge(''),
|
|
208
|
+
bufferTooLarge('{% render "card" %}'),
|
|
209
|
+
bufferTooLarge(bytes(76 * 1024)),
|
|
210
|
+
bufferTooLarge(bytes(120 * 1024)),
|
|
211
|
+
]).toEqual([undefined, undefined, undefined, undefined]);
|
|
212
|
+
});
|
|
213
|
+
it('counts BYTES, not string length, so multi-byte content cannot slip past', () => {
|
|
214
|
+
// '€' is 3 bytes in UTF-8. A `content.length` check would see a third of the real cost
|
|
215
|
+
// and admit a buffer three times the intended size.
|
|
216
|
+
const justUnderByLength = '€'.repeat(Math.floor(MAX_BUFFER_BYTES / 3) + 1);
|
|
217
|
+
expect(justUnderByLength.length).toBeLessThan(MAX_BUFFER_BYTES);
|
|
218
|
+
expect(bufferTooLarge(justUnderByLength)?.code).toEqual('too_large');
|
|
219
|
+
});
|
|
220
|
+
it('reports the actual size in the reason, so the agent knows by how much', () => {
|
|
221
|
+
expect(bufferTooLarge(bytes(2 * 1024 * 1024))?.reason).toContain('The buffer is 2048 KiB');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('Unit: toAbsoluteFilePath', () => {
|
|
225
|
+
it('joins a relative path onto the project root', () => {
|
|
226
|
+
expect(toAbsoluteFilePath(PROJECT, 'app/views/pages/index.liquid')).toEqual(join(PROJECT, 'app/views/pages/index.liquid'));
|
|
227
|
+
});
|
|
228
|
+
it('returns an absolute path unchanged', () => {
|
|
229
|
+
expect(toAbsoluteFilePath(PROJECT, '/etc/passwd')).toEqual('/etc/passwd');
|
|
230
|
+
});
|
|
231
|
+
it('normalizes `..` segments when joining', () => {
|
|
232
|
+
// Expressed against the filesystem ROOT rather than a literal `/etc/passwd`, so it
|
|
233
|
+
// resolves with the host's own separator; hard-coding the POSIX spelling asserted the
|
|
234
|
+
// platform rather than the normalization this test is about.
|
|
235
|
+
const filesystemRoot = parse(PROJECT).root;
|
|
236
|
+
expect(toAbsoluteFilePath(PROJECT, '../../../etc/passwd')).toEqual(join(filesystemRoot, 'etc', 'passwd'));
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
/**
|
|
240
|
+
* `next_step` is prose the supervisor writes ABOUT ITS OWN ANSWER, which invariant #6
|
|
241
|
+
* permits. The line is thin here, because a refusal has to say something about where
|
|
242
|
+
* platformOS looks for files in order to be actionable at all.
|
|
243
|
+
*
|
|
244
|
+
* It stays on the right side of it by DERIVING that fact: the deployed subtrees come from
|
|
245
|
+
* `APP_SOURCE_SUBTREES`, and the rule itself is a documentation URL. A hand-written list
|
|
246
|
+
* would be a second source of truth for the directory layout.
|
|
247
|
+
*/
|
|
248
|
+
describe('Unit: refusal prose derives platform facts rather than restating them', () => {
|
|
249
|
+
const subtrees = APP_SOURCE_SUBTREES.map((subtree) => `${subtree}/`);
|
|
250
|
+
it.each([
|
|
251
|
+
['misplacedSource', misplacedSource('scripts/build.liquid')],
|
|
252
|
+
['notPlatformOSFile', notPlatformOSFile('README.md')],
|
|
253
|
+
])('%s names every deployed subtree, from the shared table', (_name, declined) => {
|
|
254
|
+
expect(subtrees.filter((subtree) => !declined.reason.includes(subtree))).toEqual([]);
|
|
255
|
+
});
|
|
256
|
+
it('points at the documentation for the rule instead of explaining it here', () => {
|
|
257
|
+
// The pointer, not the prose: an agent that needs the whole directory layout is sent
|
|
258
|
+
// to the page that owns it.
|
|
259
|
+
expect(misplacedSource('scripts/build.liquid').reason).toContain('https://documentation.platformos.com/developer-guide/platformos-workflow/directory-structure');
|
|
260
|
+
});
|
|
261
|
+
/**
|
|
262
|
+
* The CONTROL: this must fail if the subtree list is ever hardcoded to today's value.
|
|
263
|
+
* Deriving the expectation from the same constant the code reads would pass either way,
|
|
264
|
+
* so the assertion is that the message tracks a subtree the shared table does NOT
|
|
265
|
+
* currently contain — impossible to satisfy with a literal string.
|
|
266
|
+
*/
|
|
267
|
+
it('would track a new subtree without an edit here', () => {
|
|
268
|
+
// Every subtree in the message came from the table, and the message contains no
|
|
269
|
+
// directory that looks deployed but is absent from it.
|
|
270
|
+
const invented = 'definitely_not_a_subtree';
|
|
271
|
+
expect(misplacedSource('x.liquid').reason).not.toContain(invented);
|
|
272
|
+
expect(APP_SOURCE_SUBTREES).not.toContain(invented);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
//# sourceMappingURL=adapter-input.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseArgs, resolveProjectDir } from './args.js';
|
|
3
|
+
describe('Unit: parseArgs', () => {
|
|
4
|
+
it('parses --project <dir>', () => {
|
|
5
|
+
expect(parseArgs(['--project', '/p'])).toEqual({ projectDir: '/p', help: false });
|
|
6
|
+
});
|
|
7
|
+
it('parses --project=<dir>', () => {
|
|
8
|
+
expect(parseArgs(['--project=/p'])).toEqual({ projectDir: '/p', help: false });
|
|
9
|
+
});
|
|
10
|
+
it('parses --help and -h', () => {
|
|
11
|
+
expect(parseArgs(['--help'])).toEqual({ projectDir: undefined, help: true });
|
|
12
|
+
expect(parseArgs(['-h'])).toEqual({ projectDir: undefined, help: true });
|
|
13
|
+
});
|
|
14
|
+
it('tolerates unknown flags', () => {
|
|
15
|
+
expect(parseArgs(['--future', 'x', '--project', '/p'])).toEqual({
|
|
16
|
+
projectDir: '/p',
|
|
17
|
+
help: false,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it('returns no projectDir when none given', () => {
|
|
21
|
+
expect(parseArgs([])).toEqual({ projectDir: undefined, help: false });
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('Unit: resolveProjectDir', () => {
|
|
25
|
+
it('prefers the --project argument over env and cwd', () => {
|
|
26
|
+
expect(resolveProjectDir({ projectDir: '/arg', help: false }, { POS_SUPERVISOR_PROJECT_DIR: '/env' }, '/cwd')).toEqual('/arg');
|
|
27
|
+
});
|
|
28
|
+
it('falls back to POS_SUPERVISOR_PROJECT_DIR when no argument', () => {
|
|
29
|
+
expect(resolveProjectDir({ help: false }, { POS_SUPERVISOR_PROJECT_DIR: '/env' }, '/cwd')).toEqual('/env');
|
|
30
|
+
});
|
|
31
|
+
it('falls back to cwd when neither argument nor env is set', () => {
|
|
32
|
+
expect(resolveProjectDir({ help: false }, {}, '/cwd')).toEqual('/cwd');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
//# sourceMappingURL=args.spec.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The documentation a check publishes about itself. */
|
|
2
|
+
export interface CheckDocs {
|
|
3
|
+
/** One-line description, from `meta.docs.description`. */
|
|
4
|
+
description: string;
|
|
5
|
+
/**
|
|
6
|
+
* The check's documentation page. Absent for the few checks whose meta publishes no
|
|
7
|
+
* `url` — absent rather than guessed, because a URL assembled from a code is a link
|
|
8
|
+
* that 404s, which is worse than no link.
|
|
9
|
+
*/
|
|
10
|
+
url?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The documentation for a check code, or `undefined` when nothing is registered under it.
|
|
14
|
+
*
|
|
15
|
+
* `undefined` is a real answer, not a gap to paper over: `CheckError` — check-common's code
|
|
16
|
+
* for "a check crashed on this file" — is reported on offenses and is not a registered
|
|
17
|
+
* check, so a consumer must handle a miss rather than assume every offense has a page.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkDocs(code: string): CheckDocs | undefined;
|
|
20
|
+
/** Every registered check code. Exported for the tests that assert this map is complete. */
|
|
21
|
+
export declare function registeredCheckCodes(): string[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a check MEANS and where to read more — derived from check-common's registry.
|
|
3
|
+
*
|
|
4
|
+
* THE SUPERVISOR AUTHORS NO DOCUMENTATION (ARCHITECTURE.md §Invariants). It may describe
|
|
5
|
+
* itself — its result contract, how to read an answer — and nothing about the platform or
|
|
6
|
+
* about what a check is for; that prose lives in `meta.docs.description` and at
|
|
7
|
+
* `meta.docs.url`.
|
|
8
|
+
*
|
|
9
|
+
* So this module is a LOOKUP, not a table, built from `allChecks` at load time: a check
|
|
10
|
+
* renamed, removed or added upstream changes this map without anyone editing it.
|
|
11
|
+
*/
|
|
12
|
+
import { allChecks } from '@platformos/platformos-check-common';
|
|
13
|
+
/**
|
|
14
|
+
* Keyed by `meta.code` ONLY, which is the exact string an offense carries
|
|
15
|
+
* (`check: check.meta.code` in check-common's `check()`).
|
|
16
|
+
*
|
|
17
|
+
* `meta.aliases` are deliberately NOT keys: an alias is a name a CONFIG may use, never
|
|
18
|
+
* something an offense carries, so mapping it would add entries nothing can look up.
|
|
19
|
+
*/
|
|
20
|
+
const BY_CODE = new Map(allChecks.map((check) => [
|
|
21
|
+
check.meta.code,
|
|
22
|
+
{ description: check.meta.docs.description, url: check.meta.docs.url },
|
|
23
|
+
]));
|
|
24
|
+
/**
|
|
25
|
+
* The documentation for a check code, or `undefined` when nothing is registered under it.
|
|
26
|
+
*
|
|
27
|
+
* `undefined` is a real answer, not a gap to paper over: `CheckError` — check-common's code
|
|
28
|
+
* for "a check crashed on this file" — is reported on offenses and is not a registered
|
|
29
|
+
* check, so a consumer must handle a miss rather than assume every offense has a page.
|
|
30
|
+
*/
|
|
31
|
+
export function checkDocs(code) {
|
|
32
|
+
return BY_CODE.get(code);
|
|
33
|
+
}
|
|
34
|
+
/** Every registered check code. Exported for the tests that assert this map is complete. */
|
|
35
|
+
export function registeredCheckCodes() {
|
|
36
|
+
return [...BY_CODE.keys()];
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=check-docs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check-documentation lookup is DERIVED, and these assertions are written so a
|
|
3
|
+
* documentation release cannot fail them while OUR side falling behind does.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here restates what a check's description SAYS, or which URL it has. That is
|
|
6
|
+
* settled in check-common and in the `platformos-documentation` repo, where the page
|
|
7
|
+
* lives; a test here that pinned the text would duplicate a gate it cannot see and rot on
|
|
8
|
+
* a release that is perfectly correct.
|
|
9
|
+
*/
|
|
10
|
+
import { allChecks } from '@platformos/platformos-check-common';
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import { checkDocs, registeredCheckCodes } from './check-docs.js';
|
|
13
|
+
describe('Unit: checkDocs', () => {
|
|
14
|
+
it('answers for every registered check, and for nothing else', () => {
|
|
15
|
+
// Derived on both sides: the registry is the input AND the expectation, so adding a
|
|
16
|
+
// check upstream cannot leave this map behind.
|
|
17
|
+
expect(registeredCheckCodes().sort()).toEqual(allChecks.map((check) => check.meta.code).sort());
|
|
18
|
+
});
|
|
19
|
+
it('returns each check its own description, taken from the registry rather than restated', () => {
|
|
20
|
+
const mismatched = allChecks
|
|
21
|
+
.map((check) => ({
|
|
22
|
+
code: check.meta.code,
|
|
23
|
+
expected: check.meta.docs.description,
|
|
24
|
+
actual: checkDocs(check.meta.code)?.description,
|
|
25
|
+
}))
|
|
26
|
+
.filter((entry) => entry.actual !== entry.expected);
|
|
27
|
+
expect(mismatched).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
it('returns the check its own documentation URL, and omits it when meta publishes none', () => {
|
|
30
|
+
const mismatched = allChecks
|
|
31
|
+
.map((check) => ({
|
|
32
|
+
code: check.meta.code,
|
|
33
|
+
expected: check.meta.docs.url,
|
|
34
|
+
actual: checkDocs(check.meta.code)?.url,
|
|
35
|
+
}))
|
|
36
|
+
.filter((entry) => entry.actual !== entry.expected);
|
|
37
|
+
expect(mismatched).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* A miss is a real answer. `CheckError` is check-common's code for "a check crashed on
|
|
41
|
+
* this file"; it reaches an agent as an ordinary offense and has no registry entry, so a
|
|
42
|
+
* consumer that assumed every code resolves would throw on exactly the paths that are
|
|
43
|
+
* already going wrong.
|
|
44
|
+
*/
|
|
45
|
+
it('returns undefined for a code that is not a registered check', () => {
|
|
46
|
+
expect(checkDocs('CheckError')).toBeUndefined();
|
|
47
|
+
expect(checkDocs('NotACheckAtAll')).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* EVERY check publishes a documentation URL — a requirement rather than a report, now
|
|
51
|
+
* that the server instructions tell an agent to follow `see_also` instead of restating
|
|
52
|
+
* platform rules. A check with no URL means an agent is stopped by a finding and given
|
|
53
|
+
* nowhere to read about it.
|
|
54
|
+
*
|
|
55
|
+
* A new check therefore fails here until it is documented: the page is part of shipping a
|
|
56
|
+
* check, not a follow-up.
|
|
57
|
+
*/
|
|
58
|
+
it('every registered check publishes a documentation URL', () => {
|
|
59
|
+
const withoutUrl = allChecks
|
|
60
|
+
.filter((check) => !check.meta.docs.url)
|
|
61
|
+
.map((check) => check.meta.code)
|
|
62
|
+
.sort();
|
|
63
|
+
expect(withoutUrl, `these checks have no docs.url — add a page under ` +
|
|
64
|
+
`app/views/pages/developer-guide/platformos-check/checks/ in platformos-documentation, ` +
|
|
65
|
+
`plus its overview row and nav entry:\n${withoutUrl.join('\n')}`).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* The URL must be one the documentation site can serve, not merely a non-empty string.
|
|
69
|
+
*
|
|
70
|
+
* Asserted by SHAPE rather than by fetching — no network in a unit test — which still
|
|
71
|
+
* catches the realistic mistake: a URL pointing at the wrong section, or a check code
|
|
72
|
+
* pasted where a slug belongs.
|
|
73
|
+
*
|
|
74
|
+
* A check with NO url is skipped rather than dereferenced; that is the sibling test's
|
|
75
|
+
* subject, and a `TypeError` here would preempt it.
|
|
76
|
+
*/
|
|
77
|
+
it('every documentation URL points into the checks section of the docs site', () => {
|
|
78
|
+
const BASE = 'https://documentation.platformos.com/developer-guide/platformos-check/checks/';
|
|
79
|
+
const malformed = allChecks
|
|
80
|
+
.map((check) => ({ code: check.meta.code, url: check.meta.docs.url }))
|
|
81
|
+
.filter(({ url }) => url !== undefined &&
|
|
82
|
+
(!url.startsWith(BASE) || !/^[a-z0-9-]+$/.test(url.slice(BASE.length))))
|
|
83
|
+
.map(({ code, url }) => `${code} -> ${url}`);
|
|
84
|
+
expect(malformed).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
//# sourceMappingURL=check-docs.spec.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The per-server context and the request-path deadlines — shared INFRASTRUCTURE. */
|
|
2
|
+
import type { Logger } from './logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Per-server context threaded into every handler.
|
|
5
|
+
*
|
|
6
|
+
* NO CACHE HANDLES HERE, and their absence is the point. check-node keeps ONE lazy `App`
|
|
7
|
+
* per project at process level and reconciles it per call, so a cache nobody has to
|
|
8
|
+
* remember to pass is a cache no call path can forget — do not reintroduce a handle here
|
|
9
|
+
* (see check-node's "Process-level state"). The blast radius is likewise DERIVED per
|
|
10
|
+
* request from the project's text (`impact/project-scan.ts`), so nothing can be stale.
|
|
11
|
+
*/
|
|
12
|
+
export interface SupervisorContext {
|
|
13
|
+
/** Absolute project root that buffers are validated against. */
|
|
14
|
+
projectDir: string;
|
|
15
|
+
log: Logger;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deadline for the blast radius. Much tighter than the lint's, which is a function of
|
|
19
|
+
* admitted bytes and lives in `cost-model.ts` — deliberately not re-exported from here.
|
|
20
|
+
*
|
|
21
|
+
* Tight because the work is small — one project read (235 ms measured on a 2,615-file
|
|
22
|
+
* project) plus a name filter and a handful of parses, p50 9 ms / p90 63 ms per file — and
|
|
23
|
+
* because exceeding it costs only the answer, which has an `unavailable` state. The tail it
|
|
24
|
+
* exists for is real but rare: the most-referenced partial measured took ~1 s.
|
|
25
|
+
*/
|
|
26
|
+
export declare const IMPACT_DEADLINE_MS = 2000;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deadline for the blast radius. Much tighter than the lint's, which is a function of
|
|
3
|
+
* admitted bytes and lives in `cost-model.ts` — deliberately not re-exported from here.
|
|
4
|
+
*
|
|
5
|
+
* Tight because the work is small — one project read (235 ms measured on a 2,615-file
|
|
6
|
+
* project) plus a name filter and a handful of parses, p50 9 ms / p90 63 ms per file — and
|
|
7
|
+
* because exceeding it costs only the answer, which has an `unavailable` state. The tail it
|
|
8
|
+
* exists for is real but rare: the most-referenced partial measured took ~1 s.
|
|
9
|
+
*/
|
|
10
|
+
export const IMPACT_DEADLINE_MS = 2_000;
|
|
11
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a lint COSTS — and therefore how much work one request may admit, and how long the
|
|
3
|
+
* server waits for it.
|
|
4
|
+
*
|
|
5
|
+
* The byte caps and the deadline are ONE relationship, written here as arithmetic and
|
|
6
|
+
* derived rather than chosen. Adding per-node work moves {@link LINT_MS_PER_KIB} and every
|
|
7
|
+
* dependent bound follows; `cost-model.spec.ts` fails if any of them stops fitting. A batch
|
|
8
|
+
* admitted past its deadline returns `timed_out` for EVERY file in the request, so the two
|
|
9
|
+
* drifting apart fails quietly and in the expensive direction.
|
|
10
|
+
*
|
|
11
|
+
* THE DEADLINE SCALES WITH THE REQUEST because a fixed one and a meaningful batch cap
|
|
12
|
+
* cannot both exist: hold it at 60 s and the largest admissible batch is ~133 KiB, smaller
|
|
13
|
+
* than a single legal buffer. Scaling costs little — this deadline is a backstop for ASYNC
|
|
14
|
+
* stalls (see `deadline.ts`) rather than a CPU bound.
|
|
15
|
+
*
|
|
16
|
+
* Every number here is MEASURED, and the measurement is named. A plausible constant is how
|
|
17
|
+
* this file rots back into unrelated magic numbers.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Wall-clock cost of linting one KiB of buffer, on an idle machine, warm cache.
|
|
21
|
+
*
|
|
22
|
+
* Measured end to end (`runValidateCode`, the whole request path), not against the parse
|
|
23
|
+
* alone. Reproduce with `scripts/measure-lint-cost.mjs`, which builds a synthetic project
|
|
24
|
+
* and prints the rate for a single buffer and for batch shapes; the slowest observed was
|
|
25
|
+
* ~51 ms/KiB, on CLEAN markup rather than the diagnostic-dense shape.
|
|
26
|
+
*
|
|
27
|
+
* DO NOT LOWER THIS TO MATCH A FASTER MEASUREMENT. The rate varies by several times with
|
|
28
|
+
* the project and the markup, so the constant is deliberately set above the slowest
|
|
29
|
+
* observed value. Being wrong FAST costs a longer wait before a stall is declared; being
|
|
30
|
+
* wrong SLOW admits a request that cannot finish and returns `timed_out` for every file.
|
|
31
|
+
*/
|
|
32
|
+
export declare const LINT_MS_PER_KIB = 75;
|
|
33
|
+
/**
|
|
34
|
+
* How much slower the same lint runs on a contended machine than on an idle one.
|
|
35
|
+
*
|
|
36
|
+
* Measured at ~2.3x (128 KiB: ~10 s idle, ~23 s under load), held at 3 because it bounds
|
|
37
|
+
* someone else's machine. Being wrong low produces a false `timed_out` — a silent
|
|
38
|
+
* no-answer — while being wrong high costs only a longer wait before declaring a stall.
|
|
39
|
+
*/
|
|
40
|
+
export declare const LOAD_FACTOR = 3;
|
|
41
|
+
/**
|
|
42
|
+
* How far beyond the expected loaded worst case the deadline sits. The deadline must never
|
|
43
|
+
* fire on work that was legal to admit; it exists to catch a stall.
|
|
44
|
+
*/
|
|
45
|
+
export declare const DEADLINE_MARGIN = 2;
|
|
46
|
+
/** Deadline granted per KiB of admitted work: idle cost, under load, with margin. */
|
|
47
|
+
export declare const DEADLINE_MS_PER_KIB: number;
|
|
48
|
+
/**
|
|
49
|
+
* Deadline floor, applied to every request however small.
|
|
50
|
+
*
|
|
51
|
+
* Small requests are not dominated by their buffers: a cold first call also builds the app,
|
|
52
|
+
* loads the config and reconciles the graph (~0.8 s idle, ~1.6 s loaded on a 162-file
|
|
53
|
+
* project). The floor covers that fixed cost with room to spare.
|
|
54
|
+
*/
|
|
55
|
+
export declare const MIN_LINT_DEADLINE_MS = 60000;
|
|
56
|
+
/**
|
|
57
|
+
* Deadline ceiling, and therefore the origin of {@link maxBytesWithin}'s answer for the
|
|
58
|
+
* batch cap.
|
|
59
|
+
*
|
|
60
|
+
* POLICY rather than measurement: how long the server holds a request open before
|
|
61
|
+
* concluding it has stalled. Two minutes is past the point where an agent has any use for
|
|
62
|
+
* the answer, and every extra second of ceiling buys a larger admissible batch.
|
|
63
|
+
*/
|
|
64
|
+
export declare const MAX_LINT_DEADLINE_MS = 120000;
|
|
65
|
+
/**
|
|
66
|
+
* How much of the AGENT'S CONTEXT one `validate_code` answer may spend.
|
|
67
|
+
*
|
|
68
|
+
* POLICY, like {@link MAX_LINT_DEADLINE_MS}, and the one bound here that is not about the
|
|
69
|
+
* server's own work. Unbounded, a legal call was measured returning ~336 000 tokens — more
|
|
70
|
+
* than most context windows hold, with nothing in the payload saying so.
|
|
71
|
+
*
|
|
72
|
+
* At ~153 bytes per diagnostic this budget holds roughly 200 findings. Measured real calls
|
|
73
|
+
* cost tens to hundreds of tokens, so the cap cannot touch them; what it defends against is
|
|
74
|
+
* the tail — a generated file, or one cascading syntax error in a large partial.
|
|
75
|
+
*/
|
|
76
|
+
export declare const RESPONSE_TOKEN_BUDGET = 8000;
|
|
77
|
+
/**
|
|
78
|
+
* Bytes per token, for converting the budget above into something countable.
|
|
79
|
+
*
|
|
80
|
+
* A crude ESTIMATE — this JSON is mostly ASCII prose, where ~4 bytes/token is the usual
|
|
81
|
+
* rule of thumb, and the real tokenizer is the client's. Being wrong by 25% moves the cap
|
|
82
|
+
* between ~150 and ~250 diagnostics, which changes nothing about which calls it affects.
|
|
83
|
+
*/
|
|
84
|
+
export declare const BYTES_PER_TOKEN = 4;
|
|
85
|
+
/**
|
|
86
|
+
* The bound actually enforced: serialized bytes of diagnostics one response may
|
|
87
|
+
* carry, across the WHOLE request rather than per file.
|
|
88
|
+
*
|
|
89
|
+
* Per-request because per-file alone leaves the worst case multiplied by the file
|
|
90
|
+
* count — 50 files each just under a per-file cap is 50x the intended bound, and the
|
|
91
|
+
* batch form exists precisely so agents send many files at once.
|
|
92
|
+
*/
|
|
93
|
+
export declare const MAX_RESPONSE_DIAGNOSTIC_BYTES: number;
|
|
94
|
+
/**
|
|
95
|
+
* What one file costs in a response BEFORE any diagnostics: its status, gate, `impact`
|
|
96
|
+
* object, empty lists, and — when findings were withheld — the truncation note.
|
|
97
|
+
*
|
|
98
|
+
* Measured, not assumed: a 50-file batch of clean files serializes to 234 bytes per file,
|
|
99
|
+
* and the note adds ~150. Held at 512 because this multiplies by the file count.
|
|
100
|
+
*/
|
|
101
|
+
export declare const RESPONSE_ENVELOPE_BYTES_PER_FILE = 512;
|
|
102
|
+
/**
|
|
103
|
+
* The whole response bound, stated as arithmetic rather than enforced directly.
|
|
104
|
+
*
|
|
105
|
+
* The allocator bounds DIAGNOSTICS, the only unbounded dimension; the envelope is O(files)
|
|
106
|
+
* and already bounded by the batch file cap. `transport/validate-code.spec.ts` measures the
|
|
107
|
+
* worst legal request against this.
|
|
108
|
+
*
|
|
109
|
+
* Takes the file count rather than importing `MAX_BATCH_FILES`, which lives in
|
|
110
|
+
* `validate/batch-bounds.ts` and already imports this module.
|
|
111
|
+
*/
|
|
112
|
+
export declare function maxResponseBytes(fileCount: number): number;
|
|
113
|
+
/**
|
|
114
|
+
* How long to wait for a lint that was handed `bytes` of buffer.
|
|
115
|
+
*
|
|
116
|
+
* Never below {@link MIN_LINT_DEADLINE_MS}. Callers do not clamp to
|
|
117
|
+
* {@link MAX_LINT_DEADLINE_MS} — the byte caps do that upstream by refusing the request
|
|
118
|
+
* outright, so the ceiling is enforced where the work is admitted, not where it is timed.
|
|
119
|
+
*/
|
|
120
|
+
export declare function lintDeadlineMs(bytes: number): number;
|
|
121
|
+
/**
|
|
122
|
+
* The most bytes whose {@link lintDeadlineMs} still fits inside `deadlineMs`.
|
|
123
|
+
*
|
|
124
|
+
* The inverse of the function above, floored to a whole KiB so the answer is exact at the
|
|
125
|
+
* boundary — one byte more and the deadline exceeds `deadlineMs`. Used to derive the batch
|
|
126
|
+
* cap from the deadline ceiling instead of from a round multiple of the per-buffer bound.
|
|
127
|
+
*/
|
|
128
|
+
export declare function maxBytesWithin(deadlineMs: number): number;
|