docs(6206): settings, projects, api routes and three models drop citations; four capitalised services stragglers
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m7s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 24s

- api.py's build-version docstring states why a channel name is not a build
  id, without the dated incident.
- rule_version states why rules need a history, without the rule that was
  rescoped and the date.
- Capitalised "Rule N" / "Milestone N" citations the earlier lowercase grep
  missed, in reply_shapes, supersession, services/family and models/family.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:42:41 -04:00
co-authored by Claude Opus 5.5
parent 78e1bdbd93
commit 0c88e1b0b0
9 changed files with 44 additions and 51 deletions
+6 -9
View File
@@ -12,14 +12,11 @@ async def health():
def build_version_payload() -> dict:
"""What build is this, separated into the values that answer different
questions (rule 149).
questions.
UNTIL 2026-08-31 THIS RETURNED THE CHANNEL. `BUILD_VERSION` in CI was
literally "dev" / "main" / the tag, so a running instance reported
`{"version": "main"}` — a channel name sitting where a build identifier
belongs. The cost was concrete: with a deploy misbehaving, nothing on the
instance could say which commit was serving it, and the one endpoint whose
job that is answered with the name of a branch.
A channel name ("dev", "main") is not a build identifier: with a deploy
misbehaving, the one endpoint whose job is to say which commit is serving
must not answer with the name of a branch.
The three values, and why they are three:
@@ -33,7 +30,7 @@ def build_version_payload() -> dict:
- `channel` — its own field, never folded into the name.
Plus `commit`, so the artifact's claim about itself can be checked against
the `:<sha>` it was published under (rule 145).
the `:<sha>` it was published under.
ABSENT RATHER THAN EMPTY when unknown. A local build has no ordering key
and no channel, and saying so is honest; emitting `""` or a placeholder
@@ -43,7 +40,7 @@ def build_version_payload() -> dict:
payload: dict = {"version": os.environ.get("APP_VERSION", "dev")}
# Reported verbatim, never validated against an enum — a build claiming
# something unexpected is better shown than dropped (rule 149).
# something unexpected is better shown than dropped.
for key, env in (("channel", "APP_CHANNEL"), ("commit", "APP_COMMIT")):
value = (os.environ.get(env) or "").strip()
if value:
+8 -8
View File
@@ -33,10 +33,10 @@ async def list_projects_route():
projects = await list_projects_for_user(uid, status=status)
if include_summary:
# Batched: four queries plus two, in two sessions, for ALL projects.
# This replaced an asyncio.gather over a per-project summary that opened
# its own session and then one more per milestone — ~250 concurrent
# checkouts against a pool of 15, all waiting out the 30s timeout and
# starving every other route on the instance (#2384).
# An asyncio.gather over a per-project summary would open a session per
# project and one more per milestone — far more concurrent checkouts
# than the pool holds, all waiting out the checkout timeout and
# starving every other route on the instance.
#
# Grouped by OWNER because a shared project's counts belong to its
# owner's records, matching what the per-project path passed.
@@ -67,7 +67,7 @@ async def create_project_route():
status = data.get("status", "active")
if status not in ("active", "paused", "completed", "archived"):
return jsonify({"error": "status must be 'active', 'paused', 'completed', or 'archived'"}), 400
# The inception decision rides the create (milestone 297): the UI's
# The inception decision rides the create: the UI's
# second step sends `inception: {choices}`; absent = undecided, and the
# project page shows the card until it is. Validated before the create
# so a bad decision never leaves a half-made project behind.
@@ -98,7 +98,7 @@ async def create_project_route():
@projects_bp.route("/<int:project_id>/inception", methods=["POST"])
@login_required
async def decide_inception_route(project_id: int):
"""Record (or re-record) what a project inherits — milestone 297.
"""Record (or re-record) what a project inherits.
Body: the choices object {design_system_id, seed_systems}; owner-only."""
uid = get_current_user_id()
data = await request.get_json() or {}
@@ -185,7 +185,7 @@ async def get_coverage_route(project_id: int):
project, _ = result
owner_uid = project.user_id or uid
return jsonify({
# The OWNER's keyring (#2778) — whether refresh could do anything,
# The OWNER's keyring — whether refresh could do anything,
# regardless of who is looking.
"configured": (await get_forges(owner_uid, project_id)).configured,
"coverage": await cached_coverage(owner_uid, project_id),
@@ -230,7 +230,7 @@ async def refresh_coverage_route(project_id: int):
@projects_bp.route("/<int:project_id>/forge", methods=["PUT"])
@login_required
async def set_project_forge_route(project_id: int):
"""Pin the project to one forge connection, or clear the pin (#2778).
"""Pin the project to one forge connection, or clear the pin.
Body: {"connection_id": <id> | null}. Owner-or-admin may ask; either way
the pin can only reference a connection the project OWNER holds — the
+7 -7
View File
@@ -24,7 +24,7 @@ settings_bp = Blueprint("settings", __name__, url_prefix="/api/settings")
# read and skip the mask on write; this generic KV surface has to apply the
# same treatment, or it silently un-masks what those endpoints masked — the
# rows live on the admin's own user_id, so the plain GET returned them raw.
# (forge_token left with 0078: forge credentials are keyring rows now, #2778.)
# (No forge_token here: forge credentials are keyring rows, since migration 0078.)
_SECRET_KEYS = frozenset({"smtp_password", "forge_webhook_secret"})
# What the tuning history records for a dial changed through this form. The MCP
@@ -62,10 +62,10 @@ async def update_settings_route():
for k, v in data.items():
str_v = str(v)
# A retrieval dial is never written as a plain settings row, wherever
# the request came from (#4102). The value would land correctly and the
# tuning history would say nothing happened — and since milestone 416
# those dials are moved by the model on the operator's behalf, a
# history with holes in it is worse than none: it reads as complete.
# the request came from. The value would land correctly and the tuning
# history would say nothing happened — and since those dials are moved
# by the model on the operator's behalf, a history with holes in it is
# worse than none: it reads as complete.
#
# Routed rather than refused on purpose. Refusing would work only for
# callers that had been updated; this way every caller that ever writes
@@ -113,7 +113,7 @@ async def test_search():
return jsonify({"configured": True, "results": [], "searxng_url": Config.SEARXNG_URL})
# --- forge connections (#2778) ------------------------------------------------
# --- forge connections ---------------------------------------------------------
# The user's keyring: read-only forge credentials, one per host, resolved by
# repo host for every server-side forge read on the user's projects. Strictly
# own-rows — a connection is a credential, and there is no admin view of
@@ -201,7 +201,7 @@ async def delete_forge_connection_route(connection_id: int):
async def test_forge_connection_route(connection_id: int):
"""Probe the SAVED connection: reachability and token acceptance in one
press, so a misconfiguration is visible now rather than as silent
fallbacks later (#2663's lesson, applied per keyring row)."""
fallbacks later, per keyring row."""
from scribe.services.forge import ForgeError, build_adapter
from scribe.services.forge_connections import connection_token, get_connection