@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
@@ -0,0 +1,319 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { assembleNotApplicableResult, assembleResult } from './assemble.js';
3
+ const diag = (over) => ({
4
+ check: 'SomeCheck',
5
+ severity: 'warning',
6
+ message: 'msg',
7
+ line: 1,
8
+ column: 1,
9
+ ...over,
10
+ });
11
+ // A neutral "not computed" impact used where the test does not exercise the
12
+ // blast radius itself (it is threaded verbatim by assembleResult).
13
+ const NO_IMPACT = {
14
+ scope: 'direct',
15
+ status: 'unavailable',
16
+ dependents: { total: 0, by_kind: {}, sample: [] },
17
+ };
18
+ // The always-empty envelope fields in this lint-only slice. Spread into each
19
+ // expected result so every assertion checks the WHOLE object, catching any
20
+ // field that unexpectedly starts being populated.
21
+ const EMPTY_ENVELOPE = {
22
+ errors: [],
23
+ warnings: [],
24
+ infos: [],
25
+ impact: NO_IMPACT,
26
+ };
27
+ describe('Unit: assembleResult', () => {
28
+ it('buckets diagnostics by severity into the full result', () => {
29
+ const error = diag({ severity: 'error', check: 'E' });
30
+ const warning = diag({ severity: 'warning', check: 'W' });
31
+ const info = diag({ severity: 'info', check: 'I' });
32
+ expect(assembleResult([error, warning, info], NO_IMPACT)).toEqual({
33
+ ...EMPTY_ENVELOPE,
34
+ status: 'error',
35
+ // 'E' is not a blocking check code, so the file is reported as having an
36
+ // error WITHOUT the write being gated — see the status/gate separation below.
37
+ must_fix_before_write: false,
38
+ errors: [error],
39
+ warnings: [warning],
40
+ infos: [info],
41
+ });
42
+ });
43
+ it('derives status = error, and gates the write, for a BLOCKING error', () => {
44
+ const error = diag({ severity: 'error', check: 'MissingPartial' });
45
+ const warning = diag({ severity: 'warning' });
46
+ expect(assembleResult([error, warning], NO_IMPACT)).toEqual({
47
+ ...EMPTY_ENVELOPE,
48
+ status: 'error',
49
+ must_fix_before_write: true,
50
+ errors: [error],
51
+ warnings: [warning],
52
+ });
53
+ });
54
+ it('derives status = warning (no must_fix) when only warnings/infos are present', () => {
55
+ const warning = diag({ severity: 'warning' });
56
+ const info = diag({ severity: 'info' });
57
+ expect(assembleResult([warning, info], NO_IMPACT)).toEqual({
58
+ ...EMPTY_ENVELOPE,
59
+ status: 'warning',
60
+ must_fix_before_write: false,
61
+ warnings: [warning],
62
+ infos: [info],
63
+ });
64
+ });
65
+ it('derives status = ok with an empty envelope for no diagnostics', () => {
66
+ expect(assembleResult([], NO_IMPACT)).toEqual({
67
+ ...EMPTY_ENVELOPE,
68
+ status: 'ok',
69
+ must_fix_before_write: false,
70
+ });
71
+ });
72
+ it('derives status = ok for infos only', () => {
73
+ const info = diag({ severity: 'info' });
74
+ expect(assembleResult([info], NO_IMPACT)).toEqual({
75
+ ...EMPTY_ENVELOPE,
76
+ status: 'ok',
77
+ must_fix_before_write: false,
78
+ infos: [info],
79
+ });
80
+ });
81
+ it('carries the impact through verbatim (status unaffected by blast radius)', () => {
82
+ const impact = {
83
+ scope: 'direct',
84
+ status: 'computed',
85
+ dependents: {
86
+ total: 2,
87
+ by_kind: { render: 1, include: 1 },
88
+ sample: ['app/views/pages/index.liquid', 'app/views/partials/wrapper.liquid'],
89
+ },
90
+ };
91
+ expect(assembleResult([], impact)).toEqual({
92
+ ...EMPTY_ENVELOPE,
93
+ status: 'ok',
94
+ must_fix_before_write: false,
95
+ impact,
96
+ });
97
+ });
98
+ });
99
+ /**
100
+ * The result carries ONLY fields that are actually populated.
101
+ *
102
+ * An agent cannot tell an always-empty field from a meaningful one, so a permanently-empty
103
+ * stub is a standing invitation to conclude "no fixes available" from a field that was
104
+ * never going to say anything else. These assertions pin the EXACT key set, so
105
+ * reintroducing a stub fails here rather than silently widening the agent surface.
106
+ */
107
+ describe('Unit: the result contract carries no permanently-empty stubs', () => {
108
+ const REMOVED = [
109
+ 'proposed_fixes',
110
+ 'clusters',
111
+ 'scorecard',
112
+ 'tips',
113
+ 'domain_guide',
114
+ 'parse_error',
115
+ ];
116
+ it('emits exactly the checked-result keys', () => {
117
+ const result = assembleResult([diag({ severity: 'error' })], NO_IMPACT);
118
+ expect(Object.keys(result).sort()).toEqual([
119
+ 'errors',
120
+ 'impact',
121
+ 'infos',
122
+ 'must_fix_before_write',
123
+ 'status',
124
+ 'warnings',
125
+ ]);
126
+ });
127
+ it('emits exactly the not-applicable keys, adding only the two that explain the refusal', () => {
128
+ const result = assembleNotApplicableResult({ code: 'ignored', reason: 'because' });
129
+ expect(Object.keys(result).sort()).toEqual([
130
+ 'errors',
131
+ 'impact',
132
+ 'infos',
133
+ 'must_fix_before_write',
134
+ 'next_step',
135
+ 'not_applicable_reason',
136
+ 'status',
137
+ 'warnings',
138
+ ]);
139
+ });
140
+ it.each(REMOVED)('does not emit the removed stub field %s', (field) => {
141
+ const checked = assembleResult([], NO_IMPACT);
142
+ const declined = assembleNotApplicableResult({ code: 'too_large', reason: 'r' });
143
+ expect(field in checked).toBe(false);
144
+ expect(field in declined).toBe(false);
145
+ });
146
+ it('survives a JSON round trip with no key reappearing', () => {
147
+ // The wire format is what the agent actually sees: an `undefined` value would
148
+ // vanish here, so this proves the keys are absent rather than merely undefined.
149
+ const wire = JSON.parse(JSON.stringify(assembleResult([], NO_IMPACT)));
150
+ expect(REMOVED.filter((field) => field in wire)).toEqual([]);
151
+ });
152
+ it('omits next_step and not_applicable_reason from a CHECKED result', () => {
153
+ // The mirror of the above: these two exist only to explain a refusal, so a
154
+ // checked file must not carry them either.
155
+ const wire = JSON.parse(JSON.stringify(assembleResult([], NO_IMPACT)));
156
+ expect(['next_step', 'not_applicable_reason'].filter((field) => field in wire)).toEqual([]);
157
+ });
158
+ });
159
+ /**
160
+ * `status` and `must_fix_before_write` answer DIFFERENT questions, and an agent depends on
161
+ * reading them independently.
162
+ *
163
+ * status -> what was FOUND (any error at all?)
164
+ * must_fix_before_write -> will the file be BROKEN if written?
165
+ *
166
+ * The second is strictly narrower; collapsing them is what made a dead argument block.
167
+ */
168
+ describe('Unit: status and the write gate are separate signals', () => {
169
+ const errorFrom = (check) => diag({ severity: 'error', check });
170
+ it('reports status=error but does NOT gate on a non-blocking error', () => {
171
+ // The reported bug, at the assembly layer.
172
+ const result = assembleResult([errorFrom('PartialCallArguments')], NO_IMPACT);
173
+ expect(result.status).toEqual('error');
174
+ expect(result.must_fix_before_write).toBe(false);
175
+ // Still fully reported — this is a de-escalation of the GATE, not suppression.
176
+ expect(result.errors).toEqual([errorFrom('PartialCallArguments')]);
177
+ });
178
+ it('reports status=error AND gates on a blocking error', () => {
179
+ const result = assembleResult([errorFrom('MissingPartial')], NO_IMPACT);
180
+ expect([result.status, result.must_fix_before_write]).toEqual(['error', true]);
181
+ });
182
+ it('gates when a blocking error is mixed among non-blocking ones', () => {
183
+ const result = assembleResult([errorFrom('ImgWidthAndHeight'), errorFrom('MissingPartial'), errorFrom('UnknownProperty')], NO_IMPACT);
184
+ expect(result.must_fix_before_write).toBe(true);
185
+ // And every one of them is still reported — nothing is dropped by the gate.
186
+ expect(result.errors).toHaveLength(3);
187
+ });
188
+ it('never gates on warnings or infos, whatever the check', () => {
189
+ const result = assembleResult([
190
+ diag({ severity: 'warning', check: 'MissingPartial' }),
191
+ diag({ severity: 'info', check: 'MissingPartial' }),
192
+ ], NO_IMPACT);
193
+ expect([result.status, result.must_fix_before_write]).toEqual(['warning', false]);
194
+ });
195
+ it('reports EVERY diagnostic, with no cap or truncation', () => {
196
+ // An agent must see the whole picture; a silently truncated list would let it
197
+ // "fix everything" and still be wrong.
198
+ const many = Array.from({ length: 60 }, (_, i) => diag({ severity: 'error', check: `Check${i}` }));
199
+ expect(assembleResult(many, NO_IMPACT).errors).toHaveLength(60);
200
+ });
201
+ });
202
+ /**
203
+ * Diagnostics are returned in READING ORDER (line, then column). `check()` batches by check
204
+ * code, so the raw order is grouped by check — an `ImgWidthAndHeight` on line 5 arrives
205
+ * before a `MissingPartial` on line 1, and an agent walking the list jumps around the file.
206
+ */
207
+ describe('Unit: diagnostics are returned in reading order', () => {
208
+ const at = (line, column, check = 'Check') => diag({ severity: 'error', check, line, column });
209
+ it('sorts by line', () => {
210
+ const result = assembleResult([at(5, 1), at(1, 1), at(3, 1)], NO_IMPACT);
211
+ expect(result.errors.map((e) => e.line)).toEqual([1, 3, 5]);
212
+ });
213
+ it('sorts by column within a line', () => {
214
+ const result = assembleResult([at(2, 30), at(2, 5), at(2, 12)], NO_IMPACT);
215
+ expect(result.errors.map((e) => e.column)).toEqual([5, 12, 30]);
216
+ });
217
+ it('breaks an exact-position tie by check code, deterministically', () => {
218
+ // Without a stable tiebreak, byte-identical input could yield different output between
219
+ // runs, undermining offense-comparison verification.
220
+ const first = assembleResult([at(1, 1, 'Zebra'), at(1, 1, 'Alpha')], NO_IMPACT);
221
+ const second = assembleResult([at(1, 1, 'Alpha'), at(1, 1, 'Zebra')], NO_IMPACT);
222
+ expect(first.errors.map((e) => e.check)).toEqual(['Alpha', 'Zebra']);
223
+ expect(first).toEqual(second);
224
+ });
225
+ it('orders each severity bucket independently', () => {
226
+ const result = assembleResult([
227
+ diag({ severity: 'error', line: 9, column: 1, check: 'E' }),
228
+ diag({ severity: 'warning', line: 8, column: 1, check: 'W' }),
229
+ diag({ severity: 'error', line: 2, column: 1, check: 'E' }),
230
+ diag({ severity: 'warning', line: 1, column: 1, check: 'W' }),
231
+ diag({ severity: 'info', line: 7, column: 1, check: 'I' }),
232
+ diag({ severity: 'info', line: 3, column: 1, check: 'I' }),
233
+ ], NO_IMPACT);
234
+ expect(result.errors.map((d) => d.line)).toEqual([2, 9]);
235
+ expect(result.warnings.map((d) => d.line)).toEqual([1, 8]);
236
+ expect(result.infos.map((d) => d.line)).toEqual([3, 7]);
237
+ });
238
+ it('sorts the REAL grouped-by-check order the engine produces', () => {
239
+ // The reported shape: check() emits ImgWidthAndHeight (line 5) before the two
240
+ // MissingPartials (lines 1 and 2) because it batches per check.
241
+ const result = assembleResult([
242
+ diag({ severity: 'error', check: 'ImgWidthAndHeight', line: 5, column: 1 }),
243
+ diag({ severity: 'error', check: 'MissingPartial', line: 1, column: 11 }),
244
+ diag({ severity: 'error', check: 'MissingPartial', line: 2, column: 11 }),
245
+ diag({ severity: 'error', check: 'UnknownFilter', line: 3, column: 8 }),
246
+ diag({ severity: 'error', check: 'PartialCallArguments', line: 4, column: 31 }),
247
+ ], NO_IMPACT);
248
+ expect(result.errors.map((d) => `${d.line}:${d.column} ${d.check}`)).toEqual([
249
+ '1:11 MissingPartial',
250
+ '2:11 MissingPartial',
251
+ '3:8 UnknownFilter',
252
+ '4:31 PartialCallArguments',
253
+ '5:1 ImgWidthAndHeight',
254
+ ]);
255
+ });
256
+ it('does not mutate the caller’s array', () => {
257
+ // assembleResult is documented as PURE; sorting in place would violate that and
258
+ // could reorder a list the caller still holds.
259
+ const input = [at(3, 1), at(1, 1)];
260
+ const snapshot = [...input];
261
+ assembleResult(input, NO_IMPACT);
262
+ expect(input).toEqual(snapshot);
263
+ });
264
+ });
265
+ /**
266
+ * ORDER-INDEPENDENCE, stated as the property that can actually be violated: assembly is a
267
+ * set of independent transforms over one input, not a chain where each step reads what the
268
+ * last one wrote.
269
+ *
270
+ * The observable form of that claim is about the INPUT, because the transforms are not
271
+ * separately callable: the engine emits offenses grouped by check, and which check runs
272
+ * first is an implementation detail of `check()` that has changed before. A result that
273
+ * depended on it could gate differently across two runs over the same file.
274
+ *
275
+ * The fixture deliberately spans every bucket, both gate outcomes, an exact-position tie,
276
+ * and a blocking error placed LAST — the position a truncating or short-circuiting
277
+ * implementation would lose it from.
278
+ */
279
+ describe('Unit: the result does not depend on the order diagnostics arrive in', () => {
280
+ const d = (check, severity, line, column) => ({ check, severity, message: `${check} fired`, line, column });
281
+ const FINDINGS = [
282
+ d('ImgWidthAndHeight', 'error', 5, 1), // non-blocking error
283
+ d('UnknownProperty', 'warning', 2, 4),
284
+ d('ParserBlockingScript', 'info', 9, 1),
285
+ d('UnknownFilter', 'error', 2, 4), // exact tie with the warning above, on position
286
+ d('MatchingTranslations', 'warning', 1, 1),
287
+ d('MissingPartial', 'error', 12, 3), // BLOCKING, and last in input order
288
+ ];
289
+ /** Deterministic permutations — a fixed set, so a failure is reproducible. */
290
+ const permutations = [
291
+ ['as given', FINDINGS],
292
+ ['reversed', [...FINDINGS].reverse()],
293
+ ['grouped by check code', [...FINDINGS].sort((a, b) => a.check.localeCompare(b.check))],
294
+ ['grouped by severity', [...FINDINGS].sort((a, b) => a.severity.localeCompare(b.severity))],
295
+ ['rotated', [...FINDINGS.slice(3), ...FINDINGS.slice(0, 3)]],
296
+ ];
297
+ it('produces an identical result for every input permutation', () => {
298
+ const results = permutations.map(([, input]) => assembleResult(input, NO_IMPACT));
299
+ const [first, ...rest] = results;
300
+ // Whole-value equality against the first, so a difference anywhere in the envelope
301
+ // fails — not merely a difference in the lists.
302
+ for (const result of rest)
303
+ expect(result).toEqual(first);
304
+ });
305
+ it('gates identically however the blocking error is positioned', () => {
306
+ expect(permutations.map(([name, input]) => [name, assembleResult(input, NO_IMPACT)])).toEqual(permutations.map(([name]) => [
307
+ name,
308
+ expect.objectContaining({ status: 'error', must_fix_before_write: true }),
309
+ ]));
310
+ });
311
+ it('resolves an exact-position tie the same way from either input order', () => {
312
+ // `UnknownFilter` and `UnknownProperty` share 2:4 but sit in different buckets, so
313
+ // the tie that matters is within one. Two errors at one position, both orders.
314
+ const a = d('UnknownFilter', 'error', 2, 4);
315
+ const b = d('MissingPartial', 'error', 2, 4);
316
+ expect(assembleResult([a, b], NO_IMPACT).errors).toEqual(assembleResult([b, a], NO_IMPACT).errors);
317
+ });
318
+ });
319
+ //# sourceMappingURL=assemble.spec.js.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Which diagnostics actually block a write.
3
+ *
4
+ * check-common's severities are calibrated for a LINTER IN AN EDITOR, where `ERROR` means
5
+ * "red squiggle, look at this". A write gate answers a different question — will this file
6
+ * be broken if I write it? — and that is a strictly smaller set than `Severity.ERROR`.
7
+ *
8
+ * THE MEMBERSHIP RULE, for whoever adds the next check:
9
+ *
10
+ * BLOCKING Writing this file produces something broken. Any one of:
11
+ * - it will not parse;
12
+ * - it will raise at runtime;
13
+ * - the deploy converter REJECTS it, which fails the whole changeset
14
+ * rather than this one file.
15
+ *
16
+ * EXCEPTION Two members block without meeting that bar, because the file violates a
17
+ * contract its own author WROTE DOWN in this repository rather than a rule
18
+ * we inferred. Nothing joins them without making that argument explicitly.
19
+ *
20
+ * NOT BLOCKING everything else — dead code, style, performance advice, hygiene,
21
+ * degraded-but-working output. Still REPORTED (severity is untouched).
22
+ *
23
+ * MEMBERSHIP IS ESTABLISHED BY MEASUREMENT, NOT BY READING THE CHECK'S NAME, and the
24
+ * measurement runs through THIS SERVER rather than through `check()`. A member clears two
25
+ * independent bars: the diagnostic means the file is broken (measured against a live
26
+ * instance), and a buffer this server accepts can produce it at all. The "every blocking
27
+ * check can actually block" group in `transport/validate-code.spec.ts` drives every member
28
+ * end to end, so adding one here without a fixture there fails the suite.
29
+ *
30
+ * Severity is deliberately NOT changed: a non-member stays an `error` in `errors[]` and
31
+ * keeps `status: 'error'`, it just no longer gates the write. Correctness lives in
32
+ * check-common; the supervisor keeps ONLY agent ergonomics.
33
+ */
34
+ /**
35
+ * Check codes whose presence means the file is broken.
36
+ *
37
+ * Each entry states WHY it blocks, because "it felt severe" is how this list rots back into
38
+ * a copy of the severity table.
39
+ */
40
+ export declare const BLOCKING_CHECKS: ReadonlySet<string>;
41
+ /**
42
+ * NOT in the set, and deliberately so — recorded because each looks severe:
43
+ *
44
+ * - `ValidJSON` and `JSONSyntaxError` — UNREACHABLE from here, which is a different reason
45
+ * from every other entry. Both declare `type: SourceCodeType.JSON`, check-common runs a
46
+ * check only against files of its own type, and this server admits only `.liquid`,
47
+ * `.graphql` and `.yml`/`.yaml`. A `.json` buffer is declined `unsupported_type` before
48
+ * any check runs. The checks themselves are fine and do fire for the CLI and language
49
+ * server; if this server ever admits `.json`, re-add both.
50
+ * - `PartialCallArguments` reports TWO things under one code — an unknown (dead) argument
51
+ * and a missing required one — with no structured discriminator, and non-goal #2 forbids
52
+ * regex over messages. Safe to leave out wholesale because the check only looks at
53
+ * UNDOCUMENTED partials: a partial whose `{% doc %}` declares a contract is owned by
54
+ * `MissingRenderPartialArguments` (which DOES block). What is left rests on required
55
+ * params INFERRED from undefined variables, and the failure mode is a nil value.
56
+ * - `UnrecognizedRenderPartialArguments` is the same dead-argument finding.
57
+ * - `MissingAsset` — measured: `asset_url` is pure string construction, never resolves the
58
+ * asset and never raises; the page returns HTTP 200 and `--dry-run` accepts. It also
59
+ * misfires on assets that exist on the instance but not in the local tree.
60
+ * - `ReservedVariableName` — measured: `{% assign blank = 'oops' %}` renders `[]` and the
61
+ * deployed page returns HTTP 200. Visibly wrong code, still a working page.
62
+ * - `TranslationKeyExists` / `MatchingTranslations` — a missing key renders the key or an
63
+ * empty string. Visibly wrong, still a working page.
64
+ * - `UnknownProperty` — resolves to nil; the page renders.
65
+ * - `UniqueDocParamNames`, `ValidDocParamTypes` — doc hygiene with no runtime effect, since
66
+ * `{% doc %}` is inert at runtime.
67
+ * - `ImgWidthAndHeight`, `ParserBlockingScript` — performance advice.
68
+ * - `ValidFrontmatter` — two of its three findings (unknown key, missing layout) ARE hard
69
+ * deploy rejections, so this absence is a known gap rather than a judgement. All three
70
+ * share one code with no discriminator, so adding it would fix two false approvals and
71
+ * create one false block. Blocked on a discriminator — TASK-26.
72
+ */
73
+ /** A diagnostic, narrowed to what the gate reads. */
74
+ interface GateInput {
75
+ check: string;
76
+ severity: string;
77
+ }
78
+ /**
79
+ * Whether this set of diagnostics should stop the agent writing the file.
80
+ *
81
+ * An UNRECOGNIZED check code is treated as NON-blocking: check-common gains checks over
82
+ * time, and a gate that blocked on codes it had never heard of would over-block every time
83
+ * the engine grew. The supervisor never blocks on its own uncertainty.
84
+ */
85
+ export declare function blocksWrite(diagnostics: readonly GateInput[]): boolean;
86
+ export {};
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Which diagnostics actually block a write.
3
+ *
4
+ * check-common's severities are calibrated for a LINTER IN AN EDITOR, where `ERROR` means
5
+ * "red squiggle, look at this". A write gate answers a different question — will this file
6
+ * be broken if I write it? — and that is a strictly smaller set than `Severity.ERROR`.
7
+ *
8
+ * THE MEMBERSHIP RULE, for whoever adds the next check:
9
+ *
10
+ * BLOCKING Writing this file produces something broken. Any one of:
11
+ * - it will not parse;
12
+ * - it will raise at runtime;
13
+ * - the deploy converter REJECTS it, which fails the whole changeset
14
+ * rather than this one file.
15
+ *
16
+ * EXCEPTION Two members block without meeting that bar, because the file violates a
17
+ * contract its own author WROTE DOWN in this repository rather than a rule
18
+ * we inferred. Nothing joins them without making that argument explicitly.
19
+ *
20
+ * NOT BLOCKING everything else — dead code, style, performance advice, hygiene,
21
+ * degraded-but-working output. Still REPORTED (severity is untouched).
22
+ *
23
+ * MEMBERSHIP IS ESTABLISHED BY MEASUREMENT, NOT BY READING THE CHECK'S NAME, and the
24
+ * measurement runs through THIS SERVER rather than through `check()`. A member clears two
25
+ * independent bars: the diagnostic means the file is broken (measured against a live
26
+ * instance), and a buffer this server accepts can produce it at all. The "every blocking
27
+ * check can actually block" group in `transport/validate-code.spec.ts` drives every member
28
+ * end to end, so adding one here without a fixture there fails the suite.
29
+ *
30
+ * Severity is deliberately NOT changed: a non-member stays an `error` in `errors[]` and
31
+ * keeps `status: 'error'`, it just no longer gates the write. Correctness lives in
32
+ * check-common; the supervisor keeps ONLY agent ergonomics.
33
+ */
34
+ /**
35
+ * Check codes whose presence means the file is broken.
36
+ *
37
+ * Each entry states WHY it blocks, because "it felt severe" is how this list rots back into
38
+ * a copy of the severity table.
39
+ */
40
+ export const BLOCKING_CHECKS = new Set([
41
+ // Does not parse. Nothing downstream can be trusted about the file at all.
42
+ 'LiquidHTMLSyntaxError',
43
+ // Unparseable YAML. Measured: `pos-cli deploy --dry-run` REJECTS the file ("Body
44
+ // contains invalid YAML"), failing the WHOLE changeset. Scoped to SYNTAX on purpose —
45
+ // the converter accepts unknown property types and duplicate property names, so
46
+ // schema-shape validation would block nothing real.
47
+ 'YAMLSyntaxError',
48
+ // Broken reference: the target does not exist, so the render fails at runtime.
49
+ 'MissingPartial',
50
+ // platformOS raises on an unknown filter rather than ignoring it.
51
+ 'UnknownFilter',
52
+ // A KNOWN filter applied with the wrong number of arguments: `Liquid::ArgumentError`,
53
+ // page 500 — the same runtime failure class as `UnknownFilter`. A filter the docset
54
+ // publishes no arity for produces nothing, so a vocabulary gap cannot refuse working
55
+ // code; swept over 8 real projects / ~11k liquid files with 0 false positives.
56
+ 'FilterArity',
57
+ // A single-quoted key or value inside a `{% assign %}` JSON literal. Measured:
58
+ // `{% assign o = {'k': 'v'} %}` raises `Liquid syntax error: Invalid JSON in assign`,
59
+ // and `pos-cli deploy --dry-run` REJECTS, failing every file in the changeset.
60
+ 'JsonLiteralQuoteStyle',
61
+ // The query is invalid against the schema, or references a variable that does
62
+ // not exist: it fails when executed.
63
+ 'GraphQLCheck',
64
+ 'GraphQLVariablesCheck',
65
+ // Runtime error on execution, MEASURED: `hash_assign` against a number, string, boolean
66
+ // or range each raises `HashAssignTagError` ("x is 5, expected Hash or Array"), while
67
+ // the object case it permits renders HTTP 200. Filter return types come from the docset,
68
+ // and an unknown one produces nothing, so a docset gap cannot refuse working code.
69
+ 'InvalidWriteTarget',
70
+ // --- The two deliberate exceptions to the membership rule. ---
71
+ //
72
+ // Neither will fail to parse, raise, or be rejected on deploy. Both block because
73
+ // the file breaks a contract its author WROTE DOWN, which is a stronger signal of
74
+ // a mistake than any inference we could make.
75
+ // A required `@param` the partial's own doc block DECLARES was not passed.
76
+ // Measured: `{% doc %}` is inert at runtime, so this cannot raise — the page
77
+ // deploys and returns HTTP 200 with the params empty (`a=[] b=[]`). It blocks
78
+ // because the author declared the parameter required in this repository.
79
+ 'MissingRenderPartialArguments',
80
+ // A layout that never outputs `content_for_layout`. Measured: HTTP 200 with the
81
+ // page body silently dropped. It blocks because a layout exists to render its
82
+ // content, so one that cannot is a defeated contract — and a silently blank page
83
+ // is harder to diagnose than an error.
84
+ 'MissingContentForLayout',
85
+ ]);
86
+ /**
87
+ * Whether this set of diagnostics should stop the agent writing the file.
88
+ *
89
+ * An UNRECOGNIZED check code is treated as NON-blocking: check-common gains checks over
90
+ * time, and a gate that blocked on codes it had never heard of would over-block every time
91
+ * the engine grew. The supervisor never blocks on its own uncertainty.
92
+ */
93
+ export function blocksWrite(diagnostics) {
94
+ return diagnostics.some((diagnostic) => diagnostic.severity === 'error' && BLOCKING_CHECKS.has(diagnostic.check));
95
+ }
96
+ //# sourceMappingURL=blocking.js.map
@@ -0,0 +1 @@
1
+ export {};