@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
|
@@ -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,92 @@
|
|
|
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
|
+
* - `DeprecatedFrontmatterField` — measured: a deprecated key (`layout_name` naming a layout
|
|
69
|
+
* that exists, `redirect_url`) and a deprecated `home.liquid` filename are all ACCEPTED by
|
|
70
|
+
* the converter. Superseded, not broken.
|
|
71
|
+
*
|
|
72
|
+
* `ValidFrontmatter` used to be listed here, recorded as blocked on a discriminator
|
|
73
|
+
* (TASK-26) because "two of its three findings" were fatal and blocking the code would have
|
|
74
|
+
* created one false block. Both halves were wrong. It had seven reachable shapes, not three,
|
|
75
|
+
* and `layout: false` — the finding named as the harmless one — is itself a converter
|
|
76
|
+
* rejection. It has since been split into per-shape codes, which is the discriminator the
|
|
77
|
+
* task was waiting for: three of them are in the set above, two are named here.
|
|
78
|
+
*/
|
|
79
|
+
/** A diagnostic, narrowed to what the gate reads. */
|
|
80
|
+
interface GateInput {
|
|
81
|
+
check: string;
|
|
82
|
+
severity: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether this set of diagnostics should stop the agent writing the file.
|
|
86
|
+
*
|
|
87
|
+
* An UNRECOGNIZED check code is treated as NON-blocking: check-common gains checks over
|
|
88
|
+
* time, and a gate that blocked on codes it had never heard of would over-block every time
|
|
89
|
+
* the engine grew. The supervisor never blocks on its own uncertainty.
|
|
90
|
+
*/
|
|
91
|
+
export declare function blocksWrite(diagnostics: readonly GateInput[]): boolean;
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
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, which is a
|
|
45
|
+
// narrower claim than it used to make here: a REAL deploy rejects an unknown property
|
|
46
|
+
// TYPE (now `InvalidSchemaPropertyType`) and an unknown top-level key, both of which
|
|
47
|
+
// `--dry-run` misses by returning before the nested converter. Duplicate property names
|
|
48
|
+
// ARE accepted — measured.
|
|
49
|
+
'YAMLSyntaxError',
|
|
50
|
+
// `Attribute type `x` is not allowed`, from the nested `CustomAttributeConverter`.
|
|
51
|
+
// Measured by a REAL deploy; `--dry-run` accepts it.
|
|
52
|
+
'InvalidSchemaPropertyType',
|
|
53
|
+
// Broken reference: the target does not exist, so the render fails at runtime.
|
|
54
|
+
'MissingPartial',
|
|
55
|
+
// platformOS raises on an unknown filter rather than ignoring it.
|
|
56
|
+
'UnknownFilter',
|
|
57
|
+
// A KNOWN filter applied with the wrong number of arguments: `Liquid::ArgumentError`,
|
|
58
|
+
// page 500 — the same runtime failure class as `UnknownFilter`. A filter the docset
|
|
59
|
+
// publishes no arity for produces nothing, so a vocabulary gap cannot refuse working
|
|
60
|
+
// code; swept over 8 real projects / ~11k liquid files with 0 false positives.
|
|
61
|
+
'FilterArity',
|
|
62
|
+
// A single-quoted key or value inside a `{% assign %}` JSON literal. Measured:
|
|
63
|
+
// `{% assign o = {'k': 'v'} %}` raises `Liquid syntax error: Invalid JSON in assign`,
|
|
64
|
+
// and `pos-cli deploy --dry-run` REJECTS, failing every file in the changeset.
|
|
65
|
+
'JsonLiteralQuoteStyle',
|
|
66
|
+
// The query is invalid against the schema, or references a variable that does
|
|
67
|
+
// not exist: it fails when executed.
|
|
68
|
+
'GraphQLCheck',
|
|
69
|
+
'GraphQLVariablesCheck',
|
|
70
|
+
// Runtime error on execution, MEASURED: `hash_assign` against a number, string, boolean
|
|
71
|
+
// or range each raises `HashAssignTagError` ("x is 5, expected Hash or Array"), while
|
|
72
|
+
// the object case it permits renders HTTP 200. Filter return types come from the docset,
|
|
73
|
+
// and an unknown one produces nothing, so a docset gap cannot refuse working code.
|
|
74
|
+
'InvalidWriteTarget',
|
|
75
|
+
// --- The two deliberate exceptions to the membership rule. ---
|
|
76
|
+
//
|
|
77
|
+
// Neither will fail to parse, raise, or be rejected on deploy. Both block because
|
|
78
|
+
// the file breaks a contract its author WROTE DOWN, which is a stronger signal of
|
|
79
|
+
// a mistake than any inference we could make.
|
|
80
|
+
// A required `@param` the partial's own doc block DECLARES was not passed.
|
|
81
|
+
// Measured: `{% doc %}` is inert at runtime, so this cannot raise — the page
|
|
82
|
+
// deploys and returns HTTP 200 with the params empty (`a=[] b=[]`). It blocks
|
|
83
|
+
// because the author declared the parameter required in this repository.
|
|
84
|
+
'MissingRenderPartialArguments',
|
|
85
|
+
// A layout that never outputs `content_for_layout`. Measured: HTTP 200 with the
|
|
86
|
+
// page body silently dropped. It blocks because a layout exists to render its
|
|
87
|
+
// content, so one that cannot is a defeated contract — and a silently blank page
|
|
88
|
+
// is harder to diagnose than an error.
|
|
89
|
+
'MissingContentForLayout',
|
|
90
|
+
// --- Frontmatter, split out of `ValidFrontmatter`. Each is a measured converter
|
|
91
|
+
// rejection, which fails the WHOLE changeset. ---
|
|
92
|
+
// `Body contains invalid YAML: <parser error>`. `YAMLSyntaxError` cannot cover this —
|
|
93
|
+
// it is YAML-typed and never sees a `.liquid` file.
|
|
94
|
+
'InvalidFrontmatterSyntax',
|
|
95
|
+
// `Unknown properties: <key>.`
|
|
96
|
+
'UnknownFrontmatterField',
|
|
97
|
+
// `Request method 'POST' is not allowed`, and `layout: false` → `undefined method 'sub'
|
|
98
|
+
// for false` (YAML reads it as a boolean; `set_layout` guards `nil`, not `false`).
|
|
99
|
+
'InvalidFrontmatterValue',
|
|
100
|
+
// `Could not find Layout with layout: <name>`.
|
|
101
|
+
'MissingLayout',
|
|
102
|
+
// `<page> tries to assign authorization_policies which do not exist: <name>`. Measured by
|
|
103
|
+
// a REAL deploy — `--dry-run` accepts it, returning before the association write.
|
|
104
|
+
'MissingFrontmatterAssociation',
|
|
105
|
+
]);
|
|
106
|
+
/**
|
|
107
|
+
* Whether this set of diagnostics should stop the agent writing the file.
|
|
108
|
+
*
|
|
109
|
+
* An UNRECOGNIZED check code is treated as NON-blocking: check-common gains checks over
|
|
110
|
+
* time, and a gate that blocked on codes it had never heard of would over-block every time
|
|
111
|
+
* the engine grew. The supervisor never blocks on its own uncertainty.
|
|
112
|
+
*/
|
|
113
|
+
export function blocksWrite(diagnostics) {
|
|
114
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === 'error' && BLOCKING_CHECKS.has(diagnostic.check));
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=blocking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|