Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions checks/board-page.mjs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion checks/board-shell.mjs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
90 changes: 80 additions & 10 deletions checks/board-strip.mjs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(`<h3>${escapeHtml(w.headWaived)}</h3>`);
// The reason is the stack file author's own, never reworded here: it is the whole content of
// the waiver, and the board is quoting it rather than summarising it.
for (const x of floor.waived) out.push(`<p class="note">${sentence(`${w.waivedAs(x.cls, x.form)} - ${x.reason}`)}</p>`);
}
if (floor.open.length) {
out.push(`<h3>${escapeHtml(w.headOpen)}</h3>`);
out.push(list(floor.open));
out.push(`<p class="hint">${escapeHtml(w.openWhy)}</p>`);
}
if (!out.length) return '';
// Where the answers live, so the reader can go read the table rather than trust this summary.
out.push(`<p class="hint">${escapeHtml(w.answeredIn)} ${floor.files.map((f) => pathName(f, opens)).join(', ')}</p>`);
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) {
Expand Down Expand Up @@ -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 '<div class="strip">'
+ line(w, facts.gates, (s) => `${armed(s.filter((x) => x.armed).length, s.length)}.`,
(s) => gatesDetail(s, w), ENFORCEMENT_PATH, opens)
// Directly under the gates, because it is the question the gates line invites: they are
// armed, and this is how much of this project's own code any of them looks at.
+ floorLine(facts.floor, fw, opens)
+ line(w, facts.graph, (g) => linksAnswer(g, lw), (g) => linksDetail(g, lw, opens), LINKS_PATH, opens)
+ '</div>';
}
103 changes: 99 additions & 4 deletions checks/board-strip.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
// Self-test for the two lines under the shelves (checks/board-strip.mjs): how many gates are
// armed on this machine, and how the documents point at each other. What is proven here is that
// Self-test for the lines under the shelves (checks/board-strip.mjs): how many gates are armed
// on this machine, how much of the project's own code any of them looks at, and how the
// documents point at each other. What is proven here is that
// each line leads with the answer in the reader's own words, keeps the whole working one click
// behind it, and that a reader that fails costs the board one line rather than the page.
// These were two of the six cards on the retired /overview page; the board they now sit on is
Expand All @@ -10,18 +11,23 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { visible } from './board-fixture.mjs';
import { renderStrip } from './board-strip.mjs';
import { formatFloor } from './enforcement.mjs';
import { linkGraph } from './links.mjs';

// The strip renders from two reads, each of which either produced a value or threw. A test hands
// The strip renders from its reads, each of which either produced a value or threw. A test hands
// them in directly, so no fixture on disk stands between an assertion and what it is about.
const facts = (gates, graph) => ({
// The floor read is left out entirely unless a test names one, which is also how a project that
// has not chosen a stack reaches this function.
const facts = (gates, graph, floor) => ({
gates: gates instanceof Error ? { error: gates } : { value: gates },
graph: graph instanceof Error ? { error: graph } : { value: graph },
...(floor === undefined ? {} : { floor: floor instanceof Error ? { error: floor } : { value: floor } }),
});
const NO_GRAPH = linkGraph([]);
const ARMED = [{ signal: 'hooks', armed: true, detail: 'core.hooksPath -> checks/hooks' }];

const strip = (gates, graph, opens = () => false) => renderStrip(facts(gates, graph), 'en', opens);
const stripF = (floor, gates = ARMED, graph = NO_GRAPH) => renderStrip(facts(gates, graph, floor), 'en');
// One line off the strip, so an assertion is about the line it names and not about its neighbour.
const lineOf = (html, n) => html.split('<section class="line">')[n].split('</section>')[0];

Expand Down Expand Up @@ -51,6 +57,95 @@ test('a machine with nothing armed says so, rather than saying nothing', () => {
assert.doesNotMatch(text, /\bArmed the\b/);
});

// ---------------------------------------------------------------- the floor line

// One floor, as the derivation in checks/check-stack.mjs hands it over: four classes answered by
// a command a workflow runs, two waived on purpose, none left open.
const FLOOR = {
total: 6,
proven: 4,
waived: [
{ cls: 'dependencies', form: 'not applicable', reason: 'No third-party code ships here', path: 'docs/standards/node.md' },
{ cls: 'renders', form: 'manual', reason: 'A named walkthrough by the owner before each release', path: 'docs/standards/node.md' },
],
open: [],
files: ['docs/standards/node.md'],
};

test('the floor line leads with the count and says in the summary what proven does not buy', () => {
const html = lineOf(stripF(FLOOR), 2);
// The limit is in the summary and not behind the fold: a folded board still has to say it, or
// the number alone reads as an audit that nobody performed.
assert.match(html, /<summary><span class="ttl">4 of the 6 risk classes are proven by a command that runs\. Proven means it runs, not that what it runs is any good\.<\/span>/);
const text = visible(html);
assert.match(text, /Waived, on purpose and with a reason/);
// The reason is the stack file author's own words, quoted rather than summarised.
assert.match(text, /dependencies, waived as not applicable - No third-party code ships here/);
assert.match(text, /renders, waived as manual - A named walkthrough by the owner/);
// And the reader is pointed at the table itself rather than asked to trust this line.
assert.match(text, /Answered in/);
assert.match(text, /docs\/standards\/node\.md/);
assert.match(html, /From <code>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, /<details>/);
});

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: '<script>alert(1)</script>', form: 'manual', reason: '<img src=x onerror=alert(2)>', path: 'p.md' }],
});
assert.doesNotMatch(html, /<script>alert|<img src=x/);
assert.match(html, /&lt;script&gt;/);
});

// ---------------------------------------------------------------- the link line

test('the link line names what is load-bearing and folds the long lists behind their counts', () => {
Expand Down
4 changes: 2 additions & 2 deletions checks/board.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// routes in checks/board-server.mjs that reach them). What is proven here is that the page is a pure function of what is on disk - move one
// status line and the card moves, add one document and it appears on a shelf, with no other edit -
// and that a card says everything a person needs and nothing they do not.
// The two lines under the shelves are proven in checks/board-strip.test.mjs, the file page and
// The lines under the shelves are proven in checks/board-strip.test.mjs, the file page and
// the server's own answers in checks/board-server.test.mjs, and what may be opened at all in
// checks/board-path.test.mjs.
// Run: node --test checks/board.test.mjs
Expand Down Expand Up @@ -337,7 +337,7 @@ test('the framing words follow the project language', () => {
assert.match(html, /aria-label="Dit project"/, 'the sidebar names itself in it');
assert.match(text, /Waarom we het bouwen/, 'and so do the subjects under it');
assert.match(text, /Bord/, 'and the destinations beside them');
assert.match(text, /poorten op deze machine staan scherp/, 'and so do the two lines');
assert.match(text, /poorten op deze machine staan scherp/, 'and so do the lines under them');
f.clean();
});

Expand Down
Loading