web: say which of the two guards keeps a comment off the map
Mutation-testing the tick sizes turned up a property with two guards and a
test that cannot tell them apart. mapTicks skips a notes row when it adds up a
block's lines — and deleting that skip changes nothing, because a notes row
carries no gutter number on either side and so already reports zero source
lines through diffRow.sourceLines.
Both the check and its test are kept: what would break the property is a notes
row that grows a number, and then the skip is what holds and the test is what
says so. What is not kept is a comment implying the skip is doing the work.
web: give the review page a map of what changed
The page could say how many blocks changed and could show each of them, and
still refused the question a reviewer arrives with: is this a new document, a
two-word fix, or a rewritten chapter. The map answers it in shape. One tick
per block down the right edge, coloured by what happened to that block, so a
proposal that adds a document is a solid bar, a one-word edit is a single
coloured tick in a grey column, and a reworked section is a band.
Context blocks are ticks too. The coloured ones are only readable against how
much document they sit in — a map of the changes alone draws the same picture
for both ends of that scale. The ladder in tickWeight is deliberately not
linear: a document with one forty-line fence in it would otherwise be a bar of
code with every paragraph beside it too thin to see.
A changed block is a link to itself and an unchanged one is a mark on the
ground, which bounds the links by the size of the change rather than by the
size of the document. A tick that points into a collapsed run opens it: the
new `:has(:target)` rule is what keeps that working with JavaScript off, since
a browser cannot scroll to a row it is not drawing.
Nothing is derived twice. rowBlock now carries the block's own change kind,
because a move-in opens with a marker row and a modified fence opens with one
too, so reading the kind off a block's first row would colour a rewritten
fence as a move. mapTicks reduces the same row stream the table is built from,
so the map cannot claim a block the table does not draw.
The map is aria-hidden and out of the tab order: every tick points at a block
that is already on this page in this order, and a new document would otherwise
put a hundred empty links in front of the keyboard. The stats line above says
the same thing in words.
diff.js adds the one thing markup cannot, the scroll indicator, by observing
the block rows themselves — with the sticky heading band cut out of the root.
Without that, a `tr.ph-head` pins at top: 0 and never leaves the viewport:
measured on a 5000px render of one RFC, scrolled to the bottom, nine of the
fifty-one ticks were still lit and every one of them was a heading long since
passed. Neither getBoundingClientRect nor offsetTop can tell a stuck row from
one that is genuinely at the top.
hooks, mcpsrv: stop two comments and one message from naming stale facts
The push refusal spelled "spec:propose" as a literal — the one line an agent
reads before going to mint a token, so a rename that missed it would send the
agent after a grant this service no longer honours. It now names
authn.ActionPropose, which the grant vocabulary test holds the docs to.
mcpsrv/cache.go said mcphttp was "not resolvable: the ecore commit that adds
mcphttp is not published". It is in the ecore this module pins today. What still
holds the swap is that mcphttp's vary is a package constant, so the comment says
that instead.
authn: enumerate the working-token grant vocabulary and hold the docs to it
spec-oqk asked for the api-meta.json scope list and the locally-checked grant
string to be derived from one source. Reproducing the current declarations
first, as the issue itself asked: that pair already is.
authn/provenance.go const ConfigSection = "spec.sr.ht"
authn/meta.go const ScopeRead = ConfigSection + "/SPECS"
graph/server.go var GrantScopes = []string{metapat.ScopeName(authn.ScopeRead)}
cmd/specsrht/*.go var apiScopes = graph.GrantScopes
apimeta.Handler(apiScopes...) / NewMetaAuth(pats, owner, authn.ScopeRead)
"SPECS" is written once, in a const; the served document and the string
MetaAuth.VerifyToken hands to metapat.Allows both descend from it. Renaming it
moves the served bytes and the check together, measured. The shape is bench's,
coverage's and artifacts' — four services already share it — so nothing here is
worth diverging for. The issue's cited sibling does not support its claim
either: artifacts' core.Grants is the tokens.sr.ht vocabulary and has no bearing
on that repo's api-meta.json, which derives from authn.ScopeRead exactly as this
one does.
What is missing is the other vocabulary, and it is the one the cover:/cov:
lesson was actually about. ActionPropose and ActionRead are checked here and
published nowhere this daemon serves: tokens.sr.ht knows no service's grant
names, so a working token is obtained by a human reading a name out of docs/ and
typing it into another service's mint form. Concretely, a drift costs this:
rename ActionPropose and every token minted for "spec:propose" is refused at
Principal.Authorize — a 403 from the REST and MCP write planes and from
hooks.badTokenMessage on a push — while docs/DESIGN.md keeps telling the next
agent to mint the string that no longer works. Nothing logs the disagreement,
because from the daemon's side each refusal is an ordinary missing grant.
So: Grants enumerates the vocabulary, and grants_test.go holds it against
ConfigSection and against every non-Go file in the repository, in both
directions. This is a test and not a type-level guarantee — the name a document
spells cannot be checked by the compiler — and the pin was mutated both ways to
confirm it fails: renaming a constant reports docs/DESIGN.md naming a grant the
service no longer honours and a grant no file can tell anybody about; dropping a
member reports the constant the enumeration cannot answer for.
api-meta.json is byte-identical across this commit, driven through a real
request against the mounted router.
cmd: tell caches that /query answers per credential
gitx, doc: finish retiring the render cache from the comments
The previous commit corrected the five sites the issue named. Four more said the
same thing and were left because they sat outside that change's file set: the
Blobs map in gitx, Page.Blob in doc, and the two test messages that quote them.
A blob sha is a content identity here, and that is all it is. Saying so in every
place that mentions one is the point: the claim survived this long because nine
files corroborated each other, and a repository that donates code copies its
comments along with it.
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.
graph: move the hand-written helpers out of the generated file
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.