diff --git a/.abcd/work/issues/open/iss-2610040744587676-the-site-screenshots-workflow-s-path-filter-rests-on-a.md b/.abcd/work/issues/open/iss-2610040744587676-the-site-screenshots-workflow-s-path-filter-rests-on-a.md new file mode 100644 index 000000000..975f7bfe5 --- /dev/null +++ b/.abcd/work/issues/open/iss-2610040744587676-the-site-screenshots-workflow-s-path-filter-rests-on-a.md @@ -0,0 +1,15 @@ +--- +schema_version: 1 +id: "iss-2610040744587676" +slug: "the-site-screenshots-workflow-s-path-filter-rests-on-a" +severity: "minor" +category: "drift" +source: "agent-finding" +found_during: "site-screenshots failing on every PR since 2026-10-03 23:37Z" +origin: researcher-authored +production_mode: hand-written +found_at: ".github/workflows/site-screenshots.yml" +remedy: "Waits on a ruling on audit cost: either add the record stores (.abcd/development/** and .abcd/work/issues/**) to the pull_request paths so a record edit is audited on its own pull request, at the price of running the browser job on most pull requests, or keep the filter and rewrite the comment to state the residual honestly (a record edit can move layout; the wrapping nets in site.css bound the known class, and the next site-touching pull request is where anything else surfaces). Grounds: this incident, where the failure surfaced on pull requests that did not cause it." +--- + +The site-screenshots workflow's path filter rests on a premise this repository has falsified: its comment says a record edit under .abcd/ moves page content without moving the layout the audit measures, so record-only pull requests do not run the audit. #795 added two ADRs whose titles quote a path with no break in it, which moved the /record/ layout past 360 px; the audit did not run on #795 and first failed on the next pull request that touched site sources, an unrelated one, which then carried a red check it did not cause. The wrapping nets of iss-2610040729344770 and iss-2610040732240935 close the one class seen so far by construction, but the comment still states the premise as fact. diff --git a/.abcd/work/issues/resolved/iss-2610040729344770-the-record-dashboard-record-scrolls-sideways-at-360-and-390.md b/.abcd/work/issues/resolved/iss-2610040729344770-the-record-dashboard-record-scrolls-sideways-at-360-and-390.md new file mode 100644 index 000000000..07ff5c307 --- /dev/null +++ b/.abcd/work/issues/resolved/iss-2610040729344770-the-record-dashboard-record-scrolls-sideways-at-360-and-390.md @@ -0,0 +1,23 @@ +--- +schema_version: 1 +id: "iss-2610040729344770" +slug: "the-record-dashboard-record-scrolls-sideways-at-360-and-390" +severity: "major" +category: "bug" +source: "agent-finding" +found_during: "site-screenshots failing on every PR since 2026-10-03 23:37Z" +origin: researcher-authored +production_mode: hand-written +found_at: "site-src/site.css" +remedy: "Give the non-id spans of a .list row the treatment .links li>span already has: .list li>span:not(.id){overflow-wrap:anywhere;min-width:0} in site-src/site.css and its seeded copy internal/core/site/setupsrc/site.css, the id column keeping white-space:nowrap. Grounds: overflow-wrap:anywhere, unlike break-word, lowers the element's min-content width (CSS Text Module Level 3, overflow-wrap), and min-width:0 removes the grid item's content-based automatic minimum (CSS Grid Layout Level 1, section 6.6), so the 1fr track gives whatever title arrives; the stylesheet's own comment above .refs li records the same reasoning for bibliography addresses. A Go test pins the rule and the row markup it matches; the overflow audit proves it in a browser at 360/390/768/1360 px." +resolution: "Fixed by giving the non-id spans of a .list row overflow-wrap:anywhere and min-width:0, at the top level of site-src/site.css and its seeded copy. The genealogy timeline was not the cause: its svg sits in its own overflow-x:auto panel. Local overflow audit at base: /record/ fails at 360 and 390 px in both schemes (scrollWidth 482); after the fix it passes every route at all four widths in both schemes, every fold open included." +impact: fix +resolved_by: + commit: "e90a2a4a0" +--- + +The record dashboard (/record/) scrolls sideways at 360 and 390 px: the site-screenshots job measures scrollWidth 476 to 482 there on every pull request since 2026-10-03 23:37Z. The cause is not the genealogy timeline, whose svg sits inside its own overflow-x:auto panel and cannot widen the page, and not the four sub-navigation links the job names first, which scroll inside their own bar; it is the Latest decisions panel. A .list row is a two-column grid (auto 1fr) whose title is a plain span, and a grid item's automatic minimum width is its min-content width, so a title holding one long unspaced token sets the width of the row, which spills past the panel and the page. The two ADRs from #795 (adr-2610031751065746 and adr-2610031751066232) carry such titles, a home-relative path with no break in it, and became the two newest decisions on 2026-10-03. Every .list on the site (dashboard decisions, health, status, development, supersessions) has the same shape, so any record whose title holds a long token reproduces it. + +## Grounds + +- pursued: no .list row on any page widens the page at 360 px whatever its title holds; a site-screenshots run on this pull request that still reports /record/, /record/health/ or /record/development/ overflowing, or a .list title rendered outside a non-id span directly under its row, would show it wrong. diff --git a/.abcd/work/issues/resolved/iss-2610040732240935-record-pages-scroll-sideways-at-360-px-whenever-a-record-s.md b/.abcd/work/issues/resolved/iss-2610040732240935-record-pages-scroll-sideways-at-360-px-whenever-a-record-s.md new file mode 100644 index 000000000..f0a0d6dac --- /dev/null +++ b/.abcd/work/issues/resolved/iss-2610040732240935-record-pages-scroll-sideways-at-360-px-whenever-a-record-s.md @@ -0,0 +1,23 @@ +--- +schema_version: 1 +id: "iss-2610040732240935" +slug: "record-pages-scroll-sideways-at-360-px-whenever-a-record-s" +severity: "major" +category: "bug" +source: "agent-finding" +found_during: "site-screenshots failing on every PR since 2026-10-03 23:37Z" +origin: researcher-authored +production_mode: hand-written +found_at: "site-src/site.css" +remedy: "Set overflow-wrap:break-word on body in site-src/site.css and its seeded copy internal/core/site/setupsrc/site.css, as a net under every block of text: it breaks a token only where the line would otherwise overflow and, unlike anywhere, leaves min-content sizing alone, so tables, grids and flex rows lay out as before (CSS Text Module Level 3, overflow-wrap). Make the overflow audit also sample, per type, the record whose title or source path holds the longest run without a space or hyphen, so the worst case for wrapping is measured on every pull request rather than the shortest. A Go test pins the body rule; the audit run proves it in a browser." +resolution: "Fixed by overflow-wrap:break-word on body in site-src/site.css and its seeded copy, and by the overflow audit also sampling, per type, the record whose title or source path holds the longest run without a space or hyphen. Against the unfixed site that sample fails /record/adr/adr-2609021016270132/ at 360 px and /record/issue/iss-2609012039114437/ at every width up to 1360 px (scrollWidth 1569); after the fix all 104 renderings pass, and adr-2609091248200336 and the two quoting ADRs measure 360 at 360 px." +impact: fix +resolved_by: + commit: "cdf1def6a" +--- + +Record pages scroll sideways at 360 px whenever a record's title or source path holds a long unbroken token, and the overflow audit cannot see it. Every ADR minted with a timestamp id shows its source path, .abcd/development/decisions/adrs/<16 digits>-.md, as one mono span in the side panel's reclinks paragraph, and the run up to the first hyphen is about fifty characters with no break opportunity: /record/adr/adr-2609091248200336/ measures scrollWidth 374 at 360 px. adr-2610031751065746 and adr-2610031751066232 also carry a home-relative path in their titles, which overflows the record page's h1 (scrollWidth 455). The stylesheet sets no overflow-wrap on body, the title or reclinks, so a block whose text has nowhere to break widens the page. The site-screenshots audit samples only the lowest-numbered record of each type (adr-1, itd-1, iss-1), whose paths and titles are short, so these pages have overflowed since timestamp ADRs began without the job noticing. + +## Grounds + +- pursued: no record page widens the page because a title or source path has nowhere to break; a site-screenshots run that reports a record page overflowing on a text run, or a later stylesheet that drops the body rule (the Go test refuses it), would show it wrong. diff --git a/.abcd/work/issues/resolved/iss-2610040741103264-the-overflow-audit-names-the-wrong-culprits-when-a-page.md b/.abcd/work/issues/resolved/iss-2610040741103264-the-overflow-audit-names-the-wrong-culprits-when-a-page.md new file mode 100644 index 000000000..a923801fc --- /dev/null +++ b/.abcd/work/issues/resolved/iss-2610040741103264-the-overflow-audit-names-the-wrong-culprits-when-a-page.md @@ -0,0 +1,23 @@ +--- +schema_version: 1 +id: "iss-2610040741103264" +slug: "the-overflow-audit-names-the-wrong-culprits-when-a-page" +severity: "minor" +category: "bug" +source: "agent-finding" +found_during: "site-screenshots failing on every PR since 2026-10-03 23:37Z" +origin: researcher-authored +production_mode: hand-written +found_at: "site-src/audit/overflow-audit.js" +remedy: "In measure(), skip any element with an ancestor whose computed overflow-x is not visible (its overflow is contained there and cannot widen the document), and also walk text nodes, measuring each with a Range, naming the parent element of any unclipped text run past the edge. Grounds: CSS Overflow Module Level 3 makes a box whose overflow-x is not visible a scroll container, and content inside it contributes to that container's scrollable overflow rather than to its ancestors'; a local probe that applies exactly this filter named the real span on /record/ and the h1 text on the ADR page." +resolution: "Fixed in measure(): an element or text run inside an ancestor whose overflow-x is not visible is skipped, and text runs are measured by their Range and named by their parent element. Against the unfixed site the audit names the .list title span on /record/, the mono source-path span on a timestamp-id ADR page and the h1 text on an issue page, and no sub-navigation link or timeline svg." +impact: fix +resolved_by: + commit: "b89a52d71" +--- + +The overflow audit names the wrong culprits when a page scrolls sideways. measure() in site-src/audit/overflow-audit.js lists the first five elements whose box passes the viewport edge, including elements inside their own scrolling container (overflow-x auto, scroll or hidden), which cannot widen the page. On /record/ at 360 px it named the four sub-navigation links and the genealogy svg, both inside their own scroll containers, and never the Latest decisions title span that set scrollWidth 482, which sent the bisect after the timeline renderer. It also reports nothing when the overflow is a text run rather than an element box, as on a record page whose h1 holds a long unbroken path. + +## Grounds + +- pursued: every culprit the audit names can widen the document; a failing run whose culprits include an element inside a scroll container, or a text-run overflow reported with no culprit, would show it wrong. diff --git a/internal/core/site/blockwrap_test.go b/internal/core/site/blockwrap_test.go new file mode 100644 index 000000000..bdb005c20 --- /dev/null +++ b/internal/core/site/blockwrap_test.go @@ -0,0 +1,26 @@ +package site + +// No block of text widens the page, whatever token it carries. +// +// A record's title and its source path are the record's own words, and some of +// them hold a long run with nowhere to break: a timestamp-id ADR's path is about +// fifty characters to its first hyphen, and a title can quote a home-relative +// path. Given nowhere to break, the token sets the width of its block and the +// page scrolls sideways on a phone (iss-2610040732240935). The net is one rule on +// `body`: `overflow-wrap:break-word` is inherited by every block, breaks a token +// only where the line would otherwise overflow, and — unlike `anywhere` — leaves +// min-content sizing alone, so tables, grids and flex rows lay out as before. + +import "testing" + +// TestTheStylesheetBreaksAnOverflowingTokenInEveryBlock holds the net in abcd's +// own stylesheet and in the copy setup seeds a managed repository with, at the +// top level so it applies at every width. +func TestTheStylesheetBreaksAnOverflowingTokenInEveryBlock(t *testing.T) { + for name, src := range bothStylesheets(t) { + if got := cascadeOf(src).top["body"]["overflow-wrap"].value; got != "break-word" { + t.Errorf("%s gives body overflow-wrap:%q, want break-word: a title or path with nowhere to break widens the page", + name, got) + } + } +} diff --git a/internal/core/site/liststack_test.go b/internal/core/site/liststack_test.go new file mode 100644 index 000000000..f161dc481 --- /dev/null +++ b/internal/core/site/liststack_test.go @@ -0,0 +1,85 @@ +package site + +// A `.list` row stays readable on a phone. +// +// On a wide screen a row is two columns, the id and then the title. On a phone +// the id column (a sixteen-digit timestamp id and its date, about 220 px) leaves +// the title a column a few characters wide, so below the width where the two +// columns stop fitting the row stacks: the id above, the title below at the full +// width of the row. A sideways scroll is not the alternative; the page never +// scrolls sideways (iss-2610040729344770). +// +// The id column wraps between ids and never inside one: a supersession row on +// /record/health/ carries two timestamp ids and an arrow, wider than the row on +// a 360 px phone, so the span gives at its spaces while each id link, the stub +// and the date keep their nowrap. + +import ( + "regexp" + "strconv" + "testing" +) + +// listStackPhone and listStackTablet bound the stacking breakpoint: at or above +// the wider of the two phone widths the overflow audit measures (390 px), and +// below its tablet width (768 px), where a row keeps its two columns. The +// measured value is 520 px, where the title column of the dashboard's latest +// decisions falls below about 240 px. +const ( + listStackPhone = 390 + listStackTablet = 768 +) + +var maxWidthPrelude = regexp.MustCompile(`^@media \(max-width:(\d+)px\)$`) + +// TestAListRowStacksOnANarrowScreen holds both stylesheets to a wide row of two +// columns at the top level and a single-column row under a max-width query +// between the phone and tablet widths, placed after the rule it overrides. +func TestAListRowStacksOnANarrowScreen(t *testing.T) { + for name, src := range bothStylesheets(t) { + c := cascadeOf(src) + wide, ok := c.top[".list li"]["grid-template-columns"] + if !ok || wide.value != "auto 1fr" { + t.Errorf("%s: a wide .list row is grid-template-columns:%q, want auto 1fr", name, wide.value) + } + var found bool + for prelude, rules := range c.media { + m := maxWidthPrelude.FindStringSubmatch(prelude) + if m == nil { + continue + } + px, _ := strconv.Atoi(m[1]) + narrow, ok := rules[".list li"]["grid-template-columns"] + if !ok { + continue + } + found = true + if narrow.value != "minmax(0,1fr)" { + t.Errorf("%s: under %s a .list row is grid-template-columns:%q, want minmax(0,1fr)", name, prelude, narrow.value) + } + if px < listStackPhone || px >= listStackTablet { + t.Errorf("%s: a .list row stacks under %s, want a breakpoint in [%d,%d) px", name, prelude, listStackPhone, listStackTablet) + } + if narrow.at < wide.at { + t.Errorf("%s: the stacking rule under %s comes before the two-column rule, which overrides it", name, prelude) + } + } + if !found { + t.Errorf("%s: no max-width media query stacks a .list row: on a phone the title is a column a few characters wide", name) + } + } +} + +// TestAListIdWrapsBetweenIdsNotInsideOne holds the id span to normal wrapping +// and every element in it to nowrap, at the top level in both stylesheets. +func TestAListIdWrapsBetweenIdsNotInsideOne(t *testing.T) { + for name, src := range bothStylesheets(t) { + c := cascadeOf(src) + if got := c.top[".list .id"]["white-space"].value; got != "normal" { + t.Errorf("%s: .list .id is white-space:%q, want normal: two ids and an arrow cannot wrap", name, got) + } + if got := c.top[".list .id>*"]["white-space"].value; got != "nowrap" { + t.Errorf("%s: .list .id>* is white-space:%q, want nowrap: an id breaks inside itself", name, got) + } + } +} diff --git a/internal/core/site/listwrap_test.go b/internal/core/site/listwrap_test.go new file mode 100644 index 000000000..a6387f456 --- /dev/null +++ b/internal/core/site/listwrap_test.go @@ -0,0 +1,229 @@ +package site + +// A `.list` row never widens the page, whatever title it carries. +// +// A row is a two-column grid, the id and then the title, and a grid item's +// automatic minimum width is its min-content width: a title holding one long +// unspaced token — a home-relative path, a URL — sets the width of the row, and +// the row spills past its panel and makes the whole page scroll sideways on a +// phone (iss-2610040729344770, the /record/ dashboard at 360 px). The browser +// half of that rule is the overflow audit in site-src/audit/; this is the half a +// test can hold without one: the title reaches the page as a non-id span +// directly under the row, and the stylesheet gives every such span, at every +// width, the two properties that let it give. + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// listTitleSelector is the selector the stylesheet must carry, and the +// declarations it must give. `overflow-wrap:anywhere` rather than `break-word` +// because only `anywhere` lowers the element's min-content width; `min-width:0` +// because a grid item's automatic minimum is that min-content width. +const listTitleSelector = ".list li>span:not(.id)" + +var listTitleDecls = map[string]string{"overflow-wrap": "anywhere", "min-width": "0"} + +// TestAListTitleWithAnUnbreakableTokenIsAWrappingSpan renders the dashboard's +// latest decisions with a title that is one 157-character token and holds that +// it lands where the stylesheet's wrapping rule reaches it. +func TestAListTitleWithAnUnbreakableTokenIsAWrappingSpan(t *testing.T) { + token := "~/.abcd.noindex/transcripts/" + strings.Repeat("0123456789abcdef", 8) + "/" + e := &explorer{c: &composer{}, export: RecordExport{Nodes: []ExportNode{ + {ID: "adr-7", Type: "adr", Date: "2026-10-03", Title: token}, + }}} + got := e.latestDecisions() + want := `
  • adr-72026-10-03` + + `` + escapeText(token) + `
  • ` + if !strings.Contains(got, `