diff --git a/CHANGELOG.md b/CHANGELOG.md index 1905d36..de64b43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,22 @@ All notable changes to **basou** are recorded here. The project follows keeps its milestones, which a start among them did not fit. A moved board keeps the milestones it had that happened, and leaves out those that were only planned. +- **A record's board page draws the reach matrix right after the summary's + tiles**, then where each lane is, then the observations outside in a + section of their own, then the period and effort and the rest as before. + The summary held the observations, and the matrix came after them and + after the charts of the effort, about 4,500px down a page 800px wide. The + summary keeps its prose and its tiles, and the observations their rows, + their notes and their `reported` mark, now on their section's heading. The + board measured on the spot, which draws no matrix, keeps its order. The + list in `basou board guide` of what the page draws, and what the agent + writes for it, follows the new order. +- **A record's board page folds the components counted in no lane** under + one line that says how many they are (`Counted in no lane (123)`), closed + until it is opened. A board that registers most of its components with + `lane: "-"` had them fill about a third of the page. When there is none, + the page says so as before; the components of each lane, those found and + not registered, and those gone are drawn open as before. ### Fixed @@ -89,6 +105,14 @@ All notable changes to **basou** are recorded here. The project follows as the command quotes it, and a plain name (`owner/repo`, `quality.yml`, `main`) is written as before, so the sources of existing boards change only where a name needed quoting. +- **The board page's reach matrix fits a narrow screen.** On a screen 600px + wide or less, each stage's head shows its id alone, the stages' meanings + are listed under the matrix, the cells take less room, and a long lane name + wraps, so that the six stages and a lane name of 16 characters fit a phone + 375px wide. The matrix was 479px wide in the 343px left for it there, with + stages 05 and 06 out of sight to the right. Each stage's head also carries + its meaning as its title at every width, which a screen reader reads once + and a pointer that hovers shows. A wider screen draws the matrix as before. ## 0.72.0 — 2026-10-20 diff --git a/packages/cli/src/commands/view.test.ts b/packages/cli/src/commands/view.test.ts index d5ce45c..7900322 100644 --- a/packages/cli/src/commands/view.test.ts +++ b/packages/cli/src/commands/view.test.ts @@ -1420,6 +1420,7 @@ describe("basou view: the board page", () => { "setStatus", "heading", "summary", + "observations", "effort", "matrix", "lanes", @@ -1462,12 +1463,15 @@ describe("basou view: the board page", () => { const words = boardPageStrings("en").effort; call("draw", board); expect(document.title).toBe("Board"); - // Every section of a record, the repos and the components it measured among them. + // Every section of a record, the repos and the components it measured + // among them: the matrix and the lanes right after the summary, then the + // observations in a section of their own, then the effort. expect(byId.board?.find((n) => n.tag === "h2").map((n) => n.textContent)).toEqual([ "Summary", - "Period and effort", "Reach matrix", "Where each lane is", + "Observed outside", + "Period and effort", "Measures", "Composition", "The operator's turns", @@ -1539,10 +1543,52 @@ describe("basou view: the board page", () => { expect(none.map((o) => [o.attrs.value, o.disabled])).toEqual([["", true]]); }); + it("draws the summary's prose and tiles, and the observations apart from them, each marked as reported", () => { + const { call } = drawing(["summary", "observations"]); + const S = boardPageStrings("en"); + const made = { observed_at: "2026-10-07T00:00:00Z", source: "s" }; + const board = { + summary: { + text: "All `well`.", + tiles: [{ key: "live_lanes", value: 2, detail: 3, reported: true }], + previous_record: "unreadable", + observed: [ + { name: "npm", value: "1.2.3", notes: ["mind the `tag`"], ...made }, + { name: "site", value: null, error: "timeout", ...made }, + ], + }, + }; + const summary = call("summary", board); + expect(summary.children[0]?.textContent).toBe(S.sections.summary); + expect(summary.find((n) => n.className === "tile")).toHaveLength(1); + // The prose's mark and the tile's: no observation, no heading of them. + expect(summary.find((n) => n.className === "reported")).toHaveLength(2); + expect(summary.find((n) => n.tag === "table" || n.tag === "h3")).toEqual([]); + expect(summary.text()).not.toContain(S.observed.heading); + expect(summary.text()).not.toContain(S.observed.previousUnreadable); + const observations = call("observations", board); + const [head] = observations.children; + expect([head?.tag, head?.textContent]).toEqual(["h2", S.observed.heading]); + expect(head?.find((n) => n.className === "reported")).toHaveLength(1); + // Its rows and the notes under them, and the line on the previous record. + const rows = observations.find((n) => n.tag === "tr").slice(1); + expect(rows.map((r) => [r.className, r.children[0]?.text()])).toEqual([ + ["", "npm"], + ["notes", "mind the tag"], + ["", "site"], + ]); + expect(observations.text()).toContain(S.observed.previousUnreadable); + expect(observations.find((n) => n.className === "tile")).toEqual([]); + expect(observations.text()).not.toContain("All "); + const none = call("observations", { summary: { ...board.summary, observed: [] } }); + expect(none.text()).toContain(S.observed.none); + expect(none.text()).not.toContain(S.observed.previousUnreadable); + }); + it("draws an http observation's words beside what it found of each, in the order declared", () => { - const { call } = drawing(["summary"]); + const { call } = drawing(["observations"]); const made = { observed_at: "2026-10-07T00:00:00Z", source: "s" }; - const drawn = call("summary", { + const drawn = call("observations", { summary: { text: "", tiles: [], @@ -1596,11 +1642,11 @@ describe("basou view: the board page", () => { }); it("draws an http value in its shape as its parts, what each observation is and looks at, and its notes", () => { - const { call } = drawing(["summary"]); + const { call } = drawing(["observations"]); const DOT = ` ${String.fromCodePoint(0xb7)} `; const [YES, NO] = [String.fromCodePoint(0x2713), String.fromCodePoint(0x2717)]; const made = { observed_at: "2026-10-07T00:00:00Z", source: "https://example.com/x" }; - const drawn = call("summary", { + const drawn = call("observations", { summary: { text: "", tiles: [], @@ -1687,7 +1733,7 @@ describe("basou view: the board page", () => { }); it("marks an observation that moved since the previous record, with what that record held", () => { - const { call } = drawing(["summary"]); + const { call } = drawing(["observations"]); const DOT = ` ${String.fromCodePoint(0xb7)} `; const [YES, NO] = [String.fromCodePoint(0x2713), String.fromCodePoint(0x2717)]; const made = { observed_at: "2026-10-07T00:00:00Z", source: "s" }; @@ -1697,9 +1743,9 @@ describe("basou view: the board page", () => { final_url: "https://example.com/", body: { words: [{ word, found }] }, }); - const summary = (previous_record: string, observed: unknown[]) => - call("summary", { summary: { text: "", tiles: [], previous_record, observed } }); - const drawn = summary("found", [ + const observations = (previous_record: string, observed: unknown[]) => + call("observations", { summary: { text: "", tiles: [], previous_record, observed } }); + const drawn = observations("found", [ { name: "npm", value: "1.1.0", moved: { status: "value", value: "1.0.0" }, ...made }, // Its type alone moved: both as JSON would tell them apart. { name: "count", value: 12, moved: { status: "value", value: "12" }, ...made }, @@ -1759,10 +1805,10 @@ describe("basou view: the board page", () => { expect(drawn.find((n) => n.className === "chip changed")).toHaveLength(8); const unreadable = boardPageStrings("en").observed.previousUnreadable; expect(drawn.text()).not.toContain(unreadable); - expect(summary("unreadable", [{ name: "npm", value: "1", ...made }]).text()).toContain( + expect(observations("unreadable", [{ name: "npm", value: "1", ...made }]).text()).toContain( unreadable, ); - expect(summary("unreadable", []).text()).not.toContain(unreadable); + expect(observations("unreadable", []).text()).not.toContain(unreadable); }); it("draws every measure in a table, with its lanes and how much a number moved", () => { @@ -1895,9 +1941,19 @@ describe("basou view: the board page", () => { gone: ["app/db"], }, }); - const flow = drawn.children - .slice(1) - .map((n) => (n.tag === "ul" ? n.children.map((li) => li.text()) : n.text())); + const items = (ul: Node | undefined) => ul?.children.map((li) => li.text()); + // Those in no lane folded, closed, under how many they are. + const flow = drawn.children.slice(1).map((n) => + n.tag === "ul" + ? items(n) + : n.tag === "details" + ? { + open: n.attrs.open, + summary: [n.children[0]?.tag, n.children[0]?.text()], + items: items(n.children[1]), + } + : n.text(), + ); expect(flow).toEqual([ "Core", [ @@ -1907,13 +1963,31 @@ describe("basou view: the board page", () => { ], "Docs", S.record.noComponentInLane, - S.record.uncounted, - ["app/scripts markertooling"], + { + open: undefined, + summary: ["summary", "Counted in no lane (1)"], + items: ["app/scripts markertooling"], + }, S.record.unacknowledged, ["app/new ci"], S.record.gone, ["app/db"], ]); + // Only they are folded. + expect(drawn.find((n) => n.tag === "details")).toHaveLength(1); + const many = call("recordComponents", { + components: { + measured: true, + reasons: [], + lanes: [], + uncounted: Array.from({ length: 1234 }, (_, i) => ({ key: `c${i}`, kinds: ["marker"] })), + unacknowledged: [], + gone: [], + }, + }); + const [folded] = many.find((n) => n.tag === "details"); + expect(folded?.children[0]?.text()).toBe("Counted in no lane (1,234)"); + expect(folded?.find((n) => n.tag === "li")).toHaveLength(1234); const notMeasured = call("recordComponents", { components: { measured: false, @@ -1937,11 +2011,95 @@ describe("basou view: the board page", () => { }, }); expect(unknown.text()).toContain(S.record.goneUnknown); + // None in no lane: said under its heading, with nothing to fold. + expect(unknown.find((n) => n.tag === "details")).toEqual([]); + expect(unknown.children.slice(1, 3).map((n) => [n.tag, n.text()])).toEqual([ + ["h3", S.record.uncounted], + ["p", S.record.none], + ]); expect(call("recordComponents", { components: null }).text()).toContain( S.record.componentsUnread, ); }); + it("puts each stage's meaning in its head's title, and draws a stage by its id alone on a narrow screen, with the meanings listed under the matrix", () => { + const { call } = drawing(["matrix"]); + const drawn = call("matrix", { + matrix: { + anomalies: [], + stages: [ + { id: "01", meaning: "Concept" }, + { id: "02", meaning: "Spec x" }, + ], + lanes: [{ id: "core", name: "Core", cells: [{ state: "done" }, { state: "none" }] }], + }, + }); + const stages = drawn.find((n) => n.tag === "th" && n.className === "stage"); + expect(stages.map((th) => th.attrs.title)).toEqual(["Concept", "Spec x"]); + // The id, and the meaning in an element of its own, which a narrow screen hides. + expect(stages.map((th) => th.children.map((c) => [c.tag, c.className, c.text()]))).toEqual([ + [ + ["b", "", "01"], + ["span", "meaning", "Concept"], + ], + [ + ["b", "", "02"], + ["span", "meaning", "Spec x"], + ], + ]); + // A screen reader reads the meaning once, as the title, not again as the text. + expect(stages.map((th) => th.children[1]?.attrs["aria-hidden"])).toEqual(["true", "true"]); + // The meanings listed under the matrix, as text. + const [listed] = drawn.find((n) => n.tag === "ul" && n.className === "stage-meanings"); + expect(listed?.children.map((li) => li.text())).toEqual(["01 Concept", "02 Spec x"]); + // The page's style, without its comments, and the rules of a part of it. + const style = BOARD_HTML.slice( + BOARD_HTML.indexOf(""), + ).replace(/\/\*[\s\S]*?\*\//g, ""); + const rules = (css: string) => + Object.fromEntries( + [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((m) => [m[1]?.trim(), m[2]?.trim()]), + ); + const opener = "@media (max-width: 600px) {"; + const start = style.indexOf(opener); + expect(start).toBeGreaterThan(-1); + const end = style.indexOf("\n }", start) + "\n }".length; + const narrow = rules(style.slice(start + opener.length, end - 1)); + expect(narrow).toEqual({ + "table.matrix th.stage": "min-width: 0; padding: 4px 2px;", + "table.matrix th.stage .meaning": "display: none;", + "table.matrix td.cell": "font-size: 16px; padding: 4px 2px;", + // A long lane name wraps, in a column of a width it cannot pass. + "table.matrix th:first-child": "max-width: 10em; overflow-wrap: anywhere;", + ".stage-meanings": "display: block;", + }); + // Its rules take the place of those of the same weight only by coming after them. + for (const selector of [ + "table.matrix th.stage {", + "table.matrix td.cell {", + ".stage-meanings {", + ]) { + const at = style.indexOf(selector); + expect(at, selector).toBeGreaterThan(-1); + expect(at, selector).toBeLessThan(start); + } + expect(style.indexOf("table.matrix", end)).toBe(-1); + expect(style.indexOf(".stage-meanings", end)).toBe(-1); + // Wider screens draw the matrix as before: its meanings in the heads, no list. + const wide = rules(style.slice(0, start) + style.slice(end)); + expect(wide["table.matrix th.stage"]).toContain("min-width: 64px;"); + expect(wide["table.matrix td.cell"]).toContain("font-size: 18px;"); + expect(Object.keys(wide).filter((selector) => selector.includes("meaning"))).toEqual([ + ".stage-meanings", + ]); + expect(wide[".stage-meanings"]).toContain("display: none;"); + for (const [selector, body] of Object.entries(wide)) { + if (selector.includes("th.stage")) expect(body, selector).not.toContain("display: none"); + } + expect(wide[".scroll"]).toContain("overflow-x: auto;"); + }); + it("draws the paths of a file_paths measure as a count, with the paths folded under it", () => { const { call } = drawing(["lanes"]); const drawn = call("lanes", { diff --git a/packages/cli/src/lib/board-ui.ts b/packages/cli/src/lib/board-ui.ts index c8b7372..7f99b4b 100644 --- a/packages/cli/src/lib/board-ui.ts +++ b/packages/cli/src/lib/board-ui.ts @@ -69,6 +69,17 @@ export const BOARD_HTML = ` table.matrix th.stage { font-weight: 400; font-size: 12px; text-align: center; min-width: 64px; } table.matrix th.stage b { display: block; font-size: 13px; } table.matrix td.cell { text-align: center; font-size: 18px; line-height: 1; } + .stage-meanings { display: none; list-style: none; padding: 0; margin: 6px 0 0; font-size: 12px; color: var(--fg-2); } + /* Six stages do not fit beside their meanings below about 600px: each is + drawn by its id, the meanings are listed under the matrix, and a lane + name wraps. After the rules above, which it overrides. */ + @media (max-width: 600px) { + table.matrix th.stage { min-width: 0; padding: 4px 2px; } + table.matrix th.stage .meaning { display: none; } + table.matrix td.cell { font-size: 16px; padding: 4px 2px; } + table.matrix th:first-child { max-width: 10em; overflow-wrap: anywhere; } + .stage-meanings { display: block; } + } .st::before { display: inline-block; width: 1.2em; } .st.done::before { content: "\\25CF"; color: var(--done); } .st.part::before { content: "\\25D0"; color: var(--done); } @@ -357,6 +368,13 @@ export const BOARD_HTML = ` k, el('div', { class: 'v', text: num(t.value) }), sub === null ? null : el('div', { class: 's', text: sub }) ])); }); + return section(S.sections.summary, false, [el('div', null, [prose(s.text), reported()]), tiles]); + } + + // The observations outside, which the record holds in its summary, in a + // section of their own after the lanes. + function observations(b) { + var s = b.summary; var observed; if (s.observed.length === 0) { observed = el('p', { class: 'muted', text: S.observed.none }); @@ -399,17 +417,11 @@ export const BOARD_HTML = ` ]) ].concat(rows))]); } - var observedHeading = el('h3', { text: S.observed.heading }, [reported()]); - var children = [ - el('div', null, [prose(s.text), reported()]), - tiles, - observedHeading, - observed - ]; + var children = [observed]; if (s.previous_record === 'unreadable' && s.observed.length > 0) { children.push(el('p', { class: 'muted', text: S.observed.previousUnreadable })); } - return section(S.sections.summary, false, children); + return section(S.observed.heading, true, children); } var SVGNS = 'http://www.w3.org/2000/svg'; @@ -696,8 +708,13 @@ export const BOARD_HTML = ` }) ))); } + // The meaning is the head's title too, as a screen reader reads it and a + // pointer that hovers shows it, and the text beside the id is left out of + // what a screen reader reads, so that it is read once. var head = el('tr', null, [el('th', { text: S.matrix.lane })].concat(x.stages.map(function (s) { - return el('th', { class: 'stage' }, [el('b', { text: s.id }), s.meaning]); + return el('th', { class: 'stage', title: s.meaning }, [ + el('b', { text: s.id }), el('span', { class: 'meaning', 'aria-hidden': 'true', text: s.meaning }) + ]); }))); var rows = x.lanes.map(function (lane) { return el('tr', null, [el('th', { text: lane.name })].concat(lane.cells.map(function (c) { @@ -709,6 +726,11 @@ export const BOARD_HTML = ` }))); }); children.push(el('div', { class: 'scroll' }, [el('table', { class: 'matrix' }, [head].concat(rows))])); + // What each stage means, drawn where a narrow screen gives a stage its id + // alone (and hidden where the heads say it). + children.push(el('ul', { class: 'stage-meanings' }, x.stages.map(function (s) { + return el('li', null, [el('b', { text: s.id }), ' ' + s.meaning]); + }))); var legend = el('div', { class: 'legend' }); ['done', 'part', 'blocked', 'shelved', 'none', 'unverified'].forEach(function (s) { legend.appendChild(el('span', null, [el('span', { class: 'st ' + s }), stateName(s)])); @@ -864,8 +886,10 @@ export const BOARD_HTML = ` var b = page.board; // The board's own title, so that boards open in other tabs can be told apart. document.title = b.heading.title; - [heading(b), summary(b), effort(b, false), matrix(b), lanes(b), measures(b), composition(b), turns(b), - recordRepos(b), recordComponents(b), footnotes(b)] + // Where each lane stops, right under the summary's tiles, and the lanes, + // before the observations and the charts of the effort. + [heading(b), summary(b), matrix(b), lanes(b), observations(b), effort(b, false), measures(b), composition(b), + turns(b), recordRepos(b), recordComponents(b), footnotes(b)] .forEach(function (node) { root.appendChild(node); }); } @@ -936,7 +960,7 @@ export const BOARD_HTML = ` return section(S.live.repos, false, children); } // The components the record found, by the lanes its registry counts them - // in, then those in no lane, those not registered, and those gone. + // in, then those in no lane (folded), those not registered, and those gone. function recordComponents(b) { var c = b.components; if (!c) return section(S.live.components, false, [el('p', { class: 'muted', text: S.record.componentsUnread })]); @@ -962,7 +986,15 @@ export const BOARD_HTML = ` c.lanes.forEach(function (lane) { children = children.concat(group(lane.name, lane.components, S.record.noComponentInLane)); }); - children = children.concat(group(S.record.uncounted, c.uncounted, S.record.none)); + // Those in no lane can be most of them: folded under how many they are. + if (c.uncounted.length > 0) { + children.push(el('details', null, [ + el('summary', { text: fill(S.record.uncountedFolded, { n: num(c.uncounted.length) }) }), + el('ul', null, c.uncounted.map(item)) + ])); + } else { + children = children.concat(group(S.record.uncounted, c.uncounted, S.record.none)); + } children = children.concat(group(S.record.unacknowledged, c.unacknowledged, S.record.none)); children.push(el('h3', { text: S.record.gone })); children.push(c.gone === null ? el('p', { class: 'muted', text: S.record.goneUnknown }) diff --git a/packages/core/src/board/guide.test.ts b/packages/core/src/board/guide.test.ts index 6f7cd2c..ea24c0a 100644 --- a/packages/core/src/board/guide.test.ts +++ b/packages/core/src/board/guide.test.ts @@ -1590,6 +1590,36 @@ describe("boardGuide: a github_check observation and an observation's notes", () } }); + it("lists what the board page draws in the order a record's page draws it", () => { + const guide = boardGuide( + guideInput({ + board: declared(httpDeclaration([{ key: "shop", kind: "http", url: SHOP }])), + }), + ); + const list = guide.slice( + guide.indexOf("## What the board page draws, and what you write"), + guide.indexOf("## Do not"), + ); + const items = list + .split("\n") + .filter((line) => /^\d+\. /.test(line)) + .map((line) => line.slice(0, line.indexOf(":"))); + expect(items).toEqual([ + "1. Heading", + "2. Summary", + "3. Reach matrix", + "4. Each lane", + "5. Observed outside", + "6. Period and effort", + "7. Ratios", + "8. Operator's turns", + "9. Footnotes", + ]); + // The summary draws its tiles; the observations are a section of their own. + expect(list).toContain("basou draws the tiles.\n"); + expect(list).not.toContain("table of observations"); + }); + it("prints each observation's notes under how it is made, and nothing for one without", () => { const guide = boardGuide( guideInput({ diff --git a/packages/core/src/board/guide.ts b/packages/core/src/board/guide.ts index 4d4c6f7..14f19b3 100644 --- a/packages/core/src/board/guide.ts +++ b/packages/core/src/board/guide.ts @@ -335,13 +335,14 @@ export function boardGuide(input: BoardGuideInput): string { "## What the board page draws, and what you write", "", "1. Heading: basou draws it (title, when recorded, the model, whether everything was measured).", - "2. Summary: `prose.summary`, what matters this time and what moved since the last record. basou draws the tiles and the table of observations.", - "3. Period and effort: basou draws it. Write nothing that gives the numbers a meaning.", - "4. Reach matrix: your `cells`. basou draws the legend, the order anomalies and the cells that moved.", - "5. Each lane: `prose.lanes`. For a stuck lane, one sentence on what would move it, with the measured values it rests on.", - "6. Ratios: basou draws them.", - "7. Operator's turns: `prose.operator_turns`, the items waiting for a ruling (not for implementation), the ones that move most first, each with its source (a decision or task id, a pull request) in `source`. This is what is read most: what the operator can do to move the board.", - "8. Footnotes: `prose.footnotes`, what was not confirmed, what is estimated, whether the trail was refreshed, a build that is not the repo's main. basou adds the axis, the model, what was not measured and which basou measured.", + "2. Summary: `prose.summary`, what matters this time and what moved since the last record. basou draws the tiles.", + "3. Reach matrix: your `cells`. basou draws the legend, the order anomalies and the cells that moved.", + "4. Each lane: `prose.lanes`. For a stuck lane, one sentence on what would move it, with the measured values it rests on.", + "5. Observed outside: your `observed`. basou draws each value as its kind reads, with what it looks at, its notes and whether it moved since the last record.", + "6. Period and effort: basou draws it. Write nothing that gives the numbers a meaning.", + "7. Ratios: basou draws them.", + "8. Operator's turns: `prose.operator_turns`, the items waiting for a ruling (not for implementation), the ones that move most first, each with its source (a decision or task id, a pull request) in `source`. This is what is read most: what the operator can do to move the board.", + "9. Footnotes: `prose.footnotes`, what was not confirmed, what is estimated, whether the trail was refreshed, a build that is not the repo's main. basou adds the axis, the model, what was not measured and which basou measured.", "", "## Do not", "", diff --git a/packages/core/src/lib/view-strings.ts b/packages/core/src/lib/view-strings.ts index 44e7e9b..6e6af60 100644 --- a/packages/core/src/lib/view-strings.ts +++ b/packages/core/src/lib/view-strings.ts @@ -962,6 +962,8 @@ export type BoardPageStrings = { componentsNotMeasured: string; noComponentInLane: string; uncounted: string; + /** The line the components in no lane are folded under, with `{n}` of them. */ + uncountedFolded: string; unacknowledged: string; gone: string; goneUnknown: string; @@ -1172,6 +1174,7 @@ const BOARD_PAGE_EN: BoardPageStrings = { componentsNotMeasured: "The components were not measured:", noComponentInLane: "No component is registered in this lane.", uncounted: "Counted in no lane", + uncountedFolded: "Counted in no lane ({n})", unacknowledged: "Found, not registered", gone: "Registered or found before, not found now", goneUnknown: "not known (the previous record could not be read)", @@ -1383,6 +1386,7 @@ const BOARD_PAGE_JA: BoardPageStrings = { componentsNotMeasured: "構成要素は測れなかった:", noComponentInLane: "このレーンに登録した構成要素はない。", uncounted: "どのレーンにも数えない", + uncountedFolded: "どのレーンにも数えない({n})", unacknowledged: "見つかったが登録していない", gone: "登録していた・前回見つかったが、今回見つからない", goneUnknown: "分からない(前回の記録が読めない)",