~bigbes/sr-ht-spec

d0f4baed — Eugene Blikh 6 hours ago
service: stop documenting a render cache that was never built

Config.Cache was documented as "the bleve index and the blob-sha-keyed render
cache", and four more files corroborated it. There is no render cache: the only
consumer of the value is filepath.Join(cfg.Cache, "index") in the daemon, and
Service.CacheDir() had no callers at all.

The comments now say what is true, and CacheDir is gone rather than
re-documented: Config() already exposes the value, so an accessor with no
callers is only a place for the claim to come back.

Removing the claim beats building the cache, and not only because building it is
the larger job. The pass that costs anything per page view is Archive.LinkPass,
and its per-page output depends on the whole revision — wikilinks resolve
against sibling documents — so its key is the commit sha, not a blob sha. A
blob-keyed cache would have missed the work it was justified by. A rev-keyed
archive cache is worth having and is its own design.
60b93a93 — Eugene Blikh 6 hours ago
graph: move the hand-written helpers out of the generated file
49e61bb9 — Eugene Blikh a day ago
web: expect the space the replacement rule now keeps

The golden row pinned "[-epsilon-]{+CHANGED+}", which is the fused rendering
prosediff has just stopped producing. The expectation described the defect, not
the behaviour, so it moves with the fix.
c9d4f735 — Eugene Blikh a day ago
prosediff: name the token the replacement rule reads

a[i-del.n] only makes sense once the reader notices that i has already
advanced past the deletion. Say so, and give the token a name.
79b637e1 — Eugene Blikh a day ago
prosediff: pin the case where the two revisions disagree about the boundary

The replacement rule reads both sides, and dropping either read survived
the test suite. It should not: without the new side's flag a boundary the
proposal ADDS is invisible ("config.yaml" -> "config. yml" renders as
"config.[-yaml-]{+yml+}", so the reviewer reads the new text as one word),
and without the old side's a boundary it REMOVES is. Two cases, one per
clause, and each now dies to its own mutation.
0d12812a — Eugene Blikh a day ago
prosediff: keep the space between a deleted word and its replacement

An insertion that directly replaces a deletion is rendered immediately after
it, so its Space flag answers "does a separator belong between the two"
rather than "was there whitespace before this text". It was hardcoded to
false, which fused every one-word substitution: "поднимается" -> "стартует"
reached the reviewer as "поднимаетсястартует". The del/ins styling keeps that
unambiguous, but a replacement is the commonest edit a proposal makes.

The flag is now true when the slot the two runs share sits on a word
boundary — either revision wrote whitespace in front of it, or nothing is
rendered before it at all — and stays false when the slot is glued to what
precedes it, so config.[-yaml-]{+yml+}, foo([-bar-]{+baz+}) and yes[-,-]{+;+}
are not split into two words. At token level a whole-word replacement and a
mid-word one are the same edit, and the token Space flag is the only thing
that tells them apart.

Decided in the package rather than in a renderer: RenderText, the web diff
rows and doc.Inline all read Span.Space with the same three lines, and an
inference repeated in three places is three chances to disagree about one
diff.
9dbb9886 — Eugene Blikh a day ago
scss: name the browser the inline-code measurement was actually made in

The rule was written with the version number the neighbouring comments
carry (141) rather than the one on this machine. A comment is a claim, and
this one is checkable.
57c5bf75 — Eugene Blikh a day ago
web: open a document's diff with what the diff amounts to

prosediff.Stats is computed on every comparison and was used only to answer
'did anything change at all'. It now says so on the page: how many blocks
were added, modified, removed and moved, and how many words came and went.
That is the question a reviewer asks before scrolling — whether this is a
proposal to read in a minute or one to sit down with.

One line at the gutter's size and colour, above the table and inside the
diff's own container so it travels with the document it describes. Kinds
that did not happen are left out rather than stated as zeroes, and the word
counts drop out entirely when nothing was reworded, which is what a
proposal that only moved blocks around reports.
93e36566 — Eugene Blikh a day ago
scss: draw a removed paragraph as removed, not as a peer of the new text

A deleted block rendered at full strength competes with the paragraph that
replaced it, and a reviewer skimming the page reads two paragraphs where
the proposal has one. It is now struck through and quieted — on the cell,
so the strike propagates to everything inside it, emphasis and links
included. Measured in Chrome 141: line-through and gray-600 against an
unchanged block's none and gray-900.

Flowed blocks only. A deleted line of a code fence carries the same class
and is left alone: the strike runs through the punctuation that carries the
meaning, and the line already has its bar, its sign and its number.

Inline code needed saying separately in all three quieted places. The
shared partial gives it a hard colour rather than an inherited one, so a
span of code stayed at full contrast inside a dimmed paragraph and read as
the one part not being removed.
27406d59 — Eugene Blikh a day ago
web: set a rewritten block's two revisions side by side

Below the similarity threshold the marks are unreadable, which is why the
threshold exists — but the fallback stacked the old paragraph above the new
one, and stacked they read as a removal followed by an unrelated addition.
They are two answers to the same question. So the fallback is now one row
carrying both revisions, drawn as two panes in the order the two number
tracks read, and the row is a modification rather than a delete plus an
insert, because that is what happened to the block. The threshold itself
and its justification are untouched.

That made a spacing defect in sideSpans visible: one side of a word script
is not a paragraph by construction. Measured on a real RFC, «всё [-, что
нужно-] {+нужное+}» rendered as «всёнужное» because both spans record
Space=false, and «внутри {+него+} [.]» rendered as «внутри .» because
carrying the dropped span's separator forward is wrong in the other
direction. Where a span was dropped the separator is now decided afresh.
a92094a2 — Eugene Blikh a day ago
web: draw a code fence as code, not as flat monospaced lines

A fence, a frontmatter block and a raw HTML block keep their per-line
numbers — the line really is the unit there — but they are now lexed with
chroma and emitted as pygments class names, which is the vocabulary
core.sr.ht's shared scss/highlight.scss already speaks. No HTML formatter
and no inline styles: the deployment's CSP has no style-src 'unsafe-inline'
and the page follows prefers-color-scheme, so the colours cannot be baked
in. The block is lexed whole rather than row by row, because a lexer that
cannot see the line above does not know it is inside a string.

Each line also gets its own scroll container. 'white-space: pre' in a
fixed-layout cell pushes a long line's overflow up to the page: measured on
a real RFC with one 280-character line of JSON, the document scrolled to
2748px against a 1440px viewport, and with the container it is 1440.
290b88bb — Eugene Blikh a day ago
web: check a log hit's fragment against the page it links to

The anchor defect survived because the producer and the consumer were tested
apart: doc/ pinned the shape of the anchor, web/ pinned that hitHref appends
whatever it is handed, and nothing asked the log page whether the id was there.
This drives the whole path — the production indexer splits the log, the search
page turns an entry into an href, and the document that href names is fetched
and inspected for the element the fragment points at.
c8713d10 — Eugene Blikh a day ago
doc: give a log entry the anchor its heading actually renders

LogEntry.Anchor was built as "e-<date>-<n>" and nothing in the tree ever
emitted an element with that id, so a search hit on a log entry linked to a
fragment the log page does not have and the browser stayed at the top. The
capability was intended, and the page already carries what it needs: a log is
served by the ordinary document handler, so every entry header is an <h2> with
an id. The anchor is therefore read off that id rather than invented — through
the renderer's own generator, so one heading has one spelling of its id and the
document keeps a single anchor scheme.
2fee6b50 — Eugene Blikh a day ago
mcpsrv: retire the claim that the session flag is what keeps a grant honest

Five comments said the read grant is per call because [stateless] is set. That
was true when it was written and it is not now, and the constant's own doc was
the worst of them: it listed a measurement -- flip it, watch these two tests go
red -- that the flip no longer reproduces.

Rewritten to say what each layer actually holds. Gate is identity per HTTP
request and the handoff; bindCaller is the caller of the call; the constant is
whether this surface keeps session state, which it has no use for. The
historical measurements stay where they were made, marked as history rather
than as behaviour.

Only comments changed. No assertion in identity_test.go was touched.
25f65e56 — Eugene Blikh a day ago
mcpsrv: bind the tool's caller to the call, not to the session

Gate resolves the principal per HTTP request and the tool layer reads one off
the handler's context, and only the SDK's stateless option made those two the
same thing. A constant is a convention: flip it and requireRead goes on
compiling while it answers a propose-only token with the whole approved corpus.

Gate now mints an unguessable key for the principal it just resolved, puts it on
the request it forwards and revokes it as that request returns; bindCaller, a
receiving middleware on the server, trades the key back and rebinds the context
the handler is called with. mcp.RequestExtra.Header is the one per-call channel
the SDK offers in both session modes -- getServer is invoked once per session
when stateful, so closing over a principal there would be the same defect again.

Measured, one session whose handshake carried alpha's spec:read token:

  stateful, before: spec_read on a spec:propose-only token -> served the document
  stateful, after:  the same call                          -> refused, spec:read missing

The two tests written for the flag now pass with stateless=false as well, which
is the point: they were the only two that turned red on that flip. The constant
stays as belt and braces, and its own argument is unchanged.
547b45a3 — Eugene Blikh a day ago
ci: prove the database suites ran, not just that the DSN was set

The guard already here answers "did the postgres task export a DSN". It does
not answer "did anything run against it", and that is the question the build
was going green over: a suite that skips every case prints ok and exits 0.

SPECSRHT_TEST_REQUIRE_PG asks the second question of the tests themselves, which
are the only ones that can answer it. Measured on this tree, the difference it
now refuses to hide is db 11.9% -> 82.4%, service 37.3% -> 80.6% and
cmd/specsrht-migrate 69.4% -> 81.2% of statements.

Also corrects the postgres task's claim that the migration-agreement check is
among the casualties. TestSchemaMatchesMigration compares schema.sql against
migrations/ as text and passes with no database at all; what skips is the
replay of those migrations against a real one.
68fe2721 — Eugene Blikh a day ago
tests: fail rather than skip when a run must exercise Postgres

A skip is the right answer on a laptop with no Postgres and the wrong one on
a builder that started one: the build reports success having exercised none of
the persistence layer. SPECSRHT_TEST_REQUIRE_PG is what tells the two apart —
unset it is a skip, set it the same empty DSN is a failure naming the test.

It is the tests and not the manifest that can report this. A shell guard on the
DSN proves the variable was exported; only the helper that would have skipped
can say that the suites ran on it.

A DSN that is set but unreachable already failed, at the Ping each caller does
next, and stays that way.
e20058ba — Eugene Blikh a day ago
api: check the layering the prose has been claiming
2cdf29e5 — Eugene Blikh a day ago
APKBUILD: run the build-stamp and embedded-css gates on the staged copy
987d44ca — Eugene Blikh a day ago
make: gate the build stamp and the embedded stylesheet
Next