From 3b0aa873230f7fde36538ffd84717449e847ddd0 Mon Sep 17 00:00:00 2001 From: Remon Panman <228601219+Tradebaas@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:17:59 +0200 Subject: [PATCH 1/2] feat(checks): a waiver is a number the owner sees, not a quiet pass The gate that reads the floor table treats an answered class and a waived one the same way, which is correct: a waiver is a stated, reasoned choice, and refusing to build over one would push projects to lie in the table rather than use the form honestly. That is also what makes waiving the cheap default, because until now nothing counted them anywhere the owner looks. The floor is now derived once, in the file that already parses the contract, and reported in two places from that one derivation: the enforcement line under checks/check.mjs, and its own line on the board directly under the gates. Neither renders the table itself, so a waiver cannot show up in one and not the other. The suite proves that by comparing the two outputs rather than asserting each, the way the printed board was proved against the served one. The wording carries its own limit, in the summary rather than behind the fold. Proven means a workflow runs the command the stack file names, and nothing in this framework has looked at what that command asserts. A folded board still has to say so, or the number alone reads as an audit nobody performed. A class whose command no workflow runs is counted as open, never as a waiver. The gate already refuses both, and counting a dead command as a stated choice would launder a hole into a decision - the one thing this line exists to prevent. A project with no stack file reports nothing at all. Not started is not a floor of zero: it has not been asked the six questions yet. The drill asserts both halves in a fresh copy, reading the floor before the clearing takes the stack file away and finding nothing after it. The gate and the report now share one read of the contract, because two parses of one table is exactly the drift this story exists to prevent. Splitting that read also fixed a live defect it introduced on the way: the design detector's half of the gate needs the workflow lines whether or not a stack file exists, and briefly stopped getting them. The stack suite caught it. Seven comments across four files still described a board of two lines under the shelves. This diff makes that false, so they say the lines instead. Groundwork reads 4 of 6 on its own floor, with builds and dependencies waived and their reasons quoted from node.md. Traces-to: SC-5 --- checks/board-page.mjs | 9 +-- checks/board-shell.mjs | 2 +- checks/board-strip.mjs | 90 +++++++++++++++++++++++++---- checks/board-strip.test.mjs | 103 +++++++++++++++++++++++++++++++-- checks/board.test.mjs | 4 +- checks/check-stack.mjs | 75 ++++++++++++++++++++---- checks/check.mjs | 5 +- checks/check.test.mjs | 110 +++++++++++++++++++++++++++++++++++- checks/drill.mjs | 19 +++++++ checks/enforcement.mjs | 43 ++++++++++++++ 10 files changed, 426 insertions(+), 34 deletions(-) diff --git a/checks/board-page.mjs b/checks/board-page.mjs index d769690..22d6a19 100644 --- a/checks/board-page.mjs +++ b/checks/board-page.mjs @@ -1,12 +1,13 @@ // The board: the whole project on one page. What it is for and what it is not, the round in // flight, six lanes with the cards in them, the four shelves that hold every document, and the -// two lines that say whether the gates are armed and how the documents point at each other. +// lines that say whether the gates are armed, how much of this project's own code they look at, +// and how the documents point at each other. // Facts in, one page out - nothing is stored, nothing is generated ahead of time. Every lane, // count, blocker and next step comes from checks/work.mjs through the derivation // checks/progress.mjs already exposes, so moving one story's status line moves its card and no // second administration exists to keep in step (decision 0021). -// The shelves are checks/shelves.mjs, the two lines checks/board-strip.mjs, the shell they render -// in checks/board-shell.mjs; serving is checks/board-server.mjs and the file page +// The shelves are checks/shelves.mjs, the lines under them checks/board-strip.mjs, the shell +// they render in checks/board-shell.mjs; serving is checks/board-server.mjs and the file page // checks/board-document.mjs. // One derivation, two outputs (decision 0021): served on this machine, or printed as one // self-contained file (`--page`, E-01/F-04/S-05). The file is the picture of the repository at @@ -118,7 +119,7 @@ const BOARD_WORDS = { // One object per language, so a caller asks once and gets the shell's sentences, the stand's // sentences, the shelves' and the board's own. Each set owns its own keys, and the merge is the -// only place they meet. The two lines under the shelves gather their own, next door. +// only place they meet. The lines under the shelves gather their own, next door. const boardWords = (lang) => ({ ...shellWords(lang), ...(WORDS[lang] || WORDS.en), diff --git a/checks/board-shell.mjs b/checks/board-shell.mjs index 4196a7c..758e060 100644 --- a/checks/board-shell.mjs +++ b/checks/board-shell.mjs @@ -1,5 +1,5 @@ // The page shell every page this board serves is built in: the look, and the safe ways project -// text gets into it. The cards, the lanes, the sidebar, the two lines under them, a file page and +// text gets into it. The cards, the lanes, the sidebar, the lines under them, a file page and // a notice all render through here, so the project has one design and one escaping rule rather // than six of each. // Split out of checks/board-document.mjs when the lanes arrived (E-01/F-04/S-03): a second page diff --git a/checks/board-strip.mjs b/checks/board-strip.mjs index 0e83773..901330c 100644 --- a/checks/board-strip.mjs +++ b/checks/board-strip.mjs @@ -1,20 +1,25 @@ -// The two lines under the shelves: how many gates are armed on this machine, and how the -// project's documents point at each other. Each says its answer in one sentence and folds the -// detail the reader behind it produces, so the board ends with two facts rather than two pages. -// Both sentences are the readers' own (checks/enforcement.mjs, checks/links.mjs), quoted rather -// than reworded: the terminal and the board must never word one fact differently. +// The lines under the shelves: how many gates are armed on this machine, how much of this +// project's own code any of them actually looks at, and how the project's documents point at +// each other. Each says its answer in one sentence and folds the detail the reader behind it +// produces, so the board ends with a handful of facts rather than a handful of pages. +// Every sentence is a reader's own (checks/enforcement.mjs, checks/check-stack.mjs, +// checks/links.mjs), quoted rather than reworded: the terminal and the board must never word one +// fact differently. The floor line reads the same derivation the enforcement line prints, so a +// waiver cannot show up in one place and not the other (E-02/F-01/S-03). // Moved here when the four shelves took the board and /overview was retired // (E-01/F-04/S-04); until then these were two of the six cards in checks/board-document.mjs. import { enforcementReport } from './enforcement.mjs'; +import { floorReport } from './check-stack.mjs'; import { projectGraph, LINK_WORDS, HUB_MIN } from './links.mjs'; import { shellWords, escapeHtml, sentence, pathName, list, folded, attempt, } from './board-shell.mjs'; -// Both lines report on the project as a whole rather than on one document, so the file each +// Each line reports on the project as a whole rather than on one document, so the file each // names is the one that does the looking. const ENFORCEMENT_PATH = 'checks/enforcement.mjs'; +const FLOOR_PATH = 'checks/check-stack.mjs'; const LINKS_PATH = 'checks/links.mjs'; // The gates line's own framing. What is armed and what is not comes from the report. @@ -49,16 +54,47 @@ const GATE_WORDS = { }, }; -// The two reads this strip needs. Done before anything renders, so the page can ask git once +// The floor line's own framing. The count comes from the derivation; the second sentence is the +// limit, and it sits in the summary rather than behind the fold on purpose: a folded board still +// has to say what "proven" does not buy, or the number alone reads as an audit nobody performed. +// This line needs no served-or-printed split the way the gates line does: a floor is read off the +// stack file and the workflows, both tracked, so it says the same thing wherever it is read. +const FLOOR_WORDS = { + en: { + floorAnswer: (n, t) => `${n} of the ${t} risk classes are proven by a command that runs. ` + + 'Proven means it runs, not that what it runs is any good.', + headWaived: 'Waived, on purpose and with a reason', + headOpen: 'Answered by nothing that runs', + waivedAs: (cls, form) => `${cls}, waived as ${form}`, + openWhy: 'A class here is either unanswered or answers with a command no workflow runs. The ' + + 'gate refuses both; neither is a waiver.', + answeredIn: 'Answered in', + }, + nl: { + floorAnswer: (n, t) => `${n} van de ${t} risicoklassen worden bewezen door een commando dat ` + + 'draait. Bewezen betekent dat het draait, niet dat wat het draait deugt.', + headWaived: 'Vrijgesteld, bewust en met reden', + headOpen: 'Beantwoord door niets dat draait', + waivedAs: (cls, form) => `${cls}, vrijgesteld als ${form}`, + openWhy: 'Een klasse hier is niet beantwoord, of antwoordt met een commando dat geen enkele ' + + 'workflow draait. De poort weigert allebei; geen van beide is een vrijstelling.', + answeredIn: 'Beantwoord in', + }, +}; + +// The three reads this strip needs. Done before anything renders, so the page can ask git once // which of the names below it is allowed to open. export const readStrip = (root) => ({ gates: attempt(() => enforcementReport(root)), + floor: attempt(() => floorReport(root)), graph: attempt(() => projectGraph(root)), }); -// Every file name these two lines will show, for that one ignore lookup. +// Every file name these lines will show, for that one ignore lookup. export const stripPaths = (facts) => [ - ENFORCEMENT_PATH, LINKS_PATH, ...(facts.graph.value?.documents || []).map((d) => d.path), + ENFORCEMENT_PATH, FLOOR_PATH, LINKS_PATH, + ...(facts.floor?.value?.files || []), + ...(facts.graph.value?.documents || []).map((d) => d.path), ]; // ---------------------------------------------------------------- one line @@ -95,6 +131,36 @@ function gatesDetail(signals, w) { return out.join('\n'); } +// What the six classes are answered with, for the reader who wants to know which hole they are +// standing in. A whole floor produces nothing here, and the line stays a single sentence. +function floorDetail(floor, w, opens) { + const out = []; + if (floor.waived.length) { + out.push(`
${sentence(`${w.waivedAs(x.cls, x.form)} - ${x.reason}`)}
`); + } + if (floor.open.length) { + out.push(`${escapeHtml(w.openWhy)}
`); + } + if (!out.length) return ''; + // Where the answers live, so the reader can go read the table rather than trust this summary. + out.push(`${escapeHtml(w.answeredIn)} ${floor.files.map((f) => pathName(f, opens)).join(', ')}
`); + return out.join('\n'); +} + +// The floor line exists only once a stack does. A project that has not chosen one has not failed +// to answer the six classes, it has not been asked yet, and a line reading "0 of the 6" would be +// the board's own version of the false confidence this gate exists to remove. +function floorLine(read, w, opens) { + if (!read || (!read.error && !read.value)) return ''; + return line(w, read, (f) => w.floorAnswer(f.proven, f.total), (f) => floorDetail(f, w, opens), + FLOOR_PATH, opens); +} + // Which document points at which, so the question behind moving or deleting a file has an answer // before the move: what nothing points at can go, and what many documents lean on is a decision. function linksDetail(graph, w, opens) { @@ -138,17 +204,21 @@ const linksAnswer = (graph, w) => (graph.documents.length + (graph.unresolved.length ? `${w.unresolved}: ${graph.unresolved.length}.` : w.noUnresolved) : w.noDocuments); -// The two lines, in the project's own language. The word sets are gathered here rather than +// The lines, in the project's own language. The word sets are gathered here rather than // handed in, so a caller cannot hand this file a set that words a gate differently than the // terminal does. `made` is the moment a printed board was made, and null on a served one: the // only thing that changes here is which of the two gate sentences is true. export function renderStrip(facts, lang, opens = () => false, made = null) { const w = { ...shellWords(lang), ...(GATE_WORDS[lang] || GATE_WORDS.en) }; + const fw = { ...shellWords(lang), ...(FLOOR_WORDS[lang] || FLOOR_WORDS.en) }; const lw = LINK_WORDS[lang] || LINK_WORDS.en; const armed = made ? w.armedThere : w.armedOf; return 'checks\/check-stack\.mjs<\/code>/);
+});
+
+test('a whole floor is one sentence, with no working to show', () => {
+ const html = lineOf(stripF({ ...FLOOR, proven: 6, waived: [] }), 2);
+ assert.match(html, /6 of the 6 risk classes/);
+ assert.doesNotMatch(html, //);
+});
+
+test('a class nothing runs is named as open, never dressed up as a waiver', () => {
+ const text = visible(lineOf(stripF({ ...FLOOR, proven: 3, open: ['behaves'] }), 2));
+ assert.match(text, /Answered by nothing that runs/);
+ assert.match(text, /behaves/);
+ assert.match(text, /neither is a waiver/);
+});
+
+// Not started is not the same as failing, which is the rule the empty copy follows everywhere.
+test('a project with no stack file gets no floor line, rather than a floor of zero', () => {
+ for (const nothing of [null, undefined]) {
+ const html = stripF(nothing);
+ assert.doesNotMatch(visible(html), /risk classes/);
+ // The rest of the strip is untouched by a line that is absent.
+ assert.match(visible(lineOf(html, 2)), /no documents to read yet/);
+ }
+});
+
+// The seam this story exists for: one derivation, two outputs. Proven by comparing the two
+// renderings rather than asserting each, so a waiver cannot show up in one and not the other.
+test('the board and the enforcement line carry one floor, not two readings of it', () => {
+ const board = visible(lineOf(stripF(FLOOR), 2));
+ const terminal = formatFloor(FLOOR).join('\n');
+ assert.match(terminal, /4 of the 6/);
+ assert.match(board, /4 of the 6/);
+ for (const w of FLOOR.waived) {
+ assert.ok(terminal.includes(w.cls) && terminal.includes(w.reason), `the terminal drops ${w.cls}`);
+ assert.ok(board.includes(w.cls) && board.includes(w.reason), `the board drops ${w.cls}`);
+ }
+ // Both state the limit rather than implying a guarantee, in their own wording.
+ assert.match(terminal, /not that what it runs is any good/);
+ assert.match(board, /not that what it runs is any good/);
+});
+
+test('the floor line speaks the language the project set', () => {
+ const text = visible(renderStrip(facts(ARMED, NO_GRAPH, FLOOR), 'nl'));
+ assert.match(text, /4 van de 6 risicoklassen/);
+ assert.match(text, /Bewezen betekent dat het draait/);
+ // The class and the form keep the contract's own vocabulary in both languages: they are what
+ // the stack file literally says, and a translated key would send a reader looking for a row
+ // that is not there.
+ assert.match(text, /dependencies, vrijgesteld als not applicable/);
+});
+
+test('nothing a stack file says can execute as markup', () => {
+ const html = stripF({
+ ...FLOOR,
+ waived: [{ cls: '', form: 'manual', reason: '
', path: 'p.md' }],
+ });
+ assert.doesNotMatch(html, /