doc: say that codegen works, now that it does
Both files told the next agent that go generate ./graph is broken, damages the
tree and is blocked in sr-ht-core. Two commits ago that was true; it stopped
being true with the gqlgen bump, and it is exactly the shape of stale claim
these files exist to prevent — the one an agent acts on without checking.
Re-measured before rewriting: the pinned run is a fixed point, twice in a row,
go.mod and go.sum included.
graph: correct what the codegen comments claim, now that they are false
generate.go said codegen is broken here and that adopting v0.17.94 is
blocked outside this repository. Neither survived the commit before this
one, and a comment that outlives the thing it describes is the defect this
repository keeps finding. It now records what is still true — v0.17.36
cannot be run as `go run …@v0.17.36`, re-measured today and still failing
on golang.org/x/tools v0.9.3's "invalid array length -delta * delta" — as
history rather than as a standing warning, and says that the ceiling came
off when sr-ht-core 68dbc35 landed.
While there: the directive is pinned to @v0.17.94 the way the five
siblings pin theirs, so the run no longer resolves the generator through
this module's graph and no longer has to be followed by a `go mod tidy` to
undo what that dragged in. The helpers.go rule moves in from CLAUDE.md so
it sits next to the generator it constrains.
introspection_test.go pointed at "the note there" in go.mod, where there
is no note and never was, to explain a v2.5.21 pin that is now v2.5.36.
The assertions are untouched: the test derives its field list from
whichever prelude is in use, and it passed across this bump without an
edit, which is the transition it was written to check.
go.mod, graph: take gqlgen v0.17.94 and regenerate against it
sr-ht-core now passes the context that complexity.Calculate wants
(68dbc35, here as v0.0.0-20260821193637-68dbc3533caa), which was the only
thing holding this module at gqlgen v0.17.36. With it out of the way the
gqlparser ceiling goes too: the v0.17.94 executor knows the fields the
v2.5.36 prelude declares, so v2.5.21 is no longer the highest version
this endpoint can serve without advertising fields that panic when
selected. graph/introspection_test.go passes unchanged at v2.5.36 —
it derives its field list from whichever prelude is in use, which is
exactly the transition it was written for.
The bump and the regenerated code are one commit because neither builds
without the other: v0.17.94 widens graphql.ExecutableSchema.Complexity to
take a context and drops DeferredGroup.Label and CollectedField.Deferrable,
so the v0.17.36 output stops compiling the moment go.mod moves.
What the regeneration changed beyond banners and ordering:
- executableSchema.Complexity takes ctx and map[string]any, and builds
its executionContext through newExecutionContext.
- The deferred-delivery plumbing follows the runtime: Label is gone from
DeferredGroup and the Deferrable checks with it (35 references to 24).
- Every interface{} is spelled any (183 to 0).
- The introspection executor gained isOneOf (0 references to 8), which is
what makes the gqlparser bump safe.
- models_gen.go gains empty Query and Mutation structs and JSON marshalers
on the three enums.
- No resolver signature moved: schema.resolvers.go changed only in its
banner, one rewrapped comment and the grouping of the six resolver
type declarations.
go mod tidy keeps the codegen tree out of go.mod as generate.go promises —
goccy/go-yaml, urfave/cli/v3, x/mod and x/tools are dropped again. What
stays is v0.17.94's runtime: coder/websocket replaces gorilla/websocket in
the transport, go-viper/mapstructure/v2 replaces mitchellh/mapstructure in
the APQ extension, and sosodev/duration and x/sync/semaphore arrive with
graphql itself. The x/crypto, x/net, x/sys, x/text and protobuf bumps come
from sr-ht-core's own graph.
doc: say in CLAUDE.md and AGENTS.md what this repository actually does
The stub still carried its two placeholder lines. What replaces them is only
what was measured here: the make targets that exist, the Postgres variable and
the CI switch that turns its skips into failures, the layering the guard test
encodes, and the state of go generate.
Deliberately absent: make check, make test-pg and make test-all, which a sibling
has and this repository does not; a testify rule, since four packages here use
none; and anything about SPEC.md, which does not exist in this tree yet.
AGENTS.md is mirrored rather than symlinked. The two were never equal — it
carries its own beads block with a different hash and a codex-setup section
CLAUDE.md has no counterpart for — so a symlink would delete one tool's
generated content and put both regenerations on one inode.
doc: correct two lines the measurements contradicted
doc: describe the service the code implements, and what it does not say
contrib: add the unit and the server block a plain-host install needs
doc: say how to build, run and test this from a checkout
doc: write down every config key the two binaries actually read
go.mod: promote getopt, which the metrics test imports directly
The bind-address and metrics work made cmd/specsrht a direct consumer of
getopt, and the requirement stayed in the indirect block. Every plain `go build`
then rewrote go.mod to promote it — which dirties the checkout and makes
check-version refuse the binary it just built, on a tree the author never
touched.
Found by an agent that ran `make build` against a real instance and had to
`git checkout go.mod` after each one.
web: stop promising a scripts seam this layout does not have
The header listed "three seams" and described "scripts" as one nothing used
yet. No such block was ever declared, so the list was one short of the truth
and read as an invitation to use something absent — a page defining "scripts"
renders nothing at all and reports nothing, verified against html/template:
an unreferenced define is dropped and Execute still returns nil.
Not adding the block instead. proposal.html is the only page with a script and
it defines "head" with <script src="/static/diff.js" defer>, which is where a
deferred script belongs: fetched during parsing, executed after parsing and
before DOMContentLoaded, i.e. exactly where an end-of-body script runs, minus
the later fetch. diff.js does not depend on either — it calls init at once
when document.readyState has left "loading" and waits for DOMContentLoaded
otherwise. The seam goes in when a page needs what defer cannot give it.
make: name the missing tool before deleting the stylesheet it would rebuild
`make css` opened by removing web/static/main.min.*.css and only then reached
for sassc, so a machine without it lost a working stylesheet to learn that:
measured, `PATH=/usr/bin:/bin make css` died on the sassc line and left
check-css reporting no stylesheet and an orphan main.css for go:embed to pick
up. The three checks go in front of the rm for that reason, not for the
message alone — re-measured after, the same failure now leaves the hashed file
untouched and check-css still passes.
The message is worth something too, most of all for the partials. Missing,
sassc says "File to import not found or unreadable: base ... on line 16:1 of
scss/main.scss" and never prints the -I path it searched, so a correct
@import in our own stylesheet reads as the bug. The check prints the ASSETS
path and names core.sr.ht's `make install` as what fills it.
MINIFY joins SASSC as a variable so the guard and the recipe cannot come to
name different tools, and so an install under another path stays buildable.
make: say what the version stamp is, and what it is not
The check-version comment claimed the binary "reports itself from the VCS
revision Go records". It does not. cmd/specsrht declares `const version =
"dev"` and hands it to mcpsrv.Handler, nothing in the tree calls
debug.ReadBuildInfo, so every build introduces itself to an MCP client as
"dev". The stamp is provenance readable with `go version -m`, which is what
this gate is for and all it now claims.
Keep the mechanism rather than adopting the siblings' -ldflags: there is no
symbol to set. `-X` at a const and `-X` at a symbol that does not exist are
both silently ignored on go1.26.5 — exit 0, no diagnostic, the binary still
prints "dev" — so a VERSION here would read like a version and do nothing.
Record what has to change first, and cov.sr.ht's argument for changing it.
Also name the linked-worktree case in the empty-dirt branch, which is how it
was found: built from .worktrees/chore/makefile-preflight with a clean tree,
the binary came out vcs.modified=true because Go had stamped the parent
checkout. Every measurement above was taken in a plain clone for that reason.
cmd: keep the metrics listener on loopback unless asked otherwise
core-go defaults the Prometheus bind to ":0" - a random port on every
interface - while giving pprof "localhost:0", and this daemon inherited the
wrong half of that asymmetry: /metrics is served by core-go's own mux with no
credential in front of it, so anyone who could reach the host read the counters
of an instance whose service port is deliberately on loopback behind nginx.
withMetricsDefault inserts -m localhost:0 immediately after argv[0], so an
operator's own -m is parsed after it and still wins - server.New keeps the last
-m it sees. The vector is built once in run and handed to both bindAddresses and
server.New, so the startup line and the daemon cannot disagree about it.
cmd: take the listen address from the config the operator already edits
The daemon's only HTTP listener was set by -b alone, with localhost:5091
compiled in as the fallback, so an instance that needs a different address
had to carry it in the unit file or the container entrypoint rather than in
the config.ini it edits for every other setting. phoebe-lab's template says
so in as many words: "the web listener is set with -b in the entrypoint ...
so no bind-address key is needed here", while every containerised sibling
beside it binds 0.0.0.0 through its own key.
[spec.sr.ht] bind-address is that key. The spelling is not invented here:
six siblings already read it under that name -- artifacts, bench, cover,
curator, snip and tokens -- and upstream's own builds.sr.ht worker reads
[builds.sr.ht::worker] bind-address, so it is the name an operator sharing
one config.ini across services already knows.
Precedence is flag > config > default, stated in service.DefaultBindAddress
and in bindAddresses rather than left to be inferred: server.New falls back
to the address it was constructed with only when the vector carries no -b,
so handing it cfg.BindAddress is what produces that order.
The address is validated while the config is read, next to repos and origin,
so a malformed one is refused by a message naming the key instead of by a
bare net error once the database is open and the hook socket is held. Port 0
is refused with the malformed ones -- it is legal to bind and useless behind
a proxy that connects to a fixed port.
bindAddresses reads the vector with core-go's own getopt rather than scanning
for "-b", so the attached form and clustered flags are read the way the real
parse reads them, and the startup line reports the addresses actually bound
rather than the configured one -- which -b had already been making it
misreport.
web: say in the embed directive that partials are wanted
//go:embed templates/*.html carried _threads.html by accident of a rule
rather than by saying so: embed excludes a name starting with '.' or '_'
when it walks a directory, not when a pattern matches the file directly.
Measured before changing anything — the partial is in the embedded FS
today, and TestEveryTemplateOnDiskIsEmbedded is what keeps it there.
The "all:" prefix states the rule instead of relying on the reader
knowing the distinction, so the line is right under either reading of it
and cannot be shortened into the directory form without the prefix being
deleted first. The glob stays: "all:templates" is the same intention
with no extension to match, and would compile a stray .DS_Store or an
editor swap file into the release binary, which pages.Load would filter
out in silence.
The comment on the neighbouring static directive moves with it, since
the two forms now differ on the page: that tree is served to a browser
by name, so excluding '.' and '_' is what is wanted there. It also
listed two assets where there are three — proposal.html has loaded
/static/diff.js since the prose differ grew a client side.
web: serve the asset tree for the two methods it answers
r.Mount registers a subtree under every method chi knows, so the routing
table listed all ten against a handler that answers two, and a
same-origin POST to /static/main.min.<sha>.css came back 200 with the
whole 133 KB stylesheet instead of a 405. Nothing was writable and
nothing leaked; the description was wrong, and the table is what chi's
own 405 and anything walking the router read.
chimw.GetHead over the pattern Mount would have appended, so the asset
tree is registered the way every other read route on this surface is.
Nothing of Mount's is lost: its RoutePath shift and its 404/405
inheritance are for a mounted *Mux, and assets.Handler is a plain
handler that reads r.URL.Path and strips the prefix itself.
Measured against a live listener, before and after: the hashed asset
still answers 200 with public, max-age=31536000, immutable and no Vary,
a HEAD still answers the same headers with no body, an
If-None-Match: * still answers 304 carrying the asset policy rather than
the page's private, no-store, and /static, /static/ and a name that is
not there still get this service's own 404 page. POST, PUT, PATCH,
DELETE and OPTIONS now get the rendered 405.
The 405 carries no Allow header, which is chi's limitation rather than
this route's: it hands the list of methods that would have matched only
to its built-in responder, through unexported types, so the custom
handler chimw.RenderRefusals installs cannot emit one. Every 405 on this
surface is missing it, not just this route.
api: tell caches that the write plane answers per credential
Every answer the REST write plane gives is computed from the bearer token on
the request and from nothing else the request line shows: the 201 names a
proposal opened for the agent that presented the credential, the 401 names the
challenge, the 403 says this token may not propose. The mux carried no cache
policy at all, which made it the last credential-varying surface of the service
without one — web/ has it, /mcp has it, and /query got it two commits ago.
The policy is sr-ht-ecore's PrivateCache, the same helper the other three use:
private, no-store with Vary: Cookie, Authorization. Where it goes is a short
question here and was a long one for mountGraphQL, because that Group also holds
api-meta.json and had to keep it publicly cacheable. This mux mounts exactly one
route and it is the credential-varying PUT, so the policy covers the whole mux
and picks up two answers a route-level one would miss: chi's 404 for an address
this surface does not serve, and the resolver middleware's 401, written before
the router has looked at the path.
The honest ranking, which is why this is one line: a PUT is even less cacheable
than the POST /query carries the same policy for, so this is defence in depth
and not a live leak.
The test drives httptest.NewServer and a real client rather than a recorder,
which hands back the live header map and so cannot observe when a header was
set
doc: check the href a review link publishes, not the destination it came from
Mutation found the dangerous-URL refusal had grown two guards for one property:
with an archive supplied, the check on the way out caught what the check on the
way in was for, so removing the incoming one turned nothing red. There is one
check now, on the href, which is the thing that reaches the anchor — and
removing it fails four tests in both configurations.
The relative-link test went the same way: a sibling destination resolves through
doc.Archive's space-wide stem fallback as well, so it passed with the linking
document's directory thrown away. It uses a path-qualified destination now,
which has no fallback.
web: resolve a proposal's links against the revision under review
An internal link inside a proposed document used to reach the review page as the
destination as typed, so following one landed nowhere. The page now reads the
proposal through ProposalReview and resolves each block against the archive of
the revision that block's text is from: the branch tip for the text under
review, the base for a block the proposal deletes.
Every href is pinned to the revision it was resolved at. A document's site path
names the document and not the revision, so an unpinned link out of a proposed
block would land on the approved head — the text the proposal is changing.
Two things the resolver could not say before and says now: a link to a document
that does not exist is marked, and so is a citation of a section the proposal
has renamed, which is the error a proposal introduces and review is the moment
to catch.