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
+56 -1
View File
@@ -9,7 +9,7 @@ import os
import re
from functools import lru_cache
from pydantic import field_validator
from pydantic import ValidationInfo, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
#: The built-in DEFAULT import formats (PLAN anchor A9, revised 2026-08-21;
@@ -178,6 +178,30 @@ class Settings(BaseSettings):
#: pattern).
upload_max_mb: int = 512
# --- Docs push (phase 59: save a chat answer as documentation) ---
#: The git repo a saved chat answer is committed to (phase 59, D3):
#: **any** remote — a URL (``https://``, ``ssh://``, ``git@``) or a
#: local path (generic git remote — no ``gh``, no GitHub assumption).
#: While empty the feature is inert: the "Save as doc" action is
#: hidden and the push endpoint 409s (the optional-feature pattern of
#: the git-sources env fallback).
docs_repo: str = ""
#: The branch pushes land on (phase 59): each push cuts it fresh from
#: ``docs_base_branch`` and ``git push --ff-only``s it — the owner
#: opens the PR themselves (D3: no PR tooling). A git branch token,
#: so no whitespace and no ``..`` (the validator below —
#: all-or-nothing with ``docs_repo``).
docs_branch: str = "bor-docs"
#: The branch each push bases off (fetched/reset before the
#: ``checkout -B`` of ``docs_branch``). Same token shape rules as
#: ``docs_branch``.
docs_base_branch: str = "main"
#: Where ``docs_repo`` is checked out on the server. Raw string —
#: ``Path.expanduser()`` is applied by the push service, not here
#: (the ``sources_dir``/``upload_dir`` convention). Deliberately kept
#: separate from ``sources_dir`` (the source checkouts).
docs_work_dir: str = "~/bor-docs"
@field_validator("import_extensions")
@classmethod
def _import_extensions_known(cls, v: str) -> str:
@@ -215,6 +239,28 @@ class Settings(BaseSettings):
raise ValueError("upload_max_mb must be > 0 (MiB)")
return v
@field_validator("docs_branch", "docs_base_branch")
@classmethod
def _docs_branch_tokens(cls, v: str, info: ValidationInfo) -> str:
"""Git branch-token shape guard (phase 59, D3) — all-or-nothing:
while ``docs_repo`` is empty the feature is inert, so the
(ignored) branch values must not block startup; once a repo IS
set, a blank / whitespace-bearing / ``..``-bearing branch is a
typo that would corrupt a ``git checkout`` argument, so it fails
loudly at startup (the ``agent_max_rounds`` pattern), naming the
field."""
repo = info.data.get("docs_repo")
if not isinstance(repo, str) or not repo.strip():
return v
name = info.field_name or "docs branch"
if not v.strip():
raise ValueError(f"{name} must not be empty while docs_repo is set")
if re.search(r"\s", v):
raise ValueError(f"{name} must not contain whitespace (a git branch token)")
if ".." in v:
raise ValueError(f"{name} must not contain '..' (a git branch token)")
return v
# Suggested questions (onboarding + empty state).
suggestions: list[str] = [
"How is my Kubernetes cluster set up?",
@@ -242,6 +288,15 @@ class Settings(BaseSettings):
"""
return [part.strip() for part in self.git_sources.split(",") if part.strip()]
@property
def docs_configured(self) -> bool:
"""True while a docs repo is configured (phase 59): the "Save as
doc" surface is live. Empty (or whitespace-only) ``docs_repo``
→ the feature is inert — no button for anyone, the push
endpoint 409s (the optional-feature pattern of the git-sources
env fallback)."""
return bool(self.docs_repo.strip())
@property
def effective_api_key(self) -> str:
"""API key for aipi: explicit setting, then $AIPI_KEY, then a placeholder."""