You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(qa): close the platform-checklist traps vocabulary against RUNNER.md, with a parser that refuses rather than fails open (#10682)
`check-platform-checklist` enforced a closed vocabulary for `status`,
`priority`, `surface`, `oracle` and `blocked.by` — and never read `traps`
at all. So an item could carry any trap name and the validator stayed
green, which is how eight undocumented values drifted in (#10416), and
how a TYPO in a documented one (`hydration-races` for `hydration-race`,
which is on 79 of the 205 items) lands as simply a twentieth trap that
no runner rules out.
The vocabulary is READ from RUNNER.md's `### Trap vocabulary` table, not
copied into the script: a sixth hardcoded `Set` would only move the
drift one level up, between the script and RUNNER.md, with nothing
watching that seam. The load-bearing part is therefore not the parser
but its positive control — `extractTrapVocabulary` refuses on a table it
cannot recognise (heading renamed, table moved, zero rows, a row that
lost its backticks) instead of returning an empty allow-list, and a
22-assertion fixture battery proves the refusal still fires. The battery
runs inline on every invocation, not only behind `--self-test`, because
this gate is not CI-wired by maintainer decision and its `pnpm` alias
lives in the fenced root package.json (#9465) — a self-test nothing runs
is the #10574/#10573 defect. The OK line now states what the parse read:
`traps: 19 documented, 19 in use (extractor control: 22 assertions)`.
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
Co-authored-by: Claude <noreply@anthropic.com>
returnno(`the "${TRAP_HEADING}" heading is not in the file — renamed, moved or removed. Restore it: this check reads the vocabulary from that table and refuses to guess.`);
126
+
}
127
+
128
+
leti=h+1;
129
+
for(;i<lines.length;i++){
130
+
if(/^#{1,6}\s/.test(lines[i]))returnno(`no markdown table under "${TRAP_HEADING}" — the next heading arrives first`);
131
+
if(lines[i].trimStart().startsWith('|'))break;
132
+
}
133
+
if(i>=lines.length)returnno(`no markdown table under "${TRAP_HEADING}" — the file ends first`);
134
+
135
+
constheader=lines[i].trim();
136
+
if(!/^\|\s*trap\s*\|/i.test(header)){
137
+
returnno(`the first table under "${TRAP_HEADING}" is not the trap table — expected a "| trap | … |" header row, found ${JSON.stringify(header.slice(0,60))}`);
returnno(`the trap table's header row is not followed by a markdown separator row — found ${JSON.stringify((lines[i+1]??'').trim().slice(0,60))}. This parser cannot read that shape, and reading it wrong would shrink the vocabulary silently.`);
returnno(`${unreadable.length} row(s) of the trap table do not name a single backticked trap in the first cell — e.g. ${JSON.stringify(unreadable[0])}. Every row must read "| \`trap-name\` | what it fakes | counter |"; a row this parser cannot read would drop that trap out of the vocabulary without a word.`);
153
+
}
154
+
if(rows.length===0){
155
+
returnno(`the trap table under "${TRAP_HEADING}" has a header but ZERO rows. An empty vocabulary would make every item's \`traps\` validate against nothing and report zero problems, so this is a refusal — never an empty allow-list.`);
returnbest!==null&&bestD<=3 ? ` — did you mean \`${best}\`?` : '';
191
+
}
192
+
193
+
/** Problems with one item's `traps`, as message strings. Pure; battery-tested below. */
194
+
functiontrapProblems(item,vocabulary){
195
+
constout=[];
196
+
if(item.traps===undefined)returnout;// optional field: 8 of 205 items carry none
197
+
if(!Array.isArray(item.traps)){
198
+
out.push(`"traps" must be an array of trap names from RUNNER.md's \`${TRAP_HEADING}\` table`);
199
+
returnout;
200
+
}
201
+
constseen=newSet();
202
+
item.traps.forEach((t,i)=>{
203
+
if(typeoft!=='string'||!t.trim()){
204
+
out.push(`traps[${i}] must be a non-empty string`);
205
+
return;
206
+
}
207
+
if(t!==t.trim()){
208
+
out.push(`traps[${i}] ${JSON.stringify(t)} has surrounding whitespace — a padded name is a different string to everyone who greps the table for it`);
209
+
}
210
+
constname=t.trim();
211
+
if(seen.has(name))out.push(`traps[${i}] \`${name}\` is listed twice`);
212
+
seen.add(name);
213
+
if(!vocabulary.has(name)){
214
+
out.push(
215
+
`traps[${i}] \`${name}\` is not in RUNNER.md's \`${TRAP_HEADING}\` table${didYouMean(name,vocabulary)}`+
216
+
` — either it is a typo of a documented trap, or it is a new one; document it in that table (name · what it fakes · the counter) before using it, so the runner told to "rule each listed trap out" has something to rule out.`,
217
+
);
218
+
}
219
+
});
220
+
returnout;
221
+
}
222
+
223
+
/**
224
+
* The positive control. Proves the extractor reads a good table AND refuses an
225
+
* empty / renamed / reshaped one, and that the item-side checker catches both
226
+
* drift shapes. Zero I/O — every subject is a literal fixture.
227
+
*/
228
+
functionselfTestTrapVocabulary(){
229
+
constfailures=[];
230
+
letchecked=0;
231
+
constt=(what,ok)=>{
232
+
checked++;
233
+
if(!ok)failures.push(what);
234
+
};
235
+
236
+
consttable=(...rows)=>['prose above','',`${TRAP_HEADING} (\`traps\` field)`,'','| trap | what it fakes | counter |','|---|---|---|', ...rows,'','## Next section','| not | a | trap |'].join('\n');
237
+
238
+
constgood=extractTrapVocabulary(table('| `hydration-race` | empty nav | settle, then read |','| `stale-dist` | src edits with no effect | rebuild |'));
239
+
t('P1 a well-formed table yields exactly its rows',good.refusal===null&&good.traps.join(',')==='hydration-race,stale-dist');
240
+
t('P2 the row scan stops at the table, not at the next table in the file',!good.traps.includes('not'));
241
+
t('P3 a clean parse reports no duplicates',good.duplicates.length===0);
242
+
243
+
constdup=extractTrapVocabulary(table('| `stale-dist` | a | b |','| `stale-dist` | c | d |'));
244
+
t('P4 a trap documented twice is reported',dup.refusal===null&&dup.duplicates.join(',')==='stale-dist'&&dup.traps.length===1);
245
+
246
+
// ── the refusals: each of these, returning an empty vocabulary quietly, is
247
+
// the fail-open this whole block exists to make impossible ──────────────
248
+
constrefusals=[
249
+
['R1 a table with a header and ZERO rows',table()],
250
+
['R2 the heading renamed away',table('| `stale-dist` | a | b |').replace(TRAP_HEADING,'### Traps you may hit')],
251
+
['R3 no table under the heading (prose, then the next heading)',['',TRAP_HEADING,'','See the skill for the list.','','## Next section',''].join('\n')],
252
+
['R4 a row that lost its backticks',table('| `hydration-race` | a | b |','| stale-dist | c | d |')],
253
+
['R5 a different table sitting under the heading',table('| `stale-dist` | a | b |').replace('| trap | what it fakes | counter |','| oracle | when | why |')],
254
+
['R6 an empty file',''],
255
+
['R7 the separator row missing',['',TRAP_HEADING,'','| trap | what it fakes | counter |','| `stale-dist` | a | b |',''].join('\n')],
256
+
['R8 the heading present but the file ends',['',TRAP_HEADING,''].join('\n')],
257
+
];
258
+
for(const[what,md]ofrefusals){
259
+
constr=extractTrapVocabulary(md);
260
+
t(`${what} is REFUSED, not read as an empty vocabulary`,typeofr.refusal==='string'&&r.refusal.length>0&&r.traps.length===0);
261
+
}
262
+
263
+
// The invariant behind every refusal above, asserted as an invariant rather
264
+
// than case by case: no input may yield "nothing to check" without saying so.
265
+
constallInputs=[...refusals.map(([,md])=>md),table('| `x` | a | b |'),'| trap |\n|---|\n| `y` |'];
266
+
t(
267
+
'R9 no input yields an empty vocabulary with no refusal',
console.log(`✓ check-platform-checklist --self-test: ${r.checked} assertions — the trap-table extractor reads a good table and REFUSES an empty/renamed/reshaped one.`);
// The extractor's own positive control, before it is trusted with anything.
304
+
consttrapControl=selfTestTrapVocabulary();
305
+
if(trapControl.failures.length){
306
+
console.error("check-platform-checklist: the trap-vocabulary extractor's own positive control FAILED — this check cannot be trusted, and a green from it would mean nothing.\n");
console.error(`check-platform-checklist: missing ${RUNNER_FILE} — the trap vocabulary lives in its "${TRAP_HEADING}" table and \`traps\` has nothing to validate against.`);
console.error(`check-platform-checklist: cannot read the trap vocabulary out of docs/qa/platform-checklist/RUNNER.md — ${trapTable.refusal}`);
318
+
console.error('\nThis is a REFUSAL, not a pass: with no vocabulary, every item\'s `traps` would validate against an empty set and report zero problems.');
319
+
process.exit(1);
320
+
}
321
+
constTRAPS=newSet(trapTable.traps);
322
+
for(constdoftrapTable.duplicates){
323
+
err('RUNNER.md',null,`\`${TRAP_HEADING}\` lists \`${d}\` twice — one trap, one row, one definition`);
`\`${TRAP_HEADING}\` documents \`${t}\` but no checklist item lists it — put it on the items it protects, or drop the row; a definition nothing points at is one no run will ever rule out`,
0 commit comments