@platformos/platformos-mcp-supervisor 0.0.2 → 0.2.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 +105 -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.d.ts +15 -2
- package/dist/bin/args.js +31 -3
- package/dist/bin/args.spec.d.ts +1 -0
- package/dist/bin/args.spec.js +35 -0
- package/dist/bin/platformos-mcp-supervisor.js +6 -2
- 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 +34 -0
- package/dist/context.js +12 -0
- package/dist/cost-model.d.ts +188 -0
- package/dist/cost-model.js +197 -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 +105 -0
- package/dist/enrich/enrich.js +136 -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/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 +21 -0
- package/dist/impact/impact.js +160 -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 +39 -0
- package/dist/impact/project-scan.js +62 -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 +11 -9
- package/dist/result/assemble.js +63 -16
- package/dist/result/assemble.spec.d.ts +1 -0
- package/dist/result/assemble.spec.js +319 -0
- package/dist/result/blocking.d.ts +92 -0
- package/dist/result/blocking.js +116 -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 +19 -0
- package/dist/result/impact-states.js +10 -0
- package/dist/result/response-budget.d.ts +9 -0
- package/dist/result/response-budget.js +133 -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 +212 -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 +150 -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 +6 -1
- package/dist/transport/server.js +21 -10
- package/dist/transport/validate-code.d.ts +50 -15
- package/dist/transport/validate-code.js +238 -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 +65 -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 +319 -0
- package/package.json +8 -7
|
@@ -1,49 +1,254 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Registration of the `validate_code` MCP tool.
|
|
2
|
+
* Registration of the `validate_code` MCP tool — the supervisor's entire surface.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* ONE TOOL, TWO INPUT SHAPES. Validating one file and validating a coordinated multi-file
|
|
5
|
+
* change are the same operation on a different number of buffers, so they are one tool:
|
|
6
|
+
*
|
|
7
|
+
* validate_code({ file_path, content }) -> a single result
|
|
8
|
+
* validate_code({ files: [...] }) -> one result per file + a batch gate
|
|
9
|
+
*
|
|
10
|
+
* There is no `many` flag — which form was sent is evident from the arguments.
|
|
11
|
+
*
|
|
12
|
+
* PASSING SEVERAL FILES IS MORE ACCURATE, not just faster: with every buffer overlaid at
|
|
13
|
+
* once a partial created alongside its caller resolves, where one at a time it is reported
|
|
14
|
+
* missing. The description says so, because that is why an agent should reach for it.
|
|
15
|
+
*
|
|
16
|
+
* This module owns only the MCP surface: schema, description, registration, and mapping
|
|
17
|
+
* input shape to output shape. All logic lives in `validate/`.
|
|
8
18
|
*/
|
|
9
19
|
import { z } from 'zod';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
20
|
+
import { getAppPaths, PlatformOSFileType } from '@platformos/platformos-common';
|
|
21
|
+
import { validateBuffers, } from '../validate/validate-buffers.js';
|
|
22
|
+
import { MAX_BATCH_BYTES, MAX_BATCH_FILES, batchTooLarge } from '../validate/batch-bounds.js';
|
|
23
|
+
import { collidingBufferPaths } from '../validate/batch-coherence.js';
|
|
24
|
+
import { assembleNotApplicableResult } from '../result/assemble.js';
|
|
25
|
+
/**
|
|
26
|
+
* A non-blank path. Blank is malformed input, refused at the protocol boundary.
|
|
27
|
+
*
|
|
28
|
+
* BOTH halves are load-bearing and neither is redundant. The refinement is what
|
|
29
|
+
* refuses a whitespace-only path; `.min(1)` is the half that SURVIVES conversion to
|
|
30
|
+
* JSON Schema, as `minLength`. A refinement converts to nothing at all, so with the
|
|
31
|
+
* refinement alone the rule exists only on this side of the wire — the model is
|
|
32
|
+
* never told, sends `""`, and learns the rule from a rejection. Drop either half and
|
|
33
|
+
* a test fails: the emitted schema loses `minLength`, or `' '` starts being accepted.
|
|
34
|
+
*
|
|
35
|
+
* A FUNCTION, not a shared constant, for the same wire-level reason: the SDK's
|
|
36
|
+
* converter dedupes by schema IDENTITY, so one instance reused across both input
|
|
37
|
+
* forms emits `{ $ref: '#/properties/file_path' }` for the second use. That drags the
|
|
38
|
+
* single-file description into the batch entries, where "Pair with `content`" is
|
|
39
|
+
* wrong, and puts a `$ref` in a tool inputSchema — which not every MCP client
|
|
40
|
+
* resolves. A fresh instance per use costs nothing and emits in full.
|
|
41
|
+
*/
|
|
42
|
+
const filePath = () => z
|
|
43
|
+
.string()
|
|
44
|
+
.min(1)
|
|
45
|
+
.refine((value) => value.trim().length > 0, { message: 'file_path must not be empty' });
|
|
46
|
+
/** File contents. Empty is legal — an empty file is a file. Fresh per use, as above. */
|
|
47
|
+
const content = () => z.string();
|
|
48
|
+
/**
|
|
49
|
+
* An example path of each kind, for the agent-facing prose below.
|
|
50
|
+
*
|
|
51
|
+
* DERIVED from `platformos-common`'s tables, not spelled out: an agent imitates these
|
|
52
|
+
* strings when it invents a path, so an example naming a directory the platform stopped
|
|
53
|
+
* accepting teaches the mistake to every caller.
|
|
54
|
+
*
|
|
55
|
+
* The first entry of `getAppPaths` is the canonical directory for the type; the rest are
|
|
56
|
+
* legacy aliases, which are exactly what an example should not suggest.
|
|
57
|
+
*/
|
|
58
|
+
const exampleOf = (type, name) => `${getAppPaths(type)[0]}/${name}.liquid`;
|
|
59
|
+
const EXAMPLE_PARTIAL = exampleOf(PlatformOSFileType.Partial, 'card');
|
|
60
|
+
const EXAMPLE_PAGE = exampleOf(PlatformOSFileType.Page, 'home');
|
|
61
|
+
const EXAMPLE_PROMO = exampleOf(PlatformOSFileType.Partial, 'promo');
|
|
12
62
|
/**
|
|
13
63
|
* zod raw shape for the tool input (validated by the MCP SDK before dispatch).
|
|
14
64
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
65
|
+
* Both forms are optional at the schema level and the handler requires exactly one,
|
|
66
|
+
* because a zod union of two object shapes defeats the SDK's JSON-Schema conversion
|
|
67
|
+
* — it emits `anyOf`, which several MCP clients do not surface to the model at all.
|
|
68
|
+
* Two optional fields with a documented rule converts cleanly and reads clearly.
|
|
69
|
+
*
|
|
70
|
+
* WHAT THE MODEL SEES IS THE CONVERSION, NOT THIS. Everything an agent knows about
|
|
71
|
+
* the arguments arrives as the JSON Schema this shape converts to, so a constraint
|
|
72
|
+
* that does not convert may as well not be advertised at all. Prefer a zod builtin
|
|
73
|
+
* over a `.refine` wherever one expresses the rule; `transport/validate-code.spec.ts`
|
|
74
|
+
* pins the emitted document end to end.
|
|
75
|
+
*
|
|
76
|
+
* Typed as `ZodRawShape` (not the inferred literal shape) so `registerTool` does not
|
|
77
|
+
* instantiate excessively deeply over the schema (TS2589 under zod 3.25).
|
|
18
78
|
*/
|
|
19
79
|
export const VALIDATE_CODE_INPUT = {
|
|
20
|
-
file_path:
|
|
21
|
-
.
|
|
22
|
-
.describe('Path of the file
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.enum(['full', 'quick'])
|
|
80
|
+
file_path: filePath()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Path of the file to validate, absolute or relative to the project root ' +
|
|
83
|
+
`(e.g. "${EXAMPLE_PARTIAL}"). Pair with \`content\`.`),
|
|
84
|
+
content: content()
|
|
26
85
|
.optional()
|
|
27
|
-
.describe('
|
|
86
|
+
.describe('The exact file contents you are about to write. Pair with `file_path`.'),
|
|
87
|
+
files: z
|
|
88
|
+
.array(z.object({
|
|
89
|
+
file_path: filePath().describe('Path of THIS file, absolute or relative to the project root.'),
|
|
90
|
+
content: content().describe('The exact contents you are about to write to THIS file.'),
|
|
91
|
+
}))
|
|
92
|
+
.min(1)
|
|
93
|
+
.max(MAX_BATCH_FILES)
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('Two or more files to validate together, each with its own `file_path` and ' +
|
|
96
|
+
'`content`. Files in one call can reference each other.'),
|
|
28
97
|
};
|
|
29
|
-
const DESCRIPTION =
|
|
30
|
-
|
|
98
|
+
const DESCRIPTION = `Validate platformOS Liquid, GraphQL and YAML code BEFORE writing it to disk.
|
|
99
|
+
|
|
100
|
+
Pass EITHER one file OR several — never both.
|
|
101
|
+
|
|
102
|
+
One file:
|
|
103
|
+
{ "file_path": "${EXAMPLE_PARTIAL}",
|
|
104
|
+
"content": "<div>{{ title }}</div>" }
|
|
105
|
+
|
|
106
|
+
Several files (use this whenever one change touches more than one file):
|
|
107
|
+
{ "files": [
|
|
108
|
+
{ "file_path": "${EXAMPLE_PAGE}",
|
|
109
|
+
"content": "{% render 'promo' %}" },
|
|
110
|
+
{ "file_path": "${EXAMPLE_PROMO}",
|
|
111
|
+
"content": "<div>Promo</div>" }
|
|
112
|
+
] }
|
|
113
|
+
|
|
114
|
+
Sending several together is faster AND more accurate: they can reference each
|
|
115
|
+
other, so the new partial above resolves. Validated one at a time, the page would
|
|
116
|
+
be reported as rendering a missing partial. List each file at most once — the same
|
|
117
|
+
file twice is refused, because only one of the two could be checked.
|
|
118
|
+
|
|
119
|
+
Returns, for one file:
|
|
120
|
+
{ "status": "ok" | "warning" | "error" | "not_applicable",
|
|
121
|
+
"must_fix_before_write": boolean,
|
|
122
|
+
"errors": [...], "warnings": [...], "infos": [...],
|
|
123
|
+
"impact": { ... } }
|
|
124
|
+
|
|
125
|
+
For several files:
|
|
126
|
+
{ "must_fix_before_write": boolean,
|
|
127
|
+
"files": [ { "file_path": "...", "result": { ...as above... } } ] }
|
|
128
|
+
|
|
129
|
+
must_fix_before_write true means the file will not work — do not write it. false
|
|
130
|
+
means nothing fatal was found, which is not a guarantee of correctness.
|
|
131
|
+
status "not_applicable" means the file was NOT checked (see not_applicable_reason);
|
|
132
|
+
that is neither approval nor refusal.
|
|
133
|
+
A "truncated" field appears only when a file produced too many findings to return
|
|
134
|
+
in full; it carries the true total per list. The lists keep the top of the file,
|
|
135
|
+
and the verdict above is always computed from ALL findings, not the shortened list.`;
|
|
136
|
+
/**
|
|
137
|
+
* The agent-facing prose, exported so tests can pin claims that must not rot. The
|
|
138
|
+
* description is the agent's whole understanding of this tool; a stale sentence
|
|
139
|
+
* here misleads silently.
|
|
140
|
+
*/
|
|
141
|
+
export const TOOL_TEXT = { VALIDATE_CODE_DESCRIPTION: DESCRIPTION };
|
|
31
142
|
export function registerValidateCode(server, ctx) {
|
|
32
|
-
server.registerTool('validate_code', { description: DESCRIPTION, inputSchema: VALIDATE_CODE_INPUT }, async (args) =>
|
|
143
|
+
server.registerTool('validate_code', { description: DESCRIPTION, inputSchema: VALIDATE_CODE_INPUT }, async (args) => ({
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: 'text',
|
|
147
|
+
text: JSON.stringify(await runValidateCode(ctx, args)),
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
}));
|
|
33
151
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
152
|
+
/**
|
|
153
|
+
* The tool handler: normalize the input shape, validate, and shape the response to
|
|
154
|
+
* match what was asked.
|
|
155
|
+
*
|
|
156
|
+
* Returns the flat single-file result for the single form and the per-file envelope
|
|
157
|
+
* for the batch form — the response mirrors the request, so a caller never parses a
|
|
158
|
+
* shape it did not ask for.
|
|
159
|
+
*/
|
|
160
|
+
export async function runValidateCode(ctx, params, overrides = {}) {
|
|
161
|
+
const requested = requestedBuffers(params);
|
|
162
|
+
if ('error' in requested) {
|
|
163
|
+
ctx.log(`validate_code: rejected — ${requested.error.reason}`);
|
|
164
|
+
return assembleNotApplicableResult(requested.error);
|
|
165
|
+
}
|
|
166
|
+
const { buffers, isBatch } = requested;
|
|
167
|
+
ctx.log(isBatch ? `validate_code: ${buffers.length} file(s)` : `validate_code: ${buffers[0].filePath}`);
|
|
168
|
+
// Request-level refusals, decided before any work starts. First refusal wins; the cheaper
|
|
169
|
+
// check runs first.
|
|
170
|
+
//
|
|
171
|
+
// GATED ON MORE THAN ONE BUFFER, not on which form carried it. One file is one file, and
|
|
172
|
+
// the two forms must answer identically — the shape an agent happened to reach for cannot
|
|
173
|
+
// change a verdict. Both refusals ask a question a lone buffer cannot answer YES to:
|
|
174
|
+
// collision needs two entries naming one file, and the caps exist because a batch can hand
|
|
175
|
+
// the server work no single buffer could. Collision was already a no-op at one buffer; the
|
|
176
|
+
// total-byte cap was NOT, and it fired FIRST — so a 300 KiB file sent as a one-file batch
|
|
177
|
+
// was refused with "split it into smaller batches", advice its caller cannot follow, and
|
|
178
|
+
// was never told MAX_BUFFER_BYTES, the bound it actually had to get under. Whether ONE
|
|
179
|
+
// buffer is admissible is `bufferTooLarge`'s question, asked per buffer inside
|
|
180
|
+
// `validateBuffers` for both forms alike.
|
|
181
|
+
const refusal = buffers.length > 1
|
|
182
|
+
? (batchTooLarge(buffers) ?? collidingBufferPaths(ctx.projectDir, buffers))
|
|
183
|
+
: undefined;
|
|
184
|
+
if (refusal) {
|
|
185
|
+
ctx.log(`validate_code: refused (${refusal.code}) — ${refusal.reason}`);
|
|
186
|
+
return {
|
|
187
|
+
must_fix_before_write: false,
|
|
188
|
+
files: buffers.map((buffer) => ({
|
|
189
|
+
file_path: buffer.filePath,
|
|
190
|
+
result: assembleNotApplicableResult(refusal),
|
|
191
|
+
})),
|
|
192
|
+
next_step: refusal.reason,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const byFile = await validateBuffers(ctx, buffers, overrides);
|
|
196
|
+
if (!isBatch) {
|
|
197
|
+
// `validateBuffers` seeds an entry per requested buffer, so this always hits.
|
|
198
|
+
return byFile.get(buffers[0].filePath);
|
|
199
|
+
}
|
|
200
|
+
const files = buffers.map((buffer) => ({
|
|
201
|
+
file_path: buffer.filePath,
|
|
202
|
+
result: byFile.get(buffer.filePath),
|
|
203
|
+
}));
|
|
204
|
+
return {
|
|
205
|
+
// A file that was merely NOT CHECKED must never block the changeset: this is the
|
|
206
|
+
// OR of the files' own gates, and a `not_applicable` file's gate is false.
|
|
207
|
+
must_fix_before_write: files.some((entry) => entry.result.must_fix_before_write),
|
|
208
|
+
files,
|
|
209
|
+
};
|
|
44
210
|
}
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Read the two accepted input forms into the one internal shape.
|
|
213
|
+
*
|
|
214
|
+
* The SDK validates types; what it cannot express is "exactly one form". Both failures are
|
|
215
|
+
* the caller's malformed request rather than a property of any file, and are reported as
|
|
216
|
+
* such — silently preferring one form would validate something the caller did not ask about.
|
|
217
|
+
*/
|
|
218
|
+
function requestedBuffers(params) {
|
|
219
|
+
const hasSingle = params.file_path !== undefined || params.content !== undefined;
|
|
220
|
+
const hasBatch = params.files !== undefined && params.files.length > 0;
|
|
221
|
+
if (hasSingle && hasBatch) {
|
|
222
|
+
return {
|
|
223
|
+
error: {
|
|
224
|
+
code: 'internal_error',
|
|
225
|
+
reason: 'Both `files` and `file_path`/`content` were provided. Send ONE file as ' +
|
|
226
|
+
'`file_path` + `content`, or several as `files` — not both. Nothing was checked.',
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (hasBatch) {
|
|
231
|
+
return {
|
|
232
|
+
buffers: params.files.map((file) => ({
|
|
233
|
+
filePath: file.file_path,
|
|
234
|
+
content: file.content,
|
|
235
|
+
})),
|
|
236
|
+
isBatch: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (params.file_path === undefined || params.content === undefined) {
|
|
240
|
+
return {
|
|
241
|
+
error: {
|
|
242
|
+
code: 'internal_error',
|
|
243
|
+
reason: 'No file to validate. Send one file as `file_path` + `content`, or several as ' +
|
|
244
|
+
'`files`. Nothing was checked.',
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
buffers: [{ filePath: params.file_path, content: params.content }],
|
|
250
|
+
isBatch: false,
|
|
251
|
+
};
|
|
48
252
|
}
|
|
253
|
+
export { MAX_BATCH_BYTES, MAX_BATCH_FILES };
|
|
49
254
|
//# sourceMappingURL=validate-code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|