~bigbes/sr-ht-spec

ref: e183a11e6590550c867395a5e1ef1acf9c16adf9 sr-ht-spec/gitx d---------
dd56e38c — Eugene Blikh 24 days ago
refactor(doc): one route from a revision to an Archive (spec-wcr #2, #4)

doc.Scan/DocumentSource were production-dead after service.Archive landed —
their only consumer was doc's own test fixture — and they were the seam that
made web's layering violation writable: a surface could reach past service/
into gitx and build its own archive. Deleted, so doc/ now owns no way to read
a revision and service.Archive is the single route from rev to Archive. The
fixture reads through ListDocuments + FromDocuments instead.

TestScanReportsGitErrors covered an unknown revision failing rather than
walking empty, which gitx did not test itself. Relocated there as
TestWalkOfAnUnknownRevisionFails rather than dropped.

linkHierarchy passed path.Dir(p.Path) where every other call site passes DirOf.
Unobservable today: the two differ only at the space root, and there the
section-proximity step is subsumed by the same-directory step above it, so "."
only ever skipped a lookup that had already answered. Verified by reverting and
re-running. Changed anyway — it stays unobservable only by coincidence of two
ranking rules — with a test pinning the invariant that `parent:` resolves to
whatever the same wikilink in the body resolves to.

spec-wcr
29370c57 — Eugene Blikh 27 days ago
refactor: one derivation of a proposal's branch name, in core

db.ProposalBranch and gitx.ProposalBranch each spelled out "proposals/" +
id with different signatures and different error behaviour: db formatted
anything including 0, gitx refused a non-positive id. Two derivations of
one name is a proposal whose row and whose ref can disagree, which is
painful to trace and cheap to prevent.

core now owns ProposalPrefix and ProposalBranch(int64) (string, error).
gitx.ProposalPrefix and db.BranchPrefix are that constant, gitx wraps
core's error in ErrBadRev so its callers keep their failure class, and
db.ProposalBranch delegates — inheriting core's refusal of a non-positive
id, which is why its signature grew an error.
b217c7a7 — Eugene Blikh 27 days ago
refactor: move the reconciler's two deletes down to the layers that own them

The reconciler reached past its layer twice, because the primitives it
needed did not exist: a raw DELETE FROM proposal — the only SQL written
outside db/ — and a go-git RemoveReference under gitx's write lock. Both
move down, with no change in behaviour.

db.Store.DeleteOpenProposal keeps the guard in the statement, as
resolveProposal does, and distinguishes the two ways it can bite: a row
that is gone is ErrNotFound, a row that has been resolved is the new
ErrProposalNotOpen, which tells the reconciler "nothing to repair" apart
from "the repair no longer applies".

gitx.Repo.DeleteProposalBranch refuses anything outside proposals/* —
the only thing between a caller bug and a deleted approved branch — takes
the per-space write lock like every other write, and treats an
already-absent branch as success: the repair is a postcondition, and the
ref may legitimately vanish between the listing that found it and the
delete.
29f6e881 — Eugene Blikh 27 days ago
feat: gitx — bare space repos, the git-object read path, and the id-keyed merge

One storage tier: there is no checkout on disk, so every read resolves a tree
and reads blobs. The approved head, a pinned ?rev= and a proposal branch are
the same code path with a different revision.

Lifecycle: Create makes a bare repo at <root>/~<owner>/<space> with HEAD on the
approved branch and one empty initial commit, so nothing downstream has to
special-case an unborn branch. Open reads the approved branch back from HEAD.

Merge: tree-splice, exactly as designed. Staleness is keyed by document ID, not
path — each changed document is resolved to its path on the approved head
through its frontmatter id before its blob is compared, so a rename between the
base and the head neither invents a conflict nor resurrects the moved path. On
staleness a *StaleError carries the current head for the caller's 409.
Otherwise the approved tree is rewritten with each document's blob at its path
in the head and committed with two explicit parents [approvedHead,
proposalHead]. Repository.Merge is never called; go-git v5 supports only
FastForwardMerge and this is pure object.Tree plumbing.

Refs rule: CheckRefUpdate is a pure predicate over (principal, ref, ancestry) —
agents may only touch proposals/*, the approved branch takes fast-forwards from
the human only and is never force-updated or deleted. hooks/ calls it without a
repository, which is why ancestry is an input rather than a lookup.

Concurrency: a process-wide per-space mutex keyed by repository directory guards
every write, and every ref move is a compare-and-swap that rebuilds and retries
when it loses to a concurrent native receive-pack push.

Bounded throughout: context deadlines on every operation, a per-blob cap and
per-walk byte and entry budgets. Nothing is truncated — a partially read
document would be indexed and served as though whole, so it fails instead.

Tests build every fixture programmatically through this package's own plumbing,
never the git binary, and cover the rename between base and head, each
staleness case, the two-parent merge shape, the refs rule for both principals
including force-update rejection, oversized blobs, and the CAS retry.