feat: service.Archive — one accessor, one tree walk, one link graph web/ and mcpsrv/ both needed a *doc.Archive and each invented its own way to get one. web/ handed sp.Repo to doc.Scan — reaching past service/ into gitx, which the layering rule forbids — and then listed the documents a second time for their bodies, two tree walks per page view. mcpsrv/ converted []service.Document back into []gitx.Document, rebuilding hashes service/ had already stringified. Two workarounds around one missing accessor is how three agent-facing surfaces stop being identical. Service.Archive resolves the revision, walks the tree once, and returns the archive with the bodies. ArchiveFrom is the same construction over a Document set a caller already holds — the one place a hex object name is converted back to a hash, malformed ids refused rather than zeroed. doc.Page.Links and Page.WordCount were documented as "filled in by a render pass" and nothing filled them, so Archive.Backlinks always returned empty; web/ worked around that by re-rendering every document of the space on each page view. doc.Archive.LinkPass now owns the pass and the accessor runs it, so the link graph exists wherever the archive does. web/ and mcpsrv/ no longer import gitx or go-git, in tests either. doc.DirOf replaces the three copies of the same fromDir helper.
fix(service): separate the read-plane rev guard from the review path The previous commit's guard broke two service tests, which were right and the guard was too broad: rendering and diffing a proposal branch genuinely needs to read one, so a blanket ban on ref names is not the shape. ReadDocument keeps the strict object-name rule. ReadDocumentAtRef reaches an arbitrary ref, deliberately as a separate awkwardly-named method rather than a flag, so serving unreviewed content is something a caller asks for by name and a reviewer can grep for. A rejected rev now wraps ErrNotFound as well, preserving the existing deliberate property that a crafted revision cannot distinguish malformed from absent by probing.
feat: service — wiring, space lifecycle, read paths, push validation, reconciler Phase 1 of the implementation plan: the orchestration layer's read, validate and repair halves. The write plane (propose/merge) is Phase 3 and is absent. - Config/New assemble a Service from the shared config.ini, reporting every missing key in one message; TokenStore adapts db/ to authn.TokenStore, mapping db.ErrNotFound onto authn.ErrUnknownToken so an unknown credential is a 401 rather than a 503. - CreateSpace writes the repository first and the row second, removing the repository again if the insert fails; a crash between the two leaves content that is merely unlisted rather than a phantom space. - ReadDocument/ListDocuments/Policy/ResolveRev resolve the approved head or a pinned revision through one code path. - ValidatePush checks the refs rule first and unconditionally, then frontmatter and document-id uniqueness, which --push-option=skip-validation waives. The rejection is a structured, terminal-shaped message naming the document. - PlanRepairs is the repair table as a pure, table-tested function; Reconcile gathers the facts and applies them, listing stale-index spaces for Phase 2. Two departures from the design's repair table, both to stop the reconciler destroying live state, documented at their definitions: an open row with no branch is left alone inside a grace window (every propose passes through that state), and a branch still sitting on its recorded base is never treated as merged (its tip is trivially an ancestor of the approved head).