~bigbes/sr-ht-spec

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
84d787d4 — Eugene Blikh a day ago
make: pick a sha256 tool instead of assuming coreutils
5b2fa228 — Eugene Blikh a day ago
doc: say which of the three guards on the review renderer is load-bearing

The file claimed that aligning the review renderer with the read one would let a
<script> run. Mutation says otherwise: putting html.WithUnsafe() back turns
nothing red, because the raw-HTML node renderer intercepts those node kinds and
the resolver strips a dangerous destination before goldmark's own check would.
Both of those ARE load-bearing — removing them turns six and three tests red
respectively, the wikilink among them. Unsafe stays off as the backstop, and the
comment now says that instead of overstating it.
827cea56 — Eugene Blikh a day ago
scss: keep a flowed list item's bullet clear of the change bar

Measured in a headless render of the real corpus: the hanging indent pulled the
bullet back into the cell's left edge, where the change bar is painted, so every
list item on a new document had its bullet sitting on the green rule. A margin
moves the whole item right instead; the hanging indent still aligns a wrapped
line with the item's first word.
195e0da6 — Eugene Blikh a day ago
scss: draw the change as a bar on the block's edge, not as a fill

A proposal that adds a document makes every row an insertion, so a tint over the
whole text cell coloured 100% of the page — saying nothing, and costing the prose
its contrast for the whole review. The signal is a 3px bar on the block's left
edge instead: one continuous rule down the margin when everything is new, one
short mark when a paragraph was edited, and on a small edit the only coloured
thing on the page is the words that changed.

It is an inset box-shadow rather than a border because a border on a cell of a
border-collapse table collapses against the rail's hairline and takes its width
out of the text column; a shadow also paints above the opaque band a sticky
heading needs, which retires the two rules that used to re-assert the tint there.

Flowed prose gets a reading measure, since the page is full-bleed and prose set
across 1400px is unreadable, and keeps the structure its own text does not carry:
an indent and a bullet for a list item, a rule for a quote. An ordered item gets
no number, because prosediff records that the list was ordered and not which item
this is.
693cbfa9 — Eugene Blikh a day ago
web: render a review block as a paragraph, not as its source lines

prosediff's package doc says prose reflows and that whitespace alone must never
produce a diff, and the web layer then emitted one row per SOURCE line: a
hard-wrapped paragraph arrived as six ragged rows with six numbers, which is the
line-differ artefact the engine exists to avoid. A prose block is now one row
holding the block's markdown rendered, with the word marks carried through as
<ins>/<del>, and the gutter states the line range the block occupies on each
side once. Code fences, frontmatter and raw HTML keep their lines, because there
the line is the unit and its whitespace is content.

That retires the recovery of per-line numbers for reflowed prose — the
WordsByLine spreading, its interleaving and the per-row pairing — and with it the
off-by-one they were defending against. A modification is one row of its own kind
rather than a delete row plus an insert row, so the edit reads in place.

The property test now checks the invariant per side rather than per row: one row
can carry both revisions' words, and the projection asserted is the one the
renderer uses.

diff.js restores a hash by extent overlap instead of membership, because a row
that states a range puts only its two ends in the cell.
644d2686 — Eugene Blikh a day ago
doc: render a proposed document with unsafe HTML off

The read plane turns goldmark's unsafe mode on because its documents are
approved; on the review page approving them is what the page is for, so a
<script> would run in the reviewer's session while they decide. A second
renderer keeps unsafe off, shows raw HTML as escaped text rather than omitting
it (a review page must not hide part of what it asks to approve), refuses a
dangerous wikilink destination the package's own anchor writer would otherwise
emit unchecked, and carries a word diff's marks through a markdown rendering
with Private Use Area sentinels, refusing the rendering when a mark did not
survive it.
4cc9f433 — Eugene Blikh a day ago
graph: pin the introspection schema to what the executor can serve

The introspection schema comes from gqlparser's built-in prelude; the
code that executes it is generated by gqlgen. Nothing tied the two
together, and gqlgen's generated executor answers a field it does not
know with panic("unknown field"), recovered into "internal system
error". A gqlparser that has learned a newer edition of the
introspection schema than the pinned gqlgen can execute therefore leaves
/query advertising fields that fail when a consumer selects them —
which matters here, because this endpoint is meant to be federated.

The new test derives the field list from whichever prelude is in use
rather than hard-coding one, and checks every declared field of the six
introspection types is selectable through a path this schema really has.
Against gqlparser v2.5.36 it fails on __Type and __InputValue, the two
types v2.5.22 added fields to; at the pinned v2.5.21 it passes.

Also corrects generate.go. Its note explained why the directive resolves
gqlgen through the module graph, which is true, but read as though that
made codegen work. It does not: v0.17.36 re-emits an existing resolver's
doc comment without its // markers and leaves schema.resolvers.go as
invalid Go. v0.17.94 fixes that and is blocked on sr-ht-core, so the
note now says so and says where the bump belongs.
Next