~bigbes/sr-ht-spec

ref: 01999c70f928a0963f60dc1670f68e7201576b1b sr-ht-spec/web/comments.go -rw-r--r-- 16.6 KiB
636dc7a8 — Eugene Blikh 9 days ago
pages: read a form's body, bounded, and never its URL
e756d504 — Eugene Blikh 9 days ago
web: draw the whole web tier from sr-ht-ecore

The chrome moved last time; this moves the four packages around it. pages
discovers the page templates and carries the shared error body, so the
hand-maintained page list, the render helper and error.html go; assets finds the
hashed stylesheet and serves the static tree, so the local regexp, the href glob
and handleStatic go; middleware brings the private-cache policy and a panic
guard that aborts a connection rather than appending an error page to a
truncated one; and ecoretest replaces the hand-built config and the TestMain
that minted its own keys.

The one that matters is csrf. The guard was a predicate three handlers
remembered to call, which made unprotected the default for any form added
later. It is now csrf.Require on the router, so it covers the routes that are
not written yet, it runs before routing — an unrouted POST is refused rather
than 404'd — and the refusal is the shared sentence. Host comparison becomes
case-insensitive, which is strictly more permissive and only for an operator
typo in the config.

threads.html becomes _threads.html, the partial spelling pages.Load discovers,
and the review page takes container-fluid: two prose columns do not fit the
centred container, which is what cover and dolt already concluded for their wide
views.

What stays here: the sentinel-to-status mapping in fail, and a renderError that
wraps ecore's error body in this service's view struct.
c7477607 — Eugene Blikh 10 days ago
authn: accept tokens.sr.ht working tokens beside the agent token

A second agent credential plane, next to the existing one rather than in
place of it. The agent_token table, every agent configured with it, and
the refs rule and provenance requirement around it are untouched; the
local plane is removed in a later phase, not this one.

The resolver tries the instance plane first and falls back to the local
store on exactly two refusals, bearer.ErrInvalid and bearer.ErrNotOurs.
spec's local token has no prefix to discriminate on — it is 32 random
bytes in base64, which is precisely what "did not decode as one of ours"
looks like — so the fallback replaces the shape test bench and cover can
afford. ErrRevoked, ErrForbidden and ErrUnavailable are terminal: a
withdrawn credential must not get a second chance at the old door, and an
unreachable daemon must not silently degrade into the legacy plane.

Grants ride on the principal and are checked where the action is known,
never in the middleware, which runs upstream of the router: spec:propose
in service.Propose, below both write surfaces, and spec:read in each read
surface's gate. /mcp checks per tool rather than at its Gate, because one
endpoint carries both kinds and a surface-wide read grant would refuse a
propose-only token at initialize. Principal.Authorize is a no-op off the
instance plane, which is what keeps the local token working.

The instance plane brings an owner where the local token had none, so a
working token belonging to anybody but [sr.ht] owner-name is refused
rather than admitted as a second identity: Principal.Owner is read by the
provenance committer, the refs rule's principal kind and the coreauth
AuthContext, all written for one human.

StatusFor is the one status table. ErrUnavailable is 503 and never 401 —
reading "I could not ask tokens.sr.ht" as "revoked" would refuse every
live instance token while a daemon that is deliberately off the hot path
restarts.

An instance with no [tokens.sr.ht] section builds no instance plane and
starts anyway, serving its own agent token as before.
f42f81ca — Eugene Blikh 13 days ago
feat(web): line-numbered unified prose diff replaces the block cards (spec-by6.3.5)

The block-card renderer was reviewed against a live proposal and rejected:
"ADDED PARAGRAPH" outweighed the content on every row, every block carried
identical chrome, and on a new file the whole page is one change, so the cards
added noise and no signal. Two columns of digits say the same thing and then
get out of the way, which is what a gutter is for.

Selection is by line, anchoring is by block. Lines are what the cursor lands
on; block hashes are what survive a reflow. The web layer maps a selected line
range onto its enclosing prosediff block and stores the existing
core.CommentAnchor unchanged — service/, db/ and core/ do not move and the POST
wire format is untouched. The composer states which block it will anchor to
before anything is typed, so the indirection is visible rather than magic.

A line number is never guessed. A modified prose block goes through
prosediff.WordsByLine, whose ok=false contract is honoured with a paired
old/new region stating a line RANGE; a block rewritten past the similarity
threshold takes that path too. An equal block that was rewrapped states an old
number only for the lines the old revision really holds — equal line counts
were the first rule and were not proof, which a property test over 2800
generated document/edit pairs found within seventeen cases.

The markup is a table because prose wraps and a number has to stay on the first
visual line of the line it names. One rail ground behind both number tracks
with a single hairline against the content; the change tint starts at the sign
column so the gutter never reads as part of the change; heading rows pin
themselves as the section readout, replacing the per-hunk breadcrumb that only
restated a heading three rows above.

Folding and commenting both work with JavaScript off — the fold is a checkbox,
and every block keeps a visible composer. With the script in, that per-block
composer is hidden and reached by selecting lines instead, because sixteen
identical "comment on this block" rows are the chrome this port removes. A
composer holding typed text is never hidden by anything.

Two pre-existing prosediff faults are fixed here because line numbers are what
made them visible: a thematic break reported line 1 for every rule in the
document, and a document whose entire content is "---" panicked in
splitFrontmatter.
472bcb1f — Eugene Blikh 24 days ago
feat(web): commentable prose diff with honest anchor state (spec-by6.3.3)

Reverses the diff view's founding rule. renderDocDiff skipped ChangeEqual
outright — "the review shows only what changed" — but any block of a proposed
document must be commentable, so unchanged blocks now render as collapsed,
dimmed context. Changed blocks keep their border, tint and full body, so the
page still reads as a diff at a glance rather than as a document dump. A
context block carrying a comment renders open. ChangeMoveIn now shows its text
too, since a comment control on invisible text is a control on nothing;
ChangeMoveOut stays a bare marker and is deliberately not commentable, because
the same paragraph is anchorable at its move-in position and two anchors for
one paragraph is the bug that avoids.

Every rendered block carries id="b-<16 hex>", hashed from the whole anchor
tuple. Not the page ordinal: an ordinal renumbers on any insertion above it, so
a saved link would silently scroll to a neighbouring paragraph, whereas
including the block hash makes a stale link resolve to nothing instead.

Threads are placed by anchor and by nothing else. Anything no rendered block
claims — an outdated anchor, an old-side anchor whose block the diff no longer
draws, a document the proposal no longer changes — is collected into a
page-level "comments that lost their anchor" area. Never dropped, never moved
onto a neighbour: a comment reads as authoritative about the block it sits
beside, so attaching it to the wrong one is worse than admitting it lost its
place. An edited anchor is drawn on its block and badged.

The comment form's anchor is built at submit time from the branch as it now
reads, through service.AnchorOf — hand-rolling the ordinal conversion here
would put the browser's comments on different blocks than the MCP tool's, which
is the one way two surfaces of one conversation disagree without either looking
broken. The form's block hash guards it: a block that moved while the page sat
open is a 409, not a comment attached to whatever took its place.

That hash is required rather than checked-when-present. Skipping the guard for
a form that omits it would let a later template refactor drop the hidden field
and disable the staleness check silently, with every test still green.

Authority is surfaced, not re-implemented: compose and resolve are the owner's
because service says so and ErrForbidden becomes a 403.

spec-by6.3.3