Skip to content

fix(website): represent changelog indent depth as a paragraph inset - #1273

Merged
vivek7405 merged 3 commits into
mainfrom
fix/changelog-indent-depth
Aug 5, 2026
Merged

fix(website): represent changelog indent depth as a paragraph inset#1273
vivek7405 merged 3 commits into
mainfrom
fix/changelog-indent-depth

Conversation

@vivek7405

@vivek7405 vivek7405 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes #1267

Stacked on #1271 (base is fix/website-typecheck-render-entry, not main). #1271 fixes a tsc failure in this same file and this same paragraph machinery, and until it lands this change cannot use a clean tsc as evidence that it is sound. GitHub retargets this to main automatically once #1271 merges. Review only the second commit; the first is #1271's.

Summary

webjs.dev/changelog rendered 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.js indents 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

  • Para becomes { depth: number; lines: string[] }, and startPara takes the depth.
  • The indented-bullet branch captures its indent and computes min(floor(indent / 2), 3). The clamp is what stops a pathological source file emitting a runaway ladder.
  • A soft-wrapped continuation joins the open paragraph unchanged. A fresh paragraph after a blank line takes min(its own indent depth, the last paragraph's depth), which is the inherit-or-shallower rule above.
  • renderParas emits 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 computed pl-${depth * 4} would generate no utility and the inset would silently do nothing while every test asserting on the class name still passed.
  • The in-file comment block and the test file docblock both asserted the old rule ("indentation controls grouping here, never bullet depth"). Both now state the new one and name the 37-line measurement, so the next reader does not undo it.

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:

  • A synthetic two-level list: the child carries 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.
  • A synthetic three-plus-level list: a 6-space bullet gets pl-8 and 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.
  • Closing prose written back at column 2 after a deeper bullet sits at the entry level, while the deeper bullet keeps its own inset. This is the shallower half.
  • A paragraph after a blank line cannot invent a level no bullet established, as a unit rather than via the corpus. This is the inheriting half.
  • A whole-corpus guard that a file insets only when its own source carries a 4-plus-space bullet. Phrased against the source rather than as a flat "no file insets", because the corpus is generated data: backfill-changelog.js will 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 list test 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):

  • Revert the renderer to the base branch, keeping the new tests: three cases red. A green corpus run proves nothing about them, which is why they had to be constructed.
  • Drop the min so 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.
  • Drop the shallower half so it always inherits: the closing-prose case reds. That is the bug this PR's second commit fixes.

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/website exits 0.

Browser / e2e / Bun parity: N/A. renderEntryBody is a pure server-side string function with no client, wire, listener, serializer, stream, or node:* 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

  • Updated: the comment block in website/modules/changelog/utils/render-entry.ts and the docblock in website/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.
  • N/A: 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.
  • N/A: changelog and version bumps. No published package changed.
  • N/A: website/public/tailwind.css is generated and gitignored, and Tailwind v4 auto-scans the tracked modules/ tree, so the new literal utilities are picked up with no config change.

@vivek7405 vivek7405 self-assigned this Aug 5, 2026
@vivek7405
vivek7405 force-pushed the fix/changelog-indent-depth branch from e7f33df to d53a79a Compare August 5, 2026 10:23

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread website/modules/changelog/utils/render-entry.ts Outdated
Comment thread website/test/changelog/render-entry.test.ts Outdated
Comment thread website/test/changelog/render-entry.test.ts Outdated

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread website/modules/changelog/utils/render-entry.ts Outdated
Comment thread website/modules/changelog/utils/render-entry.ts Outdated
Comment thread website/modules/changelog/utils/render-entry.ts Outdated
Comment thread website/test/changelog/render-entry.test.ts Outdated
@vivek7405
vivek7405 force-pushed the fix/website-typecheck-render-entry branch from b7a6707 to df4ce94 Compare August 5, 2026 10:41
@vivek7405
vivek7405 force-pushed the fix/changelog-indent-depth branch from 9ba9f68 to 87931bd Compare August 5, 2026 10:41
@vivek7405
vivek7405 force-pushed the fix/website-typecheck-render-entry branch from df4ce94 to cb37fc2 Compare August 5, 2026 10:55
@vivek7405
vivek7405 force-pushed the fix/changelog-indent-depth branch from 87931bd to 3dc004c Compare August 5, 2026 10:55
Base automatically changed from fix/website-typecheck-render-entry to main August 5, 2026 11:03
@vivek7405
vivek7405 force-pushed the fix/changelog-indent-depth branch from 3dc004c to f2af686 Compare August 5, 2026 11:04
@vivek7405
vivek7405 marked this pull request as ready for review August 5, 2026 11:28
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.
@vivek7405
vivek7405 force-pushed the fix/changelog-indent-depth branch from f2af686 to 52a74e4 Compare August 5, 2026 11:29
@vivek7405
vivek7405 merged commit 728972f into main Aug 5, 2026
10 checks passed
@vivek7405
vivek7405 deleted the fix/changelog-indent-depth branch August 5, 2026 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(website): changelog entry bodies flatten multi-level indentation

1 participant