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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,29 @@ All notable changes to **basou** are recorded here. The project follows
order is read from the files' birth times on every import, so if they
change after an import (restored from a backup, or copied to another disk)
and the order flips, the shared records end up counted in neither session.
- **A key no version of its kind takes, in a measure or an observation of
a board, is refused with the keys the kind takes**, in the version the
board is read by:
`observe[0]: unknown keys 'count', 'capture' (kind 'http' takes: key, kind, url, words, count_pattern, capture_pattern, notes)`.
Until now only the key was named, and the names of a kind's keys were
found by guessing from one refusal after another.
- **`basou board guide` says where the keys of each kind are described**:
the reference for its own version (`docs/spec/board.md` at that version's
tag), when the axis is cut before a board is declared and when it is
reviewed. Nothing basou printed named the keys of an `http`, a
`github_check`, a `file_name` or a `total`.
- **A measure whose value is a string (`file_name`, `regex_capture`) is
printed and drawn without its unit** (`latest README.md`, not
`latest README.md name`): a name is no amount. A number keeps its unit,
and so does the count of a `file_paths`. A measure still declares `unit`.
- **The board page's list of records and the last record in
`basou board guide` give each record's own `recorded_at`**, as the
heading of the page does. A record written before 0.70.1 has a name of
another time, a second or so apart, which they gave.
- **`basou board record` takes `recorded_at` after it measures the board
again**, so a record is no longer recorded a few milliseconds before the
measurement it holds. Records written by 0.71.1 or earlier keep the time
taken before.

## 0.71.1 — 2026-10-19

Expand Down
14 changes: 10 additions & 4 deletions docs/spec/board.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ Conventions on this page:
visibility.
- A record's file name is a ULID whose time is the record's `recorded_at`,
with `.json`. Any other file in `records/` is passed over. The previous
record is the last by name.
record is the last by name. A record written before basou 0.70.1 has a
name of another time, a second or so apart; the board page's list of
records and `basou board guide` give each record's own `recorded_at`.

Both shapes carry a version, and every version ever written is read:

Expand Down Expand Up @@ -89,7 +91,11 @@ last axis review) is not known; it is never taken as no record.
(`the key 01 at line 4, column 3 is not a string; write it in quotes`), so
the stage ids are written in quotes: `"01"`.
- **An unknown key is refused at every level**, so a typo is reported, not
ignored.
ignored. One in a measure or an observation itself (not in its
`section`) that no version of its kind takes is refused with the keys the
kind takes in the version the file is read by
(`observe[0]: unknown keys 'count', 'capture' (kind 'http' takes: key, kind, url, words, count_pattern, capture_pattern, notes)`);
one a later version takes is told which version reads it.
- **A problem in the YAML itself stops the reading**, and so does an unknown
`board_version` (see [Files and versions](#1-files-and-versions)): a syntax
error, a duplicate key, a `%YAML` directive or a key that is not a string
Expand Down Expand Up @@ -207,7 +213,7 @@ Every measure has:
|---|---|---|
| `id` | yes | an id, or from version 3 a dotted id |
| `kind` | yes | one of the kinds below |
| `unit` | yes | a non-empty string, shown with the value |
| `unit` | yes | a non-empty string, shown with a value that is a number, and with the count of a `file_paths`; a string (`file_name`, `regex_capture`) is shown without it |
| `lane` | no | the lanes whose section of the board page draws the measure: one lane id, or from version 3 a non-empty list of them, each a lane of the board and none twice |

A `lane` is read as a list whatever the version, so a record holds a list
Expand Down Expand Up @@ -828,7 +834,7 @@ later one is added.
| Key | Holds |
|---|---|
| `record_version` | `3` |
| `recorded_at` | when it was recorded: UTC, ISO 8601 with milliseconds |
| `recorded_at` | when it was recorded: UTC, ISO 8601 with milliseconds. After the measurement it holds; a record written by basou 0.71.1 or earlier took it just before measuring again |
| `recorded_with` | `{ basou, build }`: the version of the basou that recorded it, and the commit it was built from (null for a build from source) |
| `declaration` | the declaration the board was measured and judged by ([4.2](#42-declaration)) |
| `measure` | the measurement, as `basou board measure --json` prints it, without `diff` ([5](#5-the-measurement)) |
Expand Down
18 changes: 17 additions & 1 deletion packages/cli/src/commands/board.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,8 @@ describe("basou board measure", () => {
await runBoardMeasure({}, ctx(repo));
const text = out.join("\n");
expect(text).toContain("\n paths 2 files (paths in --json) [core]\n");
expect(text).toContain("\n latest README.md name\n");
// A name is no amount: it is printed without the unit, a count with it.
expect(text).toContain("\n latest README.md\n");
out.length = 0;
await runBoardMeasure({ json: true }, ctx(repo));
expect(JSON.parse(out.join("\n")).measures.paths).toEqual({
Expand Down Expand Up @@ -1081,6 +1082,21 @@ describe("basou board record", () => {
expect(process.exitCode ?? 0).toBe(0);
});

it("is recorded after the measurement it holds: recorded_at is not before measured_at", async () => {
const { repo, input } = await judged();
capture();
// Each reading of the clock a second later than the one before.
let tick = 0;
const result = await doRunBoardRecord(
{},
{ ...fed(repo, input), nowProvider: () => new Date(NOW.getTime() + 1000 * tick++) },
);
const written = JSON.parse(await readFile(join(repo, result.record ?? ""), "utf8"));
expect(Date.parse(written.recorded_at)).toBeGreaterThan(
Date.parse(written.measure.measured_at),
);
});

it("records a version 2 board with exactly the observations it declares", async () => {
const board = JSON.parse(boardYaml([MD]));
board.board_version = 2;
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/commands/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,9 +561,10 @@ async function checkAndRecord(
.join("\n")}`,
);
}
const recordedAt = ctx.nowProvider?.() ?? new Date();
const previous = await previousOf(loaded);
const measurement = await measureLoaded(loaded, ctx, parsed.input.judged_by.model, previous);
// After the measurement it holds: a record is not recorded before it is measured.
const recordedAt = ctx.nowProvider?.() ?? new Date();
if (measurement.digest !== parsed.input.measure_digest) {
throw new RecordRefusal(
`measure_digest is ${parsed.input.measure_digest}, but the board measures ${measurement.digest} now: something it measures or board.yaml changed since the judgement, or basou was rebuilt or upgraded since that measurement. Measure again and judge that; nothing was written.`,
Expand Down Expand Up @@ -1282,7 +1283,9 @@ function printMeasurementText(m: BoardMeasurement, hasRecords: boolean): void {
const width = Math.max(...measures.map(([id]) => id.length));
lines.push("", "Measures:");
for (const [id, measure] of measures) {
const unit = measure.value === null ? "" : ` ${displayPath(measure.unit)}`;
// A string (a file's name, a capture) is no amount: it takes no unit.
const counted = typeof measure.value === "number" || Array.isArray(measure.value);
const unit = counted ? ` ${displayPath(measure.unit)}` : "";
const listed = Array.isArray(measure.value) ? " (paths in --json)" : "";
const lane = measure.lane === undefined ? "" : ` [${measure.lane.join(", ")}]`;
lines.push(` ${id.padEnd(width)} ${shownValue(measure.value)}${unit}${listed}${lane}`);
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/commands/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1579,17 +1579,20 @@ describe("basou view: the board page", () => {
{ id: "latest", value: "20260401", unit: "name" },
{ id: "migrations", value: ["db/a.sql", "db/<b>.sql"], unit: "files" },
{ id: "gone", value: null, unit: "files" },
{ id: "pages", value: 1200, unit: "pages" },
],
flags: { live: false, blocked: false, unverified: false },
},
],
});
const items = drawn.find((n) => n.tag === "li" && n.find((c) => c.tag === "li").length === 1);
// A string is no amount: it is drawn without the unit, a number with it.
expect(items.map((n) => n.text())).toEqual([
"latest: 20260401 name",
"latest: 20260401",
"db/a.sql",
"db/<b>.sql",
"gone: not measured",
"pages: 1,200 pages",
]);
const [details] = drawn.find((n) => n.tag === "details");
expect(details?.attrs.open).toBeUndefined();
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/lib/board-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,8 @@ export const BOARD_HTML = `<!doctype html>
el('ul', null, m.value.map(function (p) { return el('li', { text: p }); }))
])]);
}
return el('li', { text: m.id + ': ' + num(m.value) + (m.value === null ? '' : ' ' + m.unit) });
// A string (a file's name, a capture) is no amount: it takes no unit.
return el('li', { text: m.id + ': ' + num(m.value) + (typeof m.value === 'number' ? ' ' + m.unit : '') });
})));
}
return el('div', { class: 'lane', 'data-lane': lane.id }, body);
Expand Down
Loading
Loading