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
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,47 @@
All notable changes to **basou** are recorded here. The project follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) starting with v0.1.0.

## Unreleased

### Fixed

- **`basou board measure` (experimental) counts how far HEAD is behind
origin's default branch, not always `origin/main`.** It read only
`refs/remotes/origin/main`, so a repository whose default branch is
`master`, or any other name, never had a `behind_main`, and the
measurement still said it was complete, as it does for a repository with
no origin. It now counts against the branch of origin that
`refs/remotes/origin/HEAD` names (a clone sets it, and
`git remote set-head origin --auto` sets it later), and against
`origin/main` when `origin/HEAD` names none (it is not set, or is not a
symbolic ref), as before. The value is not measured, and `not_found` says
why, when `origin/HEAD` names a branch that is not there (as after the
default branch is renamed and a fetch prunes the old one), names a ref
outside `refs/remotes/origin/`, or cannot be read by git; it then does not
fall back to `origin/main`. The `repos` method is now 2, so the next record
marks `behind_main` as measured another way. `board measure` and the board
page say "behind origin's default branch".
- **`basou board record` (experimental) names a record by its own time.**
The record's ULID took the time it was written, after the board was
measured again, while `recorded_at` took the time the command started, so
the board page's list of records and the guide gave a time a second or so
later than the record's heading. The ULID now carries `recorded_at`, from
a monotonic factory of its own, so records with the same `recorded_at`
still sort in the order they were written. Records written before keep
their names.
- **`basou board guide` (experimental) says how to ask for a manual
observation, and what to write when the operator hands it over.** It said
only to ask the operator and not to make it. It now asks for one thing the
operator can do as it is written (the line to run and where to run it, or
the page to open, and what to send back), and, when the operator says to
make it, to make it and say so in the source.
- **`basou board guide` and `basou board init` (experimental) ask for
decision and task ids in full.** A shortened id in a board's declaration
matched two decisions, the first of them voided. The guide asks for
decision and task ids with all 26 characters in the prose, and for a
decision id in full in a milestone's `ref`; the comment `init` prints on
milestones says the same.

## 0.70.0 — 2026-10-16

### Changed
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/board.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ describe("basou board measure", () => {
const unborn = out.join("\n");
expect(unborn).toMatch(
new RegExp(
`Repos:\\n \\. \\(${name}\\) main at no commit yet, last commit none, commits 0, files \\d+, uncommitted \\d+, behind origin/main \\(no origin/main\\)\\n \\.\\./gone not measured\\n`,
`Repos:\\n \\. \\(${name}\\) main at no commit yet, last commit none, commits 0, files \\d+, uncommitted \\d+, behind origin's default branch \\(origin/HEAD names no branch, and there is no origin/main\\)\\n \\.\\./gone not measured\\n`,
),
);
expect(unborn).toContain("repos[../gone]: the repo '../gone' is not on disk");
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1090,7 +1090,7 @@ function shownValue(value: number | string | null): string {

// One line per repository. A null the not_found entries do not explain has
// the meaning the measurement gives it (a detached HEAD, no commit yet, no
// origin/main).
// default branch: origin/HEAD names none and there is no origin/main).
function repoLines(m: BoardMeasurement): string[] {
const missing = new Set(m.not_found.map((n) => n.at));
return m.repos.map((repo) => {
Expand All @@ -1110,7 +1110,7 @@ function repoLines(m: BoardMeasurement): string[] {
`commits ${measured("commits", String) ?? "not measured"}`,
`files ${measured("files", String) ?? "not measured"}`,
`uncommitted ${measured("uncommitted", String) ?? "not measured"}`,
`behind origin/main ${measured("behind_main", String) ?? "(no origin/main)"}`,
`behind origin's default branch ${measured("behind_main", String) ?? "(origin/HEAD names no branch, and there is no origin/main)"}`,
];
return ` ${displayPath(repo.path)}${name} ${parts.join(", ")}`;
});
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1910,7 +1910,7 @@ describe("basou view: the board page", () => {

const repos = call("liveRepos", LIVE);
const cells = repos.find((n) => n.tag === "td").map((c) => c.textContent);
// Nulls that mean so: a detached HEAD, no origin/main, a last commit unread.
// Nulls that mean so: a detached HEAD, no default branch, a last commit unread.
expect(cells).toEqual(["app", "-", "0123456", "-", "2", "1", "-"]);
// Nulls the measurement could not measure are said to be.
const unmeasured = {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lib/board-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ export const BOARD_HTML = `<!doctype html>
var rows = l.repos.map(function (r) {
var at = 'repos[' + r.path + ']';
// A null the measurement says it could not measure, or one that means
// so (a detached HEAD, no commit yet, no origin/main), drawn as '-'.
// so (a detached HEAD, no commit yet, no default branch), drawn as '-'.
var value = function (field, v, show) {
if (v !== null) return show(v);
return missing(at) || missing(at + '.' + field) ? S.tiles.notMeasured : '-';
Expand Down
28 changes: 27 additions & 1 deletion packages/core/src/board/guide.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,32 @@ describe("boardGuide: a board declared", () => {
);
});

it("says how to ask for a manual observation, and what the source says when the operator hands it over", () => {
const guide = boardGuide(guideInput());
expect(guide).toContain(
"Ask for one thing they can do as it is written: the line to run and where to run it, or the page to open, and what to send back.",
);
expect(guide).toContain(
"When the operator tells you to make it yourself, make it as they said and write that in the source instead, such as `ran <the command> as the operator asked`.",
);
});

it("asks for decision and task ids in full, in the prose and in a milestone's ref", () => {
const guide = boardGuide(guideInput());
expect(guide).toContain(
"Write a decision or task id in full, all 26 characters after `decision_` or `task_`: basou prints some of them short, and a short one can match more than one, a voided decision among them.",
);
expect(guide).toContain(
"with `ref` saying where it can be checked (a tag and its commit, or a decision id in full).",
);
});

it("says which branch behind_main counts against", () => {
expect(boardGuide(guideInput())).toContain(
"- `repos[].behind_main` counts the commits on origin's default branch (the branch of origin that origin/HEAD names, or origin/main when origin/HEAD names none) as of the last fetch; measure does not fetch.",
);
});

it("leaves the model to be named, in the commands and the template, so neither runs as printed", () => {
const guide = boardGuide(guideInput());
const blocks = blocksIn(guide).join("\n");
Expand Down Expand Up @@ -553,7 +579,7 @@ describe("boardGuide: an observation not made, the milestones, isolation and wha
}),
);
const holds =
"under effort.milestones, each `{ date, label, ref }`: only what was reached (a release, a launch), with `ref` saying where it can be checked (a tag and its commit). The page marks them on the period and effort chart from the next record on: a record draws the declaration it kept. Add one with the operator's approval, before the measure of step 2 or after the record of step 7, never between: the digest covers board.yaml.";
"under effort.milestones, each `{ date, label, ref }`: only what was reached (a release, a launch), with `ref` saying where it can be checked (a tag and its commit, or a decision id in full). The page marks them on the period and effort chart from the next record on: a record draws the declaration it kept. Add one with the operator's approval, before the measure of step 2 or after the record of step 7, never between: the digest covers board.yaml.";
expect(boardGuide(guideInput())).toContain(`- Milestones: none declared ${holds}`);
expect(boardGuide(guideInput())).toContain(
"- Approving a milestone before it is added to board.yaml.",
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/board/guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export function boardGuide(input: BoardGuideInput): string {
line(`- Records: board/records/, ${recordsLine(input, recordCount)}`);
line(`- Axis: ${axisLine(input)}`);
line(
`- Milestones: ${milestonesLine(d)} under effort.milestones, each \`{ date, label, ref }\`: only what was reached (a release, a launch), with \`ref\` saying where it can be checked (a tag and its commit). The page marks them on the period and effort chart from the next record on: a record draws the declaration it kept. Add one with the operator's approval, before the measure of step 2 or after the record of step 7, never between: the digest covers board.yaml.`,
`- Milestones: ${milestonesLine(d)} under effort.milestones, each \`{ date, label, ref }\`: only what was reached (a release, a launch), with \`ref\` saying where it can be checked (a tag and its commit, or a decision id in full). The page marks them on the period and effort chart from the next record on: a record draws the declaration it kept. Add one with the operator's approval, before the measure of step 2 or after the record of step 7, never between: the digest covers board.yaml.`,
);
line(
`- Language: write every prose string and reason in ${LANGUAGE_NAMES[input.language]}, the language this board's page is drawn in (Japanese when the manifest declares ja for this repo, English otherwise)`,
Expand Down Expand Up @@ -189,7 +189,7 @@ export function boardGuide(input: BoardGuideInput): string {
"- An empty file means it did not run, not that it passed: check that measure.json reads as JSON.",
'- Never read a null as 0. Write "not confirmed" for it and do not judge by it.',
"- What moved is the point: the top of measure.txt (`Since the previous record <ULID>:`) and `diff` in the JSON. A value marked `method_changed` may have moved because basou measures it another way now.",
"- `repos[].behind_main` counts from the last fetch; measure does not fetch.",
"- `repos[].behind_main` counts the commits on origin's default branch (the branch of origin that origin/HEAD names, or origin/main when origin/HEAD names none) as of the last fetch; measure does not fetch.",
"",
);

Expand Down Expand Up @@ -245,7 +245,7 @@ export function boardGuide(input: BoardGuideInput): string {
"- `measure_digest`: the `digest` of `$W/measure.json`.",
'- `observed`: when the board declares observations, one entry per declared observation, under its key, each as step 3 says; nothing else and nothing left out, or record refuses it. An observation made has no `error`; one not made has `"value": null`, an `error` and the `observed_at` of when it was tried. The template leaves each `observed_at` empty: fill every one. With none declared, `observed` may stay empty.',
"- `cells`: every lane at every stage. Set each `state` and delete each `previous`: record refuses a cell with an empty state or a `previous`. `blocked`, `shelved` and `unverified` need a `reason`.",
`- \`prose\`: \`summary\`, \`lanes\`, \`operator_turns\` as \`{ "text", "source" }\` and \`footnotes\`, all plain strings (HTML is shown as text; only \`\` \`code\` \`\` is drawn as code). Do not repeat what basou draws (the heading, the tiles, the period and effort, the matrix, the ratios, the axis version and the model). Do not start a lane's prose with its \`about\`: the page draws the about right before it. Give \`lanes\` an entry only for a lane you have something to say about (an empty string is drawn as an empty paragraph); the lane ids are ${d.lanes.map((l) => `\`${l.id}\``).join(", ")}.`,
`- \`prose\`: \`summary\`, \`lanes\`, \`operator_turns\` as \`{ "text", "source" }\` and \`footnotes\`, all plain strings (HTML is shown as text; only \`\` \`code\` \`\` is drawn as code). Do not repeat what basou draws (the heading, the tiles, the period and effort, the matrix, the ratios, the axis version and the model). Do not start a lane's prose with its \`about\`: the page draws the about right before it. Give \`lanes\` an entry only for a lane you have something to say about (an empty string is drawn as an empty paragraph); the lane ids are ${d.lanes.map((l) => `\`${l.id}\``).join(", ")}. Write a decision or task id in full, all 26 characters after \`decision_\` or \`task_\`: basou prints some of them short, and a short one can match more than one, a voided decision among them.`,
'- `axis_review`: null, or `{ "triggers": [...], "summary": "..." }` as step 2b says.',
"",
);
Expand Down Expand Up @@ -515,7 +515,7 @@ function observeSteps(
line(
`Only the operator can make this one: ${displayPath(o.how)}`,
"",
"Ask the operator, and do not make it yourself. With no answer this time, write a null, say so as the error, and give the time you asked as `observed_at`. Source: `the operator`.",
"Ask the operator, and do not make it yourself. Ask for one thing they can do as it is written: the line to run and where to run it, or the page to open, and what to send back. With no answer this time, write a null, say so as the error, and give the time you asked as `observed_at`. Source: `the operator`. When the operator tells you to make it yourself, make it as they said and write that in the source instead, such as `ran <the command> as the operator asked`.",
"",
);
break;
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/board/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ describe("boardInitText", () => {
);
expect(ja.comments.milestones).toContain("{ date: YYYY-MM-DD, label, ref }");
expect(ja.comments.milestones).toContain("他の盤から移すときは、その節目をここに移す。");
expect(en.comments.milestones).toContain(
"with ref saying where it can be checked (a tag and its commit, or a decision id in full)",
);
expect(ja.comments.milestones).toContain(
"ref に確かめられる出所(tag とその commit、または完全な decision の ID)を書く。",
);
});

it("keeps a name as written, whatever it holds", () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/board/live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ describe("measureBoardLive", () => {
].sort(),
);
expect(m.methods).toEqual({
repos: 1,
repos: 2,
trail: 2,
integrity: 2,
review_gaps: 2,
Expand Down
Loading
Loading