fix(slice4): make all entry write paths sidecar-aware (§22.4a carried from SLICE-1)
graduate, claim, retire/unretire, _read_meta_entry, mark_entry_reviewed, body extract/wrap (api_branches + api_prs replay) now dual-read and write metadata to the sidecar via write_entry_files + bot.commit_entry_files/ open_entry_pr — a migrated body-only .md no longer crashes entry.parse or re-grows frontmatter; legacy entries lazy-migrate on first metadata write. Existing tests updated to assert the sidecar (INV-2 clean docs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+36
-21
@@ -29,7 +29,7 @@ from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from . import auth, cache, chat as chat_layer, db, entry as entry_mod, funder, models_resolver, projects as projects_mod
|
||||
from . import auth, cache, chat as chat_layer, db, entry as entry_mod, funder, metadata as metadata_mod, models_resolver, projects as projects_mod
|
||||
from .bot import Bot
|
||||
from .config import Config
|
||||
from .gitea import Gitea, GiteaError
|
||||
@@ -40,6 +40,37 @@ log = logging.getLogger(__name__)
|
||||
RFC_FILE_PATH = "RFC.md"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# §22.4a SLICE-4: sidecar-aware body extract/wrap (pure, unit-testable)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _extract_body_pure(rfc, file_contents: str, branch: str, *, is_meta: bool) -> str:
|
||||
"""Editable body of an entry file. Meta-resident files carry a frontmatter
|
||||
envelope (legacy) or are already body-only (migrated, §22.4a); per-RFC repo
|
||||
files are body-only. Dual-read tolerant: a body-only `.md` returns as-is."""
|
||||
if not is_meta:
|
||||
return file_contents
|
||||
return metadata_mod.strip_frontmatter(file_contents)
|
||||
|
||||
|
||||
def _wrap_body_pure(rfc, prior_contents: str, new_body: str, branch: str, *, is_meta: bool) -> str:
|
||||
"""Inverse of `_extract_body_pure`. Under §22.4a the body lives in the `.md`
|
||||
and metadata in the sidecar, so wrapping is identity for body-only files —
|
||||
frontmatter is never re-grown here. A legacy un-migrated meta file still has
|
||||
its metadata in the `.md` frontmatter (no sidecar yet), so preserve it rather
|
||||
than silently dropping it on a pure body edit; it is migrated to body-only on
|
||||
its next *metadata* edit."""
|
||||
nb = new_body if new_body.endswith("\n") else new_body + "\n"
|
||||
if not is_meta:
|
||||
return nb
|
||||
if entry_mod.FRONTMATTER_RE.match(prior_contents):
|
||||
e = entry_mod.parse(prior_contents)
|
||||
e.body = nb
|
||||
return entry_mod.serialize(e)
|
||||
return nb
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request bodies
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1163,28 +1194,12 @@ def make_router(
|
||||
return RFC_FILE_PATH
|
||||
|
||||
def _extract_body(rfc, file_contents: str, branch: str = "main") -> str:
|
||||
"""For super-draft entries (and active-RFC pre-graduation reads
|
||||
per §9.8) the file on disk is the full frontmatter+body envelope;
|
||||
the editable body is entry.body. For active RFCs reading their
|
||||
per-RFC repo the file is just RFC.md and the whole thing is body."""
|
||||
if not _is_meta_target(rfc, branch):
|
||||
return file_contents
|
||||
try:
|
||||
entry = entry_mod.parse(file_contents)
|
||||
except Exception:
|
||||
return file_contents
|
||||
return entry.body
|
||||
return _extract_body_pure(
|
||||
rfc, file_contents, branch, is_meta=_is_meta_target(rfc, branch))
|
||||
|
||||
def _wrap_body(rfc, prior_contents: str, new_body: str, branch: str = "main") -> str:
|
||||
"""Inverse of _extract_body: re-wrap a new body into the entry
|
||||
envelope, preserving the prior frontmatter exactly."""
|
||||
if not _is_meta_target(rfc, branch):
|
||||
return new_body
|
||||
entry = entry_mod.parse(prior_contents)
|
||||
# Ensure exactly one trailing newline so the serializer's
|
||||
# round-trip is stable.
|
||||
entry.body = new_body if new_body.endswith("\n") else new_body + "\n"
|
||||
return entry_mod.serialize(entry)
|
||||
return _wrap_body_pure(
|
||||
rfc, prior_contents, new_body, branch, is_meta=_is_meta_target(rfc, branch))
|
||||
|
||||
async def _refresh_cache_for(rfc) -> None:
|
||||
if _is_meta_resident(rfc):
|
||||
|
||||
Reference in New Issue
Block a user