@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.
Files changed (85) hide show
  1. package/README.md +90 -0
  2. package/dist/adapter-input.d.ts +113 -0
  3. package/dist/adapter-input.js +210 -0
  4. package/dist/adapter-input.spec.d.ts +1 -0
  5. package/dist/adapter-input.spec.js +275 -0
  6. package/dist/bin/args.spec.d.ts +1 -0
  7. package/dist/bin/args.spec.js +35 -0
  8. package/dist/check-docs.d.ts +21 -0
  9. package/dist/check-docs.js +38 -0
  10. package/dist/check-docs.spec.d.ts +1 -0
  11. package/dist/check-docs.spec.js +87 -0
  12. package/dist/context.d.ts +26 -0
  13. package/dist/context.js +11 -0
  14. package/dist/cost-model.d.ts +128 -0
  15. package/dist/cost-model.js +137 -0
  16. package/dist/cost-model.spec.d.ts +1 -0
  17. package/dist/cost-model.spec.js +91 -0
  18. package/dist/deadline.d.ts +27 -0
  19. package/dist/deadline.js +40 -0
  20. package/dist/deadline.spec.d.ts +1 -0
  21. package/dist/deadline.spec.js +83 -0
  22. package/dist/enrich/enrich.d.ts +87 -0
  23. package/dist/enrich/enrich.js +97 -0
  24. package/dist/enrich/enrich.spec.d.ts +1 -0
  25. package/dist/enrich/enrich.spec.js +184 -0
  26. package/dist/enrich/symbol.d.ts +16 -0
  27. package/dist/enrich/symbol.js +58 -0
  28. package/dist/enrich/symbol.spec.d.ts +1 -0
  29. package/dist/enrich/symbol.spec.js +89 -0
  30. package/dist/impact/impact.d.ts +9 -0
  31. package/dist/impact/impact.js +189 -0
  32. package/dist/impact/impact.spec.d.ts +1 -0
  33. package/dist/impact/impact.spec.js +357 -0
  34. package/dist/impact/project-scan.d.ts +37 -0
  35. package/dist/impact/project-scan.js +60 -0
  36. package/dist/impact/project-scan.spec.d.ts +1 -0
  37. package/dist/impact/project-scan.spec.js +121 -0
  38. package/dist/index.d.ts +1 -1
  39. package/dist/lint/fixes.d.ts +28 -0
  40. package/dist/lint/fixes.js +30 -0
  41. package/dist/lint/lint-batch.d.ts +100 -0
  42. package/dist/lint/lint-batch.js +98 -0
  43. package/dist/lint/lint-batch.spec.d.ts +1 -0
  44. package/dist/lint/lint-batch.spec.js +503 -0
  45. package/dist/lint/lint.d.ts +11 -1
  46. package/dist/lint/lint.js +9 -3
  47. package/dist/result/assemble.d.ts +12 -9
  48. package/dist/result/assemble.js +64 -16
  49. package/dist/result/assemble.spec.d.ts +1 -0
  50. package/dist/result/assemble.spec.js +319 -0
  51. package/dist/result/blocking.d.ts +86 -0
  52. package/dist/result/blocking.js +96 -0
  53. package/dist/result/blocking.spec.d.ts +1 -0
  54. package/dist/result/blocking.spec.js +201 -0
  55. package/dist/result/impact-states.d.ts +16 -0
  56. package/dist/result/impact-states.js +18 -0
  57. package/dist/result/response-budget.d.ts +9 -0
  58. package/dist/result/response-budget.js +131 -0
  59. package/dist/result/response-budget.spec.d.ts +1 -0
  60. package/dist/result/response-budget.spec.js +222 -0
  61. package/dist/result/types.d.ts +210 -104
  62. package/dist/result/types.js +4 -13
  63. package/dist/transport/instructions-coverage.spec.d.ts +1 -0
  64. package/dist/transport/instructions-coverage.spec.js +176 -0
  65. package/dist/transport/instructions.d.ts +1 -0
  66. package/dist/transport/instructions.js +132 -0
  67. package/dist/transport/process-guards.d.ts +43 -0
  68. package/dist/transport/process-guards.js +63 -0
  69. package/dist/transport/process-guards.spec.d.ts +1 -0
  70. package/dist/transport/process-guards.spec.js +200 -0
  71. package/dist/transport/server.d.ts +1 -1
  72. package/dist/transport/server.js +16 -9
  73. package/dist/transport/validate-code.d.ts +36 -18
  74. package/dist/transport/validate-code.js +200 -33
  75. package/dist/transport/validate-code.spec.d.ts +1 -0
  76. package/dist/transport/validate-code.spec.js +2303 -0
  77. package/dist/validate/batch-bounds.d.ts +34 -0
  78. package/dist/validate/batch-bounds.js +60 -0
  79. package/dist/validate/batch-coherence.d.ts +21 -0
  80. package/dist/validate/batch-coherence.js +54 -0
  81. package/dist/validate/batch-coherence.spec.d.ts +1 -0
  82. package/dist/validate/batch-coherence.spec.js +111 -0
  83. package/dist/validate/validate-buffers.d.ts +27 -0
  84. package/dist/validate/validate-buffers.js +265 -0
  85. package/package.json +8 -7
@@ -1,49 +1,216 @@
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
- * Current slice: the handler runs the real lint (`check()` via the check-node
5
- * seam) and returns the detection results mapped into `ValidateCodeResult`.
6
- * The ergonomic stages (enrich → advise → richer result assembly) are layered
7
- * in by later tasks; the handler shape stays the same.
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 { runLint } from '../lint/lint.js';
11
- import { assembleResult } from '../result/assemble.js';
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
+ /** A non-blank path. Blank is malformed input, refused at the protocol boundary. */
26
+ const filePath = z
27
+ .string()
28
+ .refine((value) => value.trim().length > 0, { message: 'file_path must not be empty' });
29
+ const content = z.string();
12
30
  /**
13
31
  * zod raw shape for the tool input (validated by the MCP SDK before dispatch).
14
32
  *
15
- * Typed as `ZodRawShape` (not the inferred literal shape) so the SDK's
16
- * `registerTool` does not perform excessively deep type instantiation over the
17
- * schema; the handler casts the validated args to `ValidateCodeParams`.
33
+ * Both forms are optional at the schema level and the handler requires exactly one,
34
+ * because a zod union of two object shapes defeats the SDK's JSON-Schema conversion
35
+ * — it emits `anyOf`, which several MCP clients do not surface to the model at all.
36
+ * Two optional fields with a documented rule converts cleanly and reads clearly.
37
+ *
38
+ * Typed as `ZodRawShape` (not the inferred literal shape) so `registerTool` does not
39
+ * instantiate excessively deeply over the schema (TS2589 under zod 3.25).
18
40
  */
41
+ /**
42
+ * An example path of each kind, for the agent-facing prose below.
43
+ *
44
+ * DERIVED from `platformos-common`'s tables, not spelled out: an agent imitates these
45
+ * strings when it invents a path, so an example naming a directory the platform stopped
46
+ * accepting teaches the mistake to every caller.
47
+ *
48
+ * The first entry of `getAppPaths` is the canonical directory for the type; the rest are
49
+ * legacy aliases, which are exactly what an example should not suggest.
50
+ */
51
+ const exampleOf = (type, name) => `${getAppPaths(type)[0]}/${name}.liquid`;
52
+ const EXAMPLE_PARTIAL = exampleOf(PlatformOSFileType.Partial, 'card');
53
+ const EXAMPLE_PAGE = exampleOf(PlatformOSFileType.Page, 'home');
54
+ const EXAMPLE_PROMO = exampleOf(PlatformOSFileType.Partial, 'promo');
19
55
  export const VALIDATE_CODE_INPUT = {
20
- file_path: z
21
- .string()
22
- .describe('Path of the file under edit (absolute, or relative to the project root).'),
23
- content: z.string().describe('The file contents to validate (the in-memory buffer).'),
24
- mode: z
25
- .enum(['full', 'quick'])
56
+ file_path: filePath
57
+ .optional()
58
+ .describe('Path of the file to validate, absolute or relative to the project root ' +
59
+ `(e.g. "${EXAMPLE_PARTIAL}"). Pair with \`content\`.`),
60
+ content: content
26
61
  .optional()
27
- .describe('Analysis depth. `full` (default) runs the heavier ergonomic stages; `quick` is lint + enrichment.'),
62
+ .describe('The exact file contents you are about to write. Pair with `file_path`.'),
63
+ files: z
64
+ .array(z.object({ file_path: filePath, content }))
65
+ .min(1)
66
+ .max(MAX_BATCH_FILES)
67
+ .optional()
68
+ .describe('Two or more files to validate together, each with its own `file_path` and ' +
69
+ '`content`. Files in one call can reference each other.'),
28
70
  };
29
- const DESCRIPTION = 'Validate a platformOS Liquid/GraphQL/YAML file before writing it. Returns structured errors, ' +
30
- 'warnings, infos, proposed fixes, and a must_fix_before_write gate.';
71
+ const DESCRIPTION = `Validate platformOS Liquid, GraphQL and YAML code BEFORE writing it to disk.
72
+
73
+ Pass EITHER one file OR several — never both.
74
+
75
+ One file:
76
+ { "file_path": "${EXAMPLE_PARTIAL}",
77
+ "content": "<div>{{ title }}</div>" }
78
+
79
+ Several files (use this whenever one change touches more than one file):
80
+ { "files": [
81
+ { "file_path": "${EXAMPLE_PAGE}",
82
+ "content": "{% render 'promo' %}" },
83
+ { "file_path": "${EXAMPLE_PROMO}",
84
+ "content": "<div>Promo</div>" }
85
+ ] }
86
+
87
+ Sending several together is faster AND more accurate: they can reference each
88
+ other, so the new partial above resolves. Validated one at a time, the page would
89
+ be reported as rendering a missing partial. List each file at most once — the same
90
+ file twice is refused, because only one of the two could be checked.
91
+
92
+ Returns, for one file:
93
+ { "status": "ok" | "warning" | "error" | "not_applicable",
94
+ "must_fix_before_write": boolean,
95
+ "errors": [...], "warnings": [...], "infos": [...],
96
+ "impact": { ... } }
97
+
98
+ For several files:
99
+ { "must_fix_before_write": boolean,
100
+ "files": [ { "file_path": "...", "result": { ...as above... } } ] }
101
+
102
+ must_fix_before_write true means the file will not work — do not write it. false
103
+ means nothing fatal was found, which is not a guarantee of correctness.
104
+ status "not_applicable" means the file was NOT checked (see not_applicable_reason);
105
+ that is neither approval nor refusal.
106
+ A "truncated" field appears only when a file produced too many findings to return
107
+ in full; it carries the true total per list. The lists keep the top of the file,
108
+ and the verdict above is always computed from ALL findings, not the shortened list.`;
109
+ /**
110
+ * The agent-facing prose, exported so tests can pin claims that must not rot. The
111
+ * description is the agent's whole understanding of this tool; a stale sentence
112
+ * here misleads silently.
113
+ */
114
+ export const TOOL_TEXT = { VALIDATE_CODE_DESCRIPTION: DESCRIPTION };
31
115
  export function registerValidateCode(server, ctx) {
32
- server.registerTool('validate_code', { description: DESCRIPTION, inputSchema: VALIDATE_CODE_INPUT }, async (args) => toToolResult(await runValidateCode(ctx, args)));
116
+ server.registerTool('validate_code', { description: DESCRIPTION, inputSchema: VALIDATE_CODE_INPUT }, async (args) => ({
117
+ content: [
118
+ {
119
+ type: 'text',
120
+ text: JSON.stringify(await runValidateCode(ctx, args)),
121
+ },
122
+ ],
123
+ }));
33
124
  }
34
- /** Lint the buffer via the check-node seam and assemble the result. */
35
- async function runValidateCode(ctx, params) {
36
- const mode = params.mode ?? 'full';
37
- ctx.log(`validate_code: ${params.file_path} (${mode})`);
38
- const diagnostics = await runLint({
39
- projectDir: ctx.projectDir,
40
- filePath: params.file_path,
41
- content: params.content,
42
- });
43
- return assembleResult(diagnostics, mode);
125
+ /**
126
+ * The tool handler: normalize the input shape, validate, and shape the response to
127
+ * match what was asked.
128
+ *
129
+ * Returns the flat single-file result for the single form and the per-file envelope
130
+ * for the batch form — the response mirrors the request, so a caller never parses a
131
+ * shape it did not ask for.
132
+ */
133
+ export async function runValidateCode(ctx, params, overrides = {}) {
134
+ const requested = requestedBuffers(params);
135
+ if ('error' in requested) {
136
+ ctx.log(`validate_code: rejected — ${requested.error.reason}`);
137
+ return assembleNotApplicableResult(requested.error);
138
+ }
139
+ const { buffers, isBatch } = requested;
140
+ ctx.log(isBatch ? `validate_code: ${buffers.length} file(s)` : `validate_code: ${buffers[0].filePath}`);
141
+ // Both refusals are request-level and impossible with a single buffer, so they are
142
+ // decided here, before any work starts. First refusal wins; the cheaper check runs first.
143
+ const refusal = isBatch
144
+ ? (batchTooLarge(buffers) ?? collidingBufferPaths(ctx.projectDir, buffers))
145
+ : undefined;
146
+ if (refusal) {
147
+ ctx.log(`validate_code: refused (${refusal.code}) — ${refusal.reason}`);
148
+ return {
149
+ must_fix_before_write: false,
150
+ files: buffers.map((buffer) => ({
151
+ file_path: buffer.filePath,
152
+ result: assembleNotApplicableResult(refusal),
153
+ })),
154
+ next_step: refusal.reason,
155
+ };
156
+ }
157
+ const byFile = await validateBuffers(ctx, buffers, overrides);
158
+ if (!isBatch) {
159
+ // `validateBuffers` seeds an entry per requested buffer, so this always hits.
160
+ return byFile.get(buffers[0].filePath);
161
+ }
162
+ const files = buffers.map((buffer) => ({
163
+ file_path: buffer.filePath,
164
+ result: byFile.get(buffer.filePath),
165
+ }));
166
+ return {
167
+ // A file that was merely NOT CHECKED must never block the changeset: this is the
168
+ // OR of the files' own gates, and a `not_applicable` file's gate is false.
169
+ must_fix_before_write: files.some((entry) => entry.result.must_fix_before_write),
170
+ files,
171
+ };
44
172
  }
45
- /** Wrap a result in the MCP text-content envelope (every result is one JSON text block). */
46
- function toToolResult(result) {
47
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
173
+ /**
174
+ * Read the two accepted input forms into the one internal shape.
175
+ *
176
+ * The SDK validates types; what it cannot express is "exactly one form". Both failures are
177
+ * the caller's malformed request rather than a property of any file, and are reported as
178
+ * such — silently preferring one form would validate something the caller did not ask about.
179
+ */
180
+ function requestedBuffers(params) {
181
+ const hasSingle = params.file_path !== undefined || params.content !== undefined;
182
+ const hasBatch = params.files !== undefined && params.files.length > 0;
183
+ if (hasSingle && hasBatch) {
184
+ return {
185
+ error: {
186
+ code: 'internal_error',
187
+ reason: 'Both `files` and `file_path`/`content` were provided. Send ONE file as ' +
188
+ '`file_path` + `content`, or several as `files` — not both. Nothing was checked.',
189
+ },
190
+ };
191
+ }
192
+ if (hasBatch) {
193
+ return {
194
+ buffers: params.files.map((file) => ({
195
+ filePath: file.file_path,
196
+ content: file.content,
197
+ })),
198
+ isBatch: true,
199
+ };
200
+ }
201
+ if (params.file_path === undefined || params.content === undefined) {
202
+ return {
203
+ error: {
204
+ code: 'internal_error',
205
+ reason: 'No file to validate. Send one file as `file_path` + `content`, or several as ' +
206
+ '`files`. Nothing was checked.',
207
+ },
208
+ };
209
+ }
210
+ return {
211
+ buffers: [{ filePath: params.file_path, content: params.content }],
212
+ isBatch: false,
213
+ };
48
214
  }
215
+ export { MAX_BATCH_BYTES, MAX_BATCH_FILES };
49
216
  //# sourceMappingURL=validate-code.js.map
@@ -0,0 +1 @@
1
+ export {};