Produce one page per roster row, all through the one frame [#28] - #190
Merged
Conversation
The site listed twelve plugins and had a page for none of them, so a reader who found a row in the table had nowhere to go but the repository. One page per row now, at the address decisions/0008-the-url-shape.md gives them, rendered through the frame every other page is rendered through. Each one says what the plugin does, what state it is in and what that state means, and where its repository is. A plugin in the shell state gets the same page as any other: hiding it would make the site disagree with the list it is generated from, and the reader arriving at a shell is the one least likely to know what they are looking at. Nothing about a plugin is typed twice and nothing learns how many there are. The rows the table renders are the rows the pages come from, so a page with no row and a row with no page are the same read seen twice rather than two things that can disagree. Neither the frame nor any check carries a count, and a thirteenth plugin is a row in the roster. The table's first cell now links to the plugin's page rather than to its repository. A cell offering both is a cell where a reader has to guess which of the two links is the one they want, and the repository is on the page the cell leads to. Seven cases in the invariant suite asserted a violation count of two over the pages the fixture tree produces. The tree gained a roster and produces three, and those counts are now read off a build of the fixture rather than typed, so the next page added there reds nothing that is not about it. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
The static analysis rule about roster data reaching a filesystem path refused
the two lines that join an identifier into the address of a plugin page and into
the path that page is written to. The rule is right and the case it names is
real: `..` is a legal suffix of a repository name, it survived every rule the
parser had, and joined into the container the plugin pages go in it resolves out
of that container and over a page the build already wrote.
Ran 5 rules on 31 files: 2 findings.
internal/site/plugins.go
tools.semgrep.roster-data-reaches-a-filesystem-path
110: Href: "/" + path.Join(PluginsDir, e.ID) + "/",
115: Produced: path.Join(PluginsDir, e.ID, indexDocument),
Run by the static analysis workflow on the head before this one.
The repair is where that rule's own message says it belongs, in the parser
beside the other roster rules: an identifier is lower case words joined by
hyphens, which is the shape the schema already describes and which makes it one
segment by construction. The value is reduced at the join as well, because the
line that puts a value out of a data file into a path is where a reader decides
whether a row can choose where the build writes.
Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
The landing page listed twelve plugins and the build produced a page for none
of them, so a reader who found a row in the table had nowhere to go but the
repository:
Run 2026-08-25.
b2b47bdis the mainline this branch was cut from.What this waited on was #26, the landing page, which is closed as completed:
Run 2026-08-25. The other number
go run . blockersreads out of this issue'sbody is #80, and this issue's own text says of it that it is a forward
reference rather than a dependency: #80 lands the prose these pages will render,
and this issue owns the frame, the addresses and the escaping.
What this does
internal/site/pluginpages.gowrites one page per roster row, at the addressdecisions/0008-the-url-shape.mdgives them, rendered through the frame everyother page is rendered through. Each page says what the plugin does, what state
it is in and what that state means, and where its repository is, and it offers
two ways onward: the repository, and the table it came from.
A plugin in the shell state gets the same page as any other. Hiding it would
make the site disagree with the list it is generated from, and the reader
arriving at a shell is the one most likely to have been sent by the table and
least likely to know what they are looking at.
The rows the table renders are the rows the pages come from. There is one read
behind both, so a page with no row and a row with no page are the same read seen
twice rather than two things that can disagree.
The table's first cell now links to the plugin's page rather than to its
repository. A cell offering both is a cell where a reader has to guess which of
the two links is the one they want, and the repository is on the page the cell
leads to.
What this does not do. The prose that makes a plugin page worth opening is
#79's decision and lands in #80; what is on these pages today is the sentence
the roster carries and what the state word means. The computed shipping state is
#22, so no page here says a plugin ships. The install page is #27 and this
change adds no address the plan did not already have:
decisions/0008countsthese twelve, and
decisions/0003's count of eighteen is taken against thatlist, so neither record needs correcting.
Closes
Closes #28.
What was run
The gate, at the commit being pushed,
78ad89c:No test was skipped, and none needed a display or elevation.
Exactly one page per roster row, and none without one
Both run 2026-08-25 at
78ad89c. Empty diff in both directions, which is thepart a containment check would miss: a build that also wrote a page for
something the roster does not carry passes
every row has a pageand failsthis.
No count in the frame and none in the check
Run 2026-08-25 at
78ad89c. The frame carries neither the container these pagessit in nor any number of them; it renders the rows it is handed. The invariant
row this milestone added refuses a tree carrying no roster and reads no count
either.
Inside the budget in #6
Both run 2026-08-25 at
78ad89c. Twelve pages, 25377 bytes between them, thelargest of them an order of magnitude inside the document line. The two lines of
that budget that are properties of a render rather than of the bytes, the
largest contentful paint and the layout shift, are decided nowhere in this
repository and are #35; this change moves neither. The stylesheet line still
decides nothing while no produced page carries one.
The guards bite
The writer made to write nothing, which is what losing the pages looks like from
a table that still links them:
Run 2026-08-25, and the edit reverted afterwards.
What the suite asserts that the runs above do not
A row added to the roster produces a page with nothing else edited, at the
address the record gives it. A sentence carrying markup renders as text on the
page as well as in the table, with data that tries rather than with the data
this tree happens to hold. A shell's page carries the same number of paragraphs
as any other, which is the clause about not hiding it stated as a comparison
rather than as an intention. And a tree with no roster produces no plugin page
and does not leave an empty container behind for the host to answer at.
What a server check caught that this machine's gate did not
The first head of this branch was refused by the static analysis workflow, and
the refusal is recorded here rather than removed with the commit that repaired
it. The rule is about a value out of the roster reaching a filesystem path, and
the case it names is real rather than theoretical:
..is a legal suffix of arepository name, it survived every rule the parser had, and joined into the
container these pages go in it resolves out of that container and over a page
the build already wrote.
The repair is where that rule's own message says it belongs, in the parser
beside the other roster rules: an identifier is lower case words joined by
hyphens, which is the shape the schema already describes and which makes it one
segment by construction. The value is reduced at the join as well. The rule is
proved by switching it off:
and the case beside it takes the five real identifiers of this project through
the same rule and asserts none of them is refused, because a rule that refused
one of those is a rule somebody widens the first time it is in the way.
That leg is not part of
go run . ci, which is why the gate on this machinewas green over it, and that gap is a property of the leg list rather than of
this change.
What went red on the way, and it was not about this issue
Seven cases in the invariant suite asserted
REFUSED, 2 violation(s)over thepages its fixture tree produces, and three more asserted
2 file(s)and2 declarations. The fixture tree gained a roster with #26 and produces threepages now, so ten cases about frames, colour schemes and motion went red naming
a repair none of them needed. Those counts are read off a build of the fixture
tree rather than typed, so the next page added there reds nothing that is not
about it. That is a repair to the suite in the same change that surfaced it,
and it is disclosed here rather than folded into the description above.
The means
Go, the build verb and the frame this repository already carries. The page
struct, the address derivation and the description register were all already
here and are reused rather than duplicated; what is new is the writer and the
two words a state declares. Every value out of the roster reaches the page
through
html/template, which is the same engine the prose takes, so escapingis a property of the path rather than of a filter somebody remembers to apply.
Nothing is added to the module graph.
The alternative refused was a template per plugin. It is what a site of twelve
pages is usually built as, and it is the one shape in which a thirteenth plugin
is a new file and a page can quietly say something the roster does not.
Who read it
Nobody but me. The ruleset requires no approving review, so this is merged by
whoever opened it, and the evidence above stands in place of a second reader.