@platformos/platformos-mcp-supervisor 0.1.0 → 0.2.1
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/CHANGELOG.md +1076 -0
- package/README.md +20 -5
- package/dist/bin/args.d.ts +15 -2
- package/dist/bin/args.js +31 -3
- package/dist/bin/platformos-mcp-supervisor.js +6 -2
- package/dist/context.d.ts +16 -8
- package/dist/context.js +7 -6
- package/dist/cost-model.d.ts +60 -0
- package/dist/cost-model.js +60 -0
- package/dist/enrich/enrich.d.ts +18 -0
- package/dist/enrich/enrich.js +39 -0
- package/dist/impact/dependants.d.ts +75 -0
- package/dist/impact/dependants.js +104 -0
- package/dist/impact/diff.d.ts +27 -0
- package/dist/impact/diff.js +33 -0
- package/dist/impact/impact.d.ts +17 -5
- package/dist/impact/impact.js +135 -164
- package/dist/impact/project-scan.d.ts +12 -10
- package/dist/impact/project-scan.js +12 -10
- package/dist/result/assemble.d.ts +1 -2
- package/dist/result/assemble.js +1 -2
- package/dist/result/blocking.d.ts +10 -4
- package/dist/result/blocking.js +23 -3
- package/dist/result/impact-states.d.ts +12 -9
- package/dist/result/impact-states.js +9 -17
- package/dist/result/response-budget.js +5 -3
- package/dist/result/types.d.ts +42 -40
- package/dist/transport/instructions.js +53 -86
- package/dist/transport/server.d.ts +5 -0
- package/dist/transport/server.js +8 -4
- package/dist/transport/validate-code.d.ts +23 -2
- package/dist/transport/validate-code.js +53 -18
- package/dist/validate/batch-bounds.js +5 -0
- package/dist/validate/validate-buffers.js +81 -27
- package/package.json +15 -14
|
@@ -17,11 +17,12 @@ import { enrichDiagnostics } from '../enrich/enrich.js';
|
|
|
17
17
|
import { lintDeadlineMs } from '../cost-model.js';
|
|
18
18
|
import { TIMED_OUT, withDeadline } from '../deadline.js';
|
|
19
19
|
import { runImpact } from '../impact/impact.js';
|
|
20
|
+
import { canHaveDependants } from '../impact/dependants.js';
|
|
20
21
|
import { createProjectScan } from '../impact/project-scan.js';
|
|
21
22
|
import { runBatchLint, } from '../lint/lint-batch.js';
|
|
22
23
|
import { assembleNotApplicableResult, assembleResult } from '../result/assemble.js';
|
|
23
24
|
import { capToBudget } from '../result/response-budget.js';
|
|
24
|
-
import { UNAVAILABLE_IMPACT } from '../result/impact-states.js';
|
|
25
|
+
import { DISABLED_IMPACT, UNAVAILABLE_IMPACT } from '../result/impact-states.js';
|
|
25
26
|
/** Read the process-wide docset and flatten it to the arrays enrichment takes. */
|
|
26
27
|
async function resolveDocset() {
|
|
27
28
|
const docset = getPlatformOSDocset();
|
|
@@ -65,11 +66,26 @@ export async function validateBuffers(ctx, buffers, overrides = {}) {
|
|
|
65
66
|
// Computed ONCE and threaded to both the timer and the message that reports it, so the
|
|
66
67
|
// two cannot disagree about the deadline a caller was held to.
|
|
67
68
|
const lintDeadline = lintDeadlineMs(admittedBytes(lintable));
|
|
68
|
-
//
|
|
69
|
-
|
|
69
|
+
// The project SCAN overlaps the primary lint; the impact LINT PASSES cannot.
|
|
70
|
+
//
|
|
71
|
+
// `lintBuffers` overlays buffers into check-node's process-shared `App` and reverts them
|
|
72
|
+
// on the way out, and that App has no lock — so two lint passes running at once would
|
|
73
|
+
// corrupt each other. The scan is pure filesystem I/O and touches no App, which is what
|
|
74
|
+
// lets the expensive half of impact hide behind the lint anyway.
|
|
75
|
+
const scan = projectScan(ctx, lintable);
|
|
76
|
+
const [lint] = await Promise.all([
|
|
70
77
|
lintWithDeadline(ctx, adapters, lintable, lintDeadline),
|
|
71
|
-
|
|
78
|
+
scan.warm(),
|
|
72
79
|
]);
|
|
80
|
+
// AFTER the primary lint, and never allowed to break it: the write gate is already
|
|
81
|
+
// decided by this point, so impact can only add to the answer or fail quietly.
|
|
82
|
+
//
|
|
83
|
+
// Turned off for this SERVER (`--no-impact`) it costs NOTHING: `projectScan` declines to
|
|
84
|
+
// read, and the two extra lint passes never start. `disabled` is its own status precisely
|
|
85
|
+
// so an agent does not read it as a failure worth retrying.
|
|
86
|
+
const impacts = ctx.impactEnabled === false
|
|
87
|
+
? new Map(lintable.map((buffer) => [buffer.filePath, DISABLED_IMPACT()]))
|
|
88
|
+
: await impactWithDeadline(ctx, adapters, lintable, scan.scan);
|
|
73
89
|
// The lint pass is what knows which buffers were not checked and why — it holds the
|
|
74
90
|
// config AND the classifier. Folding its answer in here keeps ONE source of truth for
|
|
75
91
|
// "is this file part of the app".
|
|
@@ -187,45 +203,83 @@ function admittedBytes(lintable) {
|
|
|
187
203
|
return lintable.reduce((total, buffer) => total + Buffer.byteLength(buffer.content, 'utf8'), 0);
|
|
188
204
|
}
|
|
189
205
|
/**
|
|
190
|
-
*
|
|
206
|
+
* Impact per lintable buffer.
|
|
191
207
|
*
|
|
192
|
-
* ONE scan serves the whole batch: `createProjectScan` reads the project's edge sources
|
|
193
|
-
*
|
|
194
|
-
*
|
|
208
|
+
* ONE scan serves the whole batch: `createProjectScan` reads the project's edge sources once,
|
|
209
|
+
* LAZILY and memoized, so a batch whose buffers can have no dependants never reads the project
|
|
210
|
+
* at all, and one where some can pays a single read. The batch's own buffers are overlaid into
|
|
211
|
+
* it, so a call a buffer has just added counts.
|
|
195
212
|
*
|
|
196
213
|
* The set shares one tight deadline because impact is discardable enrichment —
|
|
197
214
|
* `unavailable` already means "we don't know".
|
|
198
215
|
*/
|
|
199
|
-
async function impactWithDeadline(ctx, adapters, lintable) {
|
|
216
|
+
async function impactWithDeadline(ctx, adapters, lintable, scan) {
|
|
200
217
|
const byFile = new Map();
|
|
201
218
|
if (lintable.length === 0)
|
|
202
219
|
return byFile;
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
const work = adapters
|
|
221
|
+
.impact({
|
|
222
|
+
projectDir: ctx.projectDir,
|
|
223
|
+
buffers: lintable,
|
|
224
|
+
scan,
|
|
225
|
+
lint: adapters.lint,
|
|
226
|
+
docset: adapters.docset,
|
|
227
|
+
log: (message) => ctx.log(`validate_code: ${message}`),
|
|
228
|
+
})
|
|
229
|
+
.catch((error) => {
|
|
230
|
+
// ONE failure covers the whole changeset: the passes are shared, so a failure says
|
|
231
|
+
// nothing about any individual buffer.
|
|
232
|
+
ctx.log(`validate_code: impact failed: ${describe(error)}`);
|
|
233
|
+
return new Map();
|
|
234
|
+
});
|
|
217
235
|
const outcome = await withDeadline(work, IMPACT_DEADLINE_MS);
|
|
218
236
|
if (outcome === TIMED_OUT) {
|
|
219
|
-
observeAbandoned(ctx, work, '
|
|
220
|
-
ctx.log(`validate_code:
|
|
237
|
+
observeAbandoned(ctx, work, 'impact');
|
|
238
|
+
ctx.log(`validate_code: impact exceeded ${IMPACT_DEADLINE_MS} ms, continuing without it`);
|
|
221
239
|
for (const buffer of lintable)
|
|
222
240
|
byFile.set(buffer.filePath, UNAVAILABLE_IMPACT());
|
|
223
241
|
return byFile;
|
|
224
242
|
}
|
|
225
|
-
for
|
|
226
|
-
|
|
243
|
+
// A buffer the pass did not answer for gets `unavailable` rather than a silent absence:
|
|
244
|
+
// `resultFor` would otherwise default it, and "we did not look" must not read as "we
|
|
245
|
+
// looked and found nothing".
|
|
246
|
+
for (const buffer of lintable) {
|
|
247
|
+
byFile.set(buffer.filePath, outcome.get(buffer.filePath) ?? UNAVAILABLE_IMPACT());
|
|
248
|
+
}
|
|
227
249
|
return byFile;
|
|
228
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The request's project scan, plus a handle to start reading it.
|
|
253
|
+
*
|
|
254
|
+
* Split so the read can be STARTED alongside the primary lint and awaited later: it is the
|
|
255
|
+
* expensive half of impact (~235 ms on a 2,615-file project) and pure filesystem I/O, so
|
|
256
|
+
* overlapping it with the lint's CPU costs nothing and races nothing.
|
|
257
|
+
*/
|
|
258
|
+
function projectScan(ctx, lintable) {
|
|
259
|
+
const rootUri = pathUtils.normalize(pathUtils.toUri(ctx.projectDir));
|
|
260
|
+
const scan = createProjectScan(rootUri, NodeFileSystem, new Map(lintable.map((buffer) => [
|
|
261
|
+
pathUtils.normalize(pathUtils.toUri(toAbsoluteFilePath(ctx.projectDir, buffer.filePath))),
|
|
262
|
+
buffer.content,
|
|
263
|
+
])));
|
|
264
|
+
// Nothing in this changeset can HAVE dependants — every buffer is a YAML file, or sits in
|
|
265
|
+
// no platformOS directory — so impact will never consult the scan and reading the project
|
|
266
|
+
// would be pure waste. Decidable from the paths alone, before any I/O.
|
|
267
|
+
//
|
|
268
|
+
// A PERFORMANCE GUARD, NOT A CORRECTNESS ONE, which is why six mutants survive on it.
|
|
269
|
+
// `warm()` only PRE-warms: `scan.sources()` memoizes its promise and impact awaits that
|
|
270
|
+
// same promise itself, so inverting this buys a wasted read or a colder path, never a
|
|
271
|
+
// different answer. Nothing pins it — nor the `--no-impact` claim above, where what is
|
|
272
|
+
// tested is that the impact ADAPTER goes uncalled, not that the project read is skipped.
|
|
273
|
+
const worthReading = ctx.impactEnabled !== false &&
|
|
274
|
+
lintable.some((buffer) => canHaveDependants(pathUtils.normalize(pathUtils.toUri(toAbsoluteFilePath(ctx.projectDir, buffer.filePath))), rootUri));
|
|
275
|
+
return {
|
|
276
|
+
scan,
|
|
277
|
+
// A failed read is impact's problem, not the lint's: swallowed here so it cannot reject
|
|
278
|
+
// the `Promise.all` that the primary lint is riding in, and surfaced when impact awaits
|
|
279
|
+
// the same memoized promise itself.
|
|
280
|
+
warm: () => (worthReading ? scan.sources().catch(() => undefined) : Promise.resolve()),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
229
283
|
/**
|
|
230
284
|
* Attach a handler to work the deadline abandoned.
|
|
231
285
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformos/platformos-mcp-supervisor",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "platformOS Model Context Protocol (MCP) server exposing validate_code for LLM agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,9 +26,7 @@
|
|
|
26
26
|
"access": "public",
|
|
27
27
|
"@platformos:registry": "https://registry.npmjs.org"
|
|
28
28
|
},
|
|
29
|
-
"bin":
|
|
30
|
-
"platformos-mcp-supervisor": "dist/bin/platformos-mcp-supervisor.js"
|
|
31
|
-
},
|
|
29
|
+
"bin": "dist/bin/platformos-mcp-supervisor.js",
|
|
32
30
|
"files": [
|
|
33
31
|
"dist/**/*.js",
|
|
34
32
|
"dist/**/*.d.ts"
|
|
@@ -42,14 +40,17 @@
|
|
|
42
40
|
"type-check": "tsc --noEmit"
|
|
43
41
|
},
|
|
44
42
|
"dependencies": {
|
|
45
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
46
|
-
"@platformos/liquid-html-parser": "0.
|
|
47
|
-
"@platformos/platformos-check-common": "1.
|
|
48
|
-
"@platformos/platformos-check-node": "1.
|
|
49
|
-
"@platformos/platformos-common": "0.
|
|
50
|
-
"@platformos/platformos-graph": "0.1
|
|
51
|
-
"@platformos/platformos-language-server-common": "0.1.
|
|
52
|
-
"zod": "^
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
44
|
+
"@platformos/liquid-html-parser": "0.2.0",
|
|
45
|
+
"@platformos/platformos-check-common": "1.2.0",
|
|
46
|
+
"@platformos/platformos-check-node": "1.2.0",
|
|
47
|
+
"@platformos/platformos-common": "0.2.0",
|
|
48
|
+
"@platformos/platformos-graph": "0.2.1",
|
|
49
|
+
"@platformos/platformos-language-server-common": "0.1.2",
|
|
50
|
+
"zod": "^4.6.7"
|
|
53
51
|
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"typescript": "^5.9.0",
|
|
54
|
+
"vitest": "^4.0.18"
|
|
55
|
+
}
|
|
56
|
+
}
|