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.
feat: doc — warren's vault+render absorbed onto the git-object read path warren scanned a directory (filepath.WalkDir + os.ReadFile). There is no checkout here, so Scan walks a git tree through gitx instead and feeds the FromPages seam: the approved head, a pinned sha and a proposal branch are one code path with a different revision, and nothing downstream of Archive knows where its pages came from. Frontmatter is core's, not warren's. Front embeds core.Frontmatter and adds only what core deliberately does not model — parent, aliases, planned, and the ordered key list used for display and search text. Two parsers that disagree about a document header is a bug that surfaces in the ID registry months later. Documents key on their frontmatter id, falling back to their path: paths move and ids do not, and a duplicated id resolves to neither document rather than letting one win silently, matching what the merge already does with the approved branch. A header core rejects degrades to "no frontmatter" instead of failing, because --push-option=skip-validation means such a document can exist and refusing to render it would turn a typo into an outage.