feat(docs): save chat answers as docs — edit screen, commit + push to the .env docs branch

This commit is contained in:
2026-09-01 03:52:03 -04:00
parent 7b7a834a1a
commit 725af9fac1
32 changed files with 4356 additions and 107 deletions
+66
View File
@@ -31,6 +31,16 @@ Data model — see ``.agent/PLAN.md`` §Data Model:
generation of the knowledge base is current,
bumped exactly once per KB-changing sync so saved
chats can be marked stale (phase 53).
* ``doc_drafts`` — server-side drafts of chat answers saved as
documentation: one row per "Save as doc" action
(the long answer body lives here, never in a URL),
keyed by an unguessable ``uuid4`` ``token`` (the
edit screen's URL credential — the share-token
trust model, phase 51); ``status`` moves
``draft`` → ``pushed`` (``branch`` +
``commit_sha`` recorded) when the push endpoint
commits + pushes the file to the
``BOR_DOCS_REPO`` branch (phase 59).
"""
from __future__ import annotations
@@ -205,6 +215,62 @@ class GitSource(Base):
added_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class DocDraft(Base):
"""One server-side draft of a chat answer saved as documentation
(phase 59, task 01).
A long answer body must live on the **server**, never in a URL: the
"Save as doc" action POSTs the answer's raw markdown to
``POST /api/doc-drafts`` (task 02), which stores it here and hands
back an unguessable 128-bit ``uuid4`` ``token`` — the edit
screen's URL credential (``/doc-edit.html?draft=<token>``, the
share-token trust model, phase 51). ``status`` stays ``draft``
until the push endpoint (task 04) commits + pushes the file to the
``BOR_DOCS_REPO`` branch — then it is ``pushed``, with ``branch``
and ``commit_sha`` recorded (the UI's branch + sha feedback; D3:
no PR tooling — the owner opens the PR themselves).
"""
__tablename__ = "doc_drafts"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
#: The URL credential (``/doc-edit.html?draft=<token>``): an
#: unguessable 128-bit ``uuid4`` — never the row id, never
#: sequential/guessable. Unique NOT NULL: unlike the NULLable
#: ``saved_chats.share_token`` there is no "un-drafted" state, so
#: NULLs never occur (always set on create).
token: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), unique=True, nullable=False, default=uuid.uuid4
)
#: The document's title. Defaults client-side to the last user
#: question (whitespace-collapsed, ≤120 chars — the chat auto-title
#: convention, phase 50); the edit screen changes anything.
title: Mapped[str] = mapped_column(Text)
#: The in-repo file path (default ``docs/<slug>.md``). Guard-railled
#: by the API layer (task 02 — repo-relative, no ``..``); the
#: column itself is plain TEXT (the ``documents.path`` precedent).
path: Mapped[str] = mapped_column(Text)
#: The markdown body — the answer's raw text (never HTML — the
#: ``bor.chat.v1`` record's ``text``), edited on the edit screen.
body: Mapped[str] = mapped_column(Text)
#: "draft" until the push endpoint commits + pushes the file, then
#: "pushed" — the domain is enforced by the API layer (the
#: ``git_sources.kind`` phase-38 precedent: plain TEXT + server
#: default, no CHECK constraint).
status: Mapped[str] = mapped_column(Text, default="draft", server_default="'draft'")
#: Set on push (task 04): the branch the commit landed on (the
#: ``BOR_DOCS_BRANCH`` name); NULL while still a draft.
branch: Mapped[str | None] = mapped_column(Text)
#: ... and the pushed branch's new HEAD sha (must equal
#: ``git rev-parse <branch>`` in the repo); NULL while still a
#: draft.
commit_sha: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class SavedChat(Base):
"""One owner-saved chat conversation (phase 50).