Skip to content

Compute the shipping state from what is published [#22] - #191

Merged
iderex merged 2 commits into
mainfrom
enhancement/the-shipping-state-comes-from-the-releases
Aug 25, 2026
Merged

Compute the shipping state from what is published [#22]#191
iderex merged 2 commits into
mainfrom
enhancement/the-shipping-state-comes-from-the-releases

Conversation

@iderex

@iderex iderex commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

What was wrong

The table showed what a roster row declared and nothing else, so the one state
word that is a fact rather than an opinion was the one word nothing computed.
Nothing in the tree read a release list, and nothing named the rule that decides
which releases count:

git grep -c prerelease 184e34c -- '*.go' ; echo "exit=$?"
exit=1

Run 2026-08-25. 184e34c is the mainline this branch was cut from.

What this waited on was the roster, which is entry 6 of #7 and is answered, and
the record is now in the tree under #26.

What this does

internal/releases holds what each repository the roster names has published:
two counts per repository, finished and prerelease, and the day and the request
that took them. Two counts rather than a verdict, because the rule that turns
them into a word is decisions/0009-what-counts-as-shipping.md and can move,
and a record carrying the word would have to be retaken the day it does.

The rule is applied in one place. A plugin ships when a finished release is
published; the prerelease flag is the signal and the tag string is not read,
which is what that record decides and what the case beside it asserts. A
declared state word is the floor and what is published raises it, which is
decisions/0001's rule.

A prerelease is not dropped. decisions/0009 says it is the one piece of news a
plugin that does not ship can offer and that it belongs on the page rather than
in the state, so the plugin's own page says how many are published in words that
cannot be mistaken for the table's word.

go run . releases is the refresh path. It asks each repository the roster
names, reports what moved, and writes the record. It is not a leg of the gate,
for the reason pins and tokens are not: what it reads is somebody else's
release lists, so its answer moves when they publish rather than when this tree
changes.

data/repositories.json is removed. It answered whether a roster row's
repository is there, and that is the same question this record answers: a
repository that answered with its release list exists, and a second file
carrying the same set is a second thing to keep current.

What this lands that somebody has to look at

Three rows now compute as shipping where the roster and the organisation profile
say otherwise, and the build does not refuse the disagreement. Refusing it is
#23, which names one of the three and does not name the other two.

go run . releases
  Flowfin/jellyfin-plugin-requests: NEW, 1 finished, 0 prerelease(s)
  Flowfin/jellyfin-plugin-sso: NEW, 28 finished, 52 prerelease(s)
  Flowfin/jellyfin-plugin-stats: NEW, 1 finished, 0 prerelease(s)

Taken again with a different tool, so the counts are not one program agreeing
with itself:

for r in sso requests stats; do
  gh api "repos/Flowfin/jellyfin-plugin-$r/releases?per_page=100" \
    --jq '{finished:[.[]|select(.draft==false and .prerelease==false)]|length, pre:[.[]|select(.draft==false and .prerelease==true)]|length, total:length}'
done
{"finished":28,"pre":52,"total":80}
{"finished":1,"pre":0,"total":1}
{"finished":1,"pre":0,"total":1}

Both run 2026-08-25.

sso is declared build-up and the profile calls it Ships, so the computed
word agrees with what is published about it. The other two do not:
requests is declared shell and stats is declared build-up, and both have
a finished release. #23 carries the requests instance already; the stats
one is measured here for the first time and is not written into #23, because
that issue is being worked elsewhere and a second copy of a finding is worse
than none.

Nothing is served out of this repository, so no reader meets any of it today:

gh api repos/Flowfin/site/pages
gh: Not Found (HTTP 404)

Run 2026-08-25.

Closes

Closes #22.

What was run

The gate, at the commit being pushed, 48cb1fb:

go run . ci
gate: 7 legs, in order: format, vet, test, build, links, sitemap, invariants
  needs-network was not asked for. Asking costs a request to the public name from whatever machine runs it, and a verdict that moves when somebody else's service does rather than when this tree changes. Ask with: go run ./harness/needs-network
  format: ok, 63 file(s)
  vet: ok
  test: ok, 30 test file(s)
  build: ok, 20 file(s)
  links: every reference that stays inside this site resolves to a file the build wrote
  sitemap: every page the build wrote is listed once, and every entry has a page behind it
  invariants: ok, 39 rule(s) decided, 1 owed and not decided
7 of 7 legs ran. None was skipped.

No test was skipped, and none needed a display or elevation.

The state on the page is the computed one

go run . build > /dev/null
grep -o -E '<td>(Ships|In build-up|Shell only)</td>' dist/index.html | sort | uniq -c
      6 <td>In build-up</td>
      3 <td>Shell only</td>
      3 <td>Ships</td>

grep -o -E '<p>[0-9]+ rows[^<]*' dist/index.html
<p>12 rows, read from the roster this repository carries. What each plugin has published was read on 2026-08-25, and the state below is computed from that rather than declared: a row says what is true before anything is published, and a finished release raises it.

Both run 2026-08-25 at 48cb1fb. The roster declares eight build-up and four
shell; what the page shows is six, three and three, so the word is computed
rather than copied.

A build with the network unavailable

Nothing a build reads reaches the network, and that is a property of the
dependency graph rather than of anybody remembering it:

go list -deps ./internal/releases | grep -cE '^net(/|$)'
0

Run 2026-08-25 at 48cb1fb. What asks a repository anything is
internal/releases/github, which imports internal/releases and is imported by
main.go alone.

The bound on that reading, stated rather than left for somebody to find.
internal/site does still reach net/http transitively:

go list -deps ./internal/site | grep -c '^net/http$'
1

That is internal/tokens, whose fetcher the tokens verb uses and no build
calls. So the reading above is about the release path and not about every
package a build links, and what stands behind the wider claim is that no build
verb calls a fetcher at all rather than a reading that could refuse one.

The guards bite

The record taken out of the tree, which is what a build with nothing recorded
would otherwise render:

go run . build
reading data/releases.json, which is what the shipping state and the roster's repositories are read from: open data\releases.json: Das System kann die angegebene Datei nicht finden.
exit status 1

The rule made to count any published release, which is the reading
decisions/0009 refuses by name and the one a rule written quickly arrives at:

go test ./internal/releases/ ./internal/site/
--- FAIL: TestOnlyAFinishedReleaseMeansAPluginShips (0.00s)
FAIL	github.com/Flowfin/site/internal/releases
--- FAIL: TestWhatIsPublishedRaisesTheDeclaredState (0.00s)
FAIL	github.com/Flowfin/site/internal/site

The recorded moment dropped out of the sentence above the table, which is the
clause that makes a page produced from a recorded answer readable as one:

go test ./internal/site/ -run 'TheSentenceAboveTheTableStates'
--- FAIL: TestTheSentenceAboveTheTableStatesWhenTheDataWasTaken (0.02s)
    plugins_test.go:252: the page does not state when the release data was taken:

All three run 2026-08-25, each edit reverted before the next.

What the suite asserts that the runs above do not

The record fails closed on six shapes, each in its own words: bytes that are not
the record, a record carrying no repository, one with no repositories field at
all, one saying nothing about when it was taken, one taken at whitespace, and a
count a release list cannot answer. A refresh that could not read one repository
writes nothing, because a record missing a repository is a record that says the
repository is not there, and a network failure turned into a claim about
somebody's repository is the one failure a recorded answer must not have. Two
refreshes over the same answers in different orders write the same bytes, so a
run that changed nothing produces no diff. And the verb over a tree reports what
moved, what is new and what the roster no longer names.

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 refuses a read whose path the line itself does not show to be
assembled, and FromSlash converts separators rather than assembling a path:

Ran 5 rules on 32 files: 1 finding.
  main.go
  tools.semgrep.file-read-with-a-path-the-call-site-cannot-vouch-for
    116: body, err := os.ReadFile(filepath.FromSlash(site.RosterFile))

The root the verb works against is named once and joined on 48cb1fb, which is
also what it hands to the refresh below it. That leg is not part of
go run . ci, which is why the gate on this machine was green over it, and that
gap is a property of the leg list rather than of this change.

The means

Go, and the shape this repository already uses for a verb that reads something
somebody else publishes: pins and tokens are both verbs rather than gate
legs for the same reason, and this one follows them. It differs from both in
writing rather than reporting, and that difference is argued at the function: a
pinned copy has an authority elsewhere and a difference is evidence about it,
while this record has no authority anywhere and is the answer itself, so a run
that only reported it would leave somebody typing counts into a file by hand.

JSON for the record, which is what every other file the build reads is written
in. Nothing is added to the module graph: the fetcher is net/http and
encoding/json, and the record is encoding/json alone.

The alternative refused was having the build ask. It would produce different
bytes on different days, fail with no network, and put a request to somebody
else's service inside the one verb that has to be reproducible.

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.

The table showed what a roster row declared and nothing else, so the one state
word that is a fact rather than an opinion was the one word nothing computed.

`internal/releases` holds what each repository has published, taken once by a
verb somebody runs and read from the tree by every build. Two counts per
repository rather than a verdict, because the rule that turns them into a word
is `decisions/0009-what-counts-as-shipping.md` and can move; a record carrying
the word would have to be retaken the day it does. The rule is applied in one
place: a plugin ships when a finished release is published, the prerelease flag
decides and the tag string is not read.

Nothing in a build reaches the network. `go run . releases` asks and writes,
and what asks is `internal/releases/github`, a package the build imports
nowhere, so the separation is a property of the dependency graph rather than of
anybody remembering it. A build renders what was last recorded and the landing
page states the day it was recorded, which is what makes a page produced from a
recorded answer readable as one.

The record also answers whether a roster row's repository is there, which
`data/repositories.json` answered until now. The two are one question asked
once: a repository that answered with its release list is a repository that
exists, and a second file carrying the same set is a second thing to keep
current. That file is removed.

Three rows now compute as shipping where their row declares otherwise, and the
build does not refuse the disagreement, because refusing it is #23. The
disagreement is named in the pull request that lands this rather than left for a
reader to find.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
Comment thread main.go Fixed
@iderex iderex added the enhancement New feature or request label Aug 25, 2026
@iderex iderex self-assigned this Aug 25, 2026
The static analysis rule refuses a read whose path the line itself does not show
to be assembled, and FromSlash converts separators rather than assembling a
path, so the new verb's read was one the call site could not vouch for.

    Ran 5 rules on 32 files: 1 finding.
      main.go
      tools.semgrep.file-read-with-a-path-the-call-site-cannot-vouch-for
        116: body, err := os.ReadFile(filepath.FromSlash(site.RosterFile))

Run by the static analysis workflow on the head before this one. The root the
verb works against is now named once and joined, which is also what it hands to
the refresh below it.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex
iderex merged commit 9a11335 into main Aug 25, 2026
17 checks passed
@iderex
iderex deleted the enhancement/the-shipping-state-comes-from-the-releases branch August 25, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Derive the shipping state from published releases

2 participants