fix(website): represent changelog indent depth as a paragraph inset - #1273
Conversation
e7f33df to
d53a79a
Compare
vivek7405
left a comment
There was a problem hiding this comment.
The inset is the right call for this page. It represents the hierarchy without reintroducing the second level of glyphs we deliberately removed, and expressing it as a literal lookup rather than a computed class is exactly right for a Tailwind scan.
The rule as written is only half a rule though, and the missing half is a real bug rather than a theoretical one. "A continuation inherits the open paragraph depth" is well argued for a soft-wrapped line, but the fresh-paragraph-after-a-blank case is not a soft wrap, and inheriting there means a paragraph the author wrote back at column 2 renders inset under whatever bullet happened to precede it. Reachable from generated output too, not just hand-written notes.
The other two are about the tests rather than the render: one of them asserts a property of generated data, so it becomes a CI failure the first time the feature legitimately fires, and one cannot fail at all. Details inline.
vivek7405
left a comment
There was a problem hiding this comment.
Went back over the shallower-half fix. The rule itself holds: output is byte-identical across all 229 corpus files, depth is provably bounded by the deepest bullet marker so the corpus guard cannot false-red, and each new case reds against its own reverted counterfactual. No problem with the code.
The problem is the evidence I wrote around it. The comments cite "37 lines, 10 files" as the reason not to let an unmarked line read its own indent, and that number is doing a lot of persuasive work it has not earned. Rendering the corpus both ways says 4 files and 6 lines. The rest is a different branch entirely. That matters more than a usual comment nit, because the whole point of writing the measurement down was to stop the next reader undoing the rule, and an inflated number is exactly what gets checked and then dismissed.
b7a6707 to
df4ce94
Compare
9ba9f68 to
87931bd
Compare
df4ce94 to
cb37fc2
Compare
87931bd to
3dc004c
Compare
3dc004c to
f2af686
Compare
Every indented line of a changelog entry rendered at one visual level whatever its indentation, so a child point and its parent's peer came out indistinguishable. No text was dropped, which is why the whole-corpus no-drop guard never saw it; what was lost is structure. A paragraph now carries the depth it was opened at, expressed purely as a left inset: no marker, no type-size change, no rule. The single level of flattening #1233 established is untouched, and no entry body renders a second level of glyphs at any depth. The rule both indented branches follow, stated once in the file: a bullet marker establishes depth, a continuation inherits the open paragraph's. The corpus decided that second half. It has zero bullets past two spaces but 37 non-bullet lines at four or more across 10 files, every one wrapped prose whose author aligned it under the text above, so reading depth off a continuation would re-render those files today. Depth clamps at three levels, and the inset comes from a literal lookup because Tailwind cannot see a computed class name.
Inheriting the last paragraph's depth outright was half a rule. Closing prose written back at column 2 after a deeper bullet stayed inset under that bullet, so a paragraph the author put at the entry level rendered as a child of the point before it. Reachable from generated output too, since backfill-changelog indents every commit-body line by two, so a body with its own nested bullet lands at four and the body paragraph after it stays at two. The rule is now stated whole: only a bullet marker ESTABLISHES depth, so an unmarked line inherits or goes shallower but never deeper. Both halves are load-bearing and each has its own test. Inheriting keeps the corpus's 37 wrapped-prose lines out of an inset; going shallower is what this fixes. Also two test corrections. The corpus guard asserted a property of generated data, so it would have reddened the first release PR whose squashed commit body carried a nested bullet, for rendering exactly right. It now asks the weaker durable question, whether a file insets when its own source has no deep bullet, which still catches a continuation branch that reads its own indent. And a clamp assertion could not fail, since the inset lookup can only emit the three classes it holds; the pl-8 loop above it was already the real guard.
The comments justifying the inherit-or-shallower rule cited 37 lines across 10 files as the reason not to let an unmarked line read its own indent. All 37 do render without an inset, but 31 of them are soft wraps that join the paragraph above through a branch that reads no indent under any implementation. Only 6 lines, in 4 files, reach the branch the rule governs. Measured by rendering the whole corpus both ways. Those 6 are also not the wrapped prose the shorthand claimed. They are indented code blocks and one alignment table, which is a weaker and more honest argument than the one the comment was making, and the next reader should get the real one. Also corrected the rule's own wording. It said an unmarked line may return to a level its own indent states, but the ceiling is the paragraph immediately before it, not the deepest level any bullet reached, so once an item steps back out a later line cannot climb back in. No behaviour change: all 229 corpus files still render byte-identically and the 15 tests are unchanged in what they assert.
f2af686 to
52a74e4
Compare
Closes #1267
Stacked on #1271 (base is
fix/website-typecheck-render-entry, notmain). #1271 fixes atscfailure in this same file and this same paragraph machinery, and until it lands this change cannot use a cleantscas evidence that it is sound. GitHub retargets this tomainautomatically once #1271 merges. Review only the second commit; the first is #1271's.Summary
webjs.dev/changelogrendered every indented line of an entry as a flat paragraph at one visual level, whatever its indentation depth, so a markdown hierarchy deeper than one level was not represented at all. A child point and its parent's peer came out indistinguishable. No text was dropped, which is why the whole-corpus no-drop guard never saw it: what was lost is structure, not content.A paragraph now carries the depth it was opened at, expressed purely as a left inset on the emitted
<p>. No marker, no type-size change, no rule. The single level of flattening #1233 established is untouched, and no entry body renders a second level of glyphs at any depth.The rule both indented branches follow, now stated once in the file: only a bullet marker establishes depth, so an unmarked line inherits or goes shallower but never deeper.
Both halves are load-bearing and each fails differently.
The corpus decided the inheriting half, and the exact numbers matter, because the round version of them oversells the evidence. There are zero bullets at four or more spaces across the 229 entry files, and 37 non-bullet lines at four or more, spread over 10 files. All 37 render with no inset, but by two different mechanisms: 31 are soft wraps that join the paragraph above through a depth-blind branch, which reads no indent under any implementation. Only 6 lines, in 4 files, reach the branch this rule governs, and those 6 are indented code blocks and one alignment table rather than the wrapped prose the shorthand suggests. Measured by rendering the whole corpus both ways.
The shallower half stops the opposite failure, which the first cut of this change actually had: closing prose written back at column 2 after a deeper bullet stayed inset under that bullet, so a paragraph the author put at the entry level rendered as a child of the point before it. Reachable from generated output too, since
backfill-changelog.jsindents every commit-body line by two, so a body carrying its own nested bullet lands at four and the paragraph after it stays at two.One nuance worth stating, since the code says it and prose easily blurs it: the ceiling is the paragraph immediately before, not the deepest level any bullet in the item reached. Once an item steps back out to the entry level, a later unmarked line cannot climb back in. That is the conservative direction for a signal as weak as leading whitespace.
What changed
Parabecomes{ depth: number; lines: string[] }, andstartParatakes the depth.min(floor(indent / 2), 3). The clamp is what stops a pathological source file emitting a runaway ladder.min(its own indent depth, the last paragraph's depth), which is the inherit-or-shallower rule above.renderParasemits the inset from a literal lookup ({ 1: '', 2: ' pl-4', 3: ' pl-8' }), never string concatenation. Tailwind v4 scans for complete literal class strings, so a computedpl-${depth * 4}would generate no utility and the inset would silently do nothing while every test asserting on the class name still passed.Deliberately excluded
Nested
<ul>rendering is not coming back. #1233 settled that: 303 of 589 entries carried a nested bullet and 119 of 378 of those merely restated the entry title above them, because the generator writes each squashed commit subject as its own indented line.Also rejected: delegating to a markdown parser (WebJs is buildless, so it lands as a real server dependency for one page's renderer), reading the bullet character to infer generator output from hand-written output, making the continuation branch read its own indent (refuted by the 37 corpus lines), distinguishing a deeper run by smaller type or a muted rule, keeping a subtle marker at depth 2, and an uncapped proportional inset.
Test plan
npm test --workspace=@webjsdev/website: 442 node tests pass, 84 browser tests pass. Six cases added:pl-4, its parent and its parent's peer do not, and the two render differently. Synthetic by necessity, since the corpus has zero instances.pl-8and a 10-space and a 16-space bullet clamp there rather than going deeper.changelog/cli/0.10.30.md, whose 4-space lines are wrapped prose under a 2-space bullet: they stay in that bullet's paragraph and gain no inset.backfill-changelog.jswill emit a deep bullet eventually, and a flat assertion would red that release PR for rendering exactly right.The existing
no changelog file renders a nested listtest is untouched and still green, so the property #1233 established still holds.Counterfactuals, all three run by reverting rather than by reasoning (proven at
d53a79a6):minso a fresh paragraph reads its own indent: the inheritance case and the whole-corpus guard red. That is the 10-file wrapped-prose claim, enforced rather than merely written down.Each half of the rule is therefore independently guarded, which is the property I wanted before calling the rule settled.
Rendering all 229 corpus entry files before and after gives byte-identical HTML (0 diffs of 229), including the 10 files carrying 4-plus-space continuation lines.
npm run typecheck --workspace=@webjsdev/websiteexits 0.Browser / e2e / Bun parity: N/A.
renderEntryBodyis a pure server-side string function with no client, wire, listener, serializer, stream, ornode:*surface.Dogfood: N/A for the four-app boot check. No framework package changed, and the one app touched is the website, whose own full suite (node + browser) runs above.
Doc surfaces
website/modules/changelog/utils/render-entry.tsand the docblock inwebsite/test/changelog/render-entry.test.ts. Those are the only two places the flattening rule is written down, so leaving them saying "never bullet depth" would have been the drift, not a cosmetic miss.AGENTS.md, the skill at.agents/skills/webjs/, the scaffold templates, the docs site,README.md, the MCP inventory, and the editor plugins. This is website-internal rendering with no public API.website/public/tailwind.cssis generated and gitignored, and Tailwind v4 auto-scans the trackedmodules/tree, so the new literal utilities are picked up with no config change.