docs(6206): the remaining models, routes, mcp/tools/repos and config comments drop citations

A shipped tool docstring (mcp/tools/repos bind_repo) cited this instance's
"rule 1: dev is home"; it now says what a dev-branch project should do.
routes/knowledge, routes/tasks, routes/systems and models/retrieval_log state
the property each guards rather than the bug that once broke it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:44:31 -04:00
co-authored by Claude Opus 5.5
parent 6ec78b530e
commit f6734291c9
22 changed files with 54 additions and 62 deletions
+2 -2
View File
@@ -60,9 +60,9 @@ class Config:
# the MCP layer doesn't proxy web search (Claude has its own).
SEARXNG_URL: str = os.environ.get("SEARXNG_URL", "")
# Git forge integration (#2689) — optional read access to a git forge so
# Git forge integration — optional read access to a git forge so
# snippet bodies can be fetched/verified server-side. Connections are
# per-user keyring rows (#2778, Settings → Git forges); these env values
# per-user keyring rows (Settings → Git forges); these env values
# survive as an implicit keyring entry for ADMIN users' projects only, so
# a deployment can keep the operator's token in a Docker secret instead
# of the database. A stored row for the same host wins over the env entry.
+2 -2
View File
@@ -22,8 +22,8 @@ async def bind_repo(repo_url: str, project_id: int, ref: str = "") -> dict:
The binding is also what the shape ledger reads (refresh_pattern_coverage):
`ref` names the branch it follows. Default (""): the repo's default branch
— which means the ledger only sees work after a merge. A dev-first project
(rule 1: dev is home) should bind with ref="dev" so classification follows
— which means the ledger only sees work after a merge. A project that
works on a dev branch should bind with ref="dev" so classification follows
the push, not the merge. Re-binding with ref="" keeps the standing ref;
pass ref="-" to clear it back to the default branch.
+1 -1
View File
@@ -7,7 +7,7 @@ from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
class CanonicalSystem(Base, TimestampMixin, SoftDeleteMixin):
"""A GLOBAL area name — the shared vocabulary every project's Systems
can point at (milestone 307, decision note 3026).
can point at.
A `System` is per-project and NOT NULL on project_id, so nothing outside a
project can reference one: a rule that applies across projects has no way
+1 -1
View File
@@ -135,7 +135,7 @@ class DesignToken(Base, TimestampMixin, SoftDeleteMixin):
# and the token's own value are usually different colours entirely. No
# value-matching rule could ever connect them.
#
# Consumed by the source lint (#2277), not by the drift panel: these
# Consumed by the source lint, not by the drift panel: these
# literals live in component CSS, which the panel cannot see and says so.
supersedes: Mapped[list] = mapped_column(
JSONB, nullable=False, default=list, server_default=text("'[]'::jsonb")
+1 -1
View File
@@ -6,7 +6,7 @@ from scribe.models.base import TimestampMixin, iso
class ForgeConnection(Base, TimestampMixin):
"""One user's read-only credential for one git forge host (#2778).
"""One user's read-only credential for one git forge host.
The keyring model: a user owns a set of connections and every server-side
forge read for a project runs on the PROJECT OWNER's set, resolved by the
+3 -3
View File
@@ -6,7 +6,7 @@ from scribe.models.base import CreatedAtMixin, iso
class MomentMapping(Base, CreatedAtMixin):
"""One install's correction to which actions reach which moment (milestone 458).
"""One install's correction to which actions reach which moment.
The moments are the product's vocabulary and the shipped defaults say how
the common actions reach them (`services/moment_actions.py`). What no
@@ -20,8 +20,8 @@ class MomentMapping(Base, CreatedAtMixin):
say "not here", and saying so must survive an upgrade that ships the same
default again.
Text rather than a CHECK on `effect`, for retrieval_tuning_events' reason
(rule 36): the service validates it, and a constraint would buy nothing but
Text rather than a CHECK on `effect`, for retrieval_tuning_events' reason:
the service validates it, and a constraint would buy nothing but
a migration the day a third effect is wanted.
ONE ROW PER (user, tool, match, moment), so mapping the same thing twice
+6 -7
View File
@@ -54,23 +54,22 @@ class NoteUsageEvent(Base, CreatedAtMixin):
# A CONVENTION, not a fixed vocabulary: `mcp_<tool>` for an agent call,
# `rest_<kind>` for a human opening a detail view, and a bare name for a
# hook or background surface ('auto_inject', 'write_path_place',
# 'write_path_semantic'). This comment deliberately no longer lists the
# members — the previous list had gone stale, naming 'rest_note' that
# nothing wrote while omitting sources that existed, and a half-true
# enumeration reads as authoritative in exactly the way that misleads
# (#2476). `grep -rn record_pulled\\\|record_surfaced src/` is the
# 'write_path_semantic'). This comment deliberately does not list every
# member — such a list goes stale, naming sources nothing writes while
# omitting ones that exist, and a half-true enumeration reads as
# authoritative in exactly the way that misleads. `grep -rn record_pulled\\\|record_surfaced src/` is the
# authoritative list, and unlike a comment it cannot drift.
#
# The mcp_/rest_ split is load-bearing. "Is this dead weight?" is served by
# any pull; "was that injected line useful?" is served by AGENT pulls only,
# so never aggregate across the prefix without saying why (#1038, #2085).
# so never aggregate across the prefix without saying why.
source: Mapped[str] = mapped_column(Text, nullable=False)
# The project the READER was in when this happened — not the project the
# record belongs to, which is already on `notes.project_id`. The whole
# point is the comparison between the two: a record opened somewhere other
# than where it was written is the evidence that it TRANSFERRED, which is
# the claim the lesson kind rests on (milestone 385) and the one thing
# the claim the lesson kind rests on and the one thing
# RetrievalLog could half-answer and this table could not answer at all.
#
# Nullable, and null is ordinary rather than historical. A surfacing arm
+2 -2
View File
@@ -28,7 +28,7 @@ class Project(Base, TimestampMixin, SoftDeleteMixin):
design_system_id: Mapped[int | None] = mapped_column(
BigInteger, ForeignKey("design_systems.id", ondelete="SET NULL"), nullable=True
)
# The per-project forge pin (#2778). NULL is the ordinary state: forge
# The per-project forge pin. NULL is the ordinary state: forge
# reads resolve against the owner's keyring by repo host. When set, the
# project's forge reads use ONLY this connection — an explicit, auditable
# choice, constrained by the service layer to a connection the project
@@ -37,7 +37,7 @@ class Project(Base, TimestampMixin, SoftDeleteMixin):
BigInteger, ForeignKey("forge_connections.id", ondelete="SET NULL"),
nullable=True,
)
# The inception record (milestone 297): what this project was decided to
# The inception record: what this project was decided to
# inherit, when, and through which door — {decided_at, decided_by, via,
# choices: {design_system_id, seed_systems}}. NULL means nobody has
# decided yet, and enter_project asks; the effects themselves live in
+1 -1
View File
@@ -28,7 +28,7 @@ class RepoBinding(Base, TimestampMixin):
Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
)
repo_key: Mapped[str] = mapped_column(Text, nullable=False)
# The branch the coverage refresh reads for this binding (#2873); NULL =
# The branch the coverage refresh reads for this binding; NULL =
# the forge's default branch. Chosen at bind time so a dev-first project
# can have its ledger follow dev instead of waiting for the merge.
ref: Mapped[str | None] = mapped_column(Text, nullable=True)
+3 -4
View File
@@ -11,12 +11,11 @@ VERDICTS = (ON_POINT, ADJACENT, UNRELATED)
class RetrievalJudgment(Base, CreatedAtMixin):
"""One reviewer's verdict on one candidate of one logged retrieval call
(#4772).
"""One reviewer's verdict on one candidate of one logged retrieval call.
The relevance half the usage tables cannot hold. `note_usage_events` says
whether a line was OPENED, and since every menu line carries its matched
passage (#4364) "not opened" covers an unrelated line, a line whose passage
whether a line was OPENED, and because every menu line carries its matched
passage, "not opened" covers an unrelated line, a line whose passage
was enough, and a line the reader already had. Only someone reading the
query beside the line can tell those apart, so this records that reading —
with its reason, because a verdict nobody can re-read is a threshold.
+4 -4
View File
@@ -48,8 +48,8 @@ class RetrievalLog(Base):
# suppressed". `result_count == 0` alone conflates two different events —
# the ranker found nothing above threshold, and the ranker found something
# the reader already had — and only the first says a threshold is too high.
# Reading a zero as a ranker decline is how #3311 mis-scoped a milestone;
# an unmeasured value that renders as 0 is the same mistake with a nicer
# Reading a zero as a ranker decline sends a reader to move a floor that
# was never involved; an unmeasured value that renders as 0 is the same mistake with a nicer
# face, so surfaces that filter INSIDE the search leave this null.
suppressed_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
top_score: Mapped[float | None] = mapped_column(Float, nullable=True)
@@ -58,12 +58,12 @@ class RetrievalLog(Base):
# against `top_score`, which is the best it DID offer. They are equal on
# any call that returned something, and only this one exists on a call
# that returned nothing, which is the only place a bar can be judged from
# (#3670). Null means the caller did not measure it, never "nothing was
# Null means the caller did not measure it, never "nothing was
# close": a 0.0 there would read as a corpus with no relevant records at
# all, which is an artifact standing in for a measurement.
best_available_score: Mapped[float | None] = mapped_column(Float, nullable=True)
# WHICH record scored that, so a reader can judge what the bar refused
# rather than only how close it came (#3807). Written from the same ranked
# rather than only how close it came. Written from the same ranked
# candidate as the score above — the two describing different records would
# be worse than no id at all, because it invites judging the wrong one.
#
+3 -3
View File
@@ -10,7 +10,7 @@ from scribe.models.lesson_rule_link import CONFIRMED, LINK_STATES, REJECTED, SUG
# The same three states as a lesson's link to a rule, for the same reasons —
# re-exported so a reader of this table need not know where they were first
# named. CHECK ck_rule_moment_judgments_state (migration 0118, rule 36).
# named. CHECK ck_rule_moment_judgments_state (migration 0118).
JUDGMENT_STATES = LINK_STATES
__all__ = [
"CONFIRMED", "JUDGMENT_STATES", "NO_MOMENT", "REJECTED", "SUGGESTED",
@@ -31,7 +31,7 @@ SOURCES = ("pass", "signal", "edit")
class RuleMomentJudgment(Base, CreatedAtMixin):
"""Whether a rule belongs on a moment, and who said so (milestone 458 step 7).
"""Whether a rule belongs on a moment, and who said so.
A mount (`rule_moments`) is the current answer; this is the reasoning
behind it and the answers that are NOT mounts. One row per (rule, moment):
@@ -60,7 +60,7 @@ class RuleMomentJudgment(Base, CreatedAtMixin):
note: Mapped[str | None] = mapped_column(Text, nullable=True)
# The co-occurrence evidence (signal source), in lesson_rules' shape.
evidence: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
# The other direction (step 7b, migration 0119): sessions saying this
# The other direction (migration 0119): sessions saying this
# MOUNTED rule arrived at the moment and did not apply. lesson_rules'
# evidence shape keyed per day, plus the reasons given and the actions
# that reached the moment; `kept_at` once the operator kept the mount.
+2 -2
View File
@@ -25,7 +25,7 @@ class System(Base, TimestampMixin, SoftDeleteMixin):
Integer, ForeignKey("projects.id", ondelete="CASCADE")
)
name: Mapped[str] = mapped_column(Text, default="", server_default="")
# The GLOBAL area this local System is an instance of (milestone 307).
# The GLOBAL area this local System is an instance of.
# Nullable and SET NULL on purpose: the catalog is a convergence aid, not a
# gate — an unmapped System is fully usable, and retiring a canonical entry
# must never take a project's System with it. The local `name` is NEVER
@@ -40,7 +40,7 @@ class System(Base, TimestampMixin, SoftDeleteMixin):
status: Mapped[str] = mapped_column(Text, default="active", server_default="active")
order_index: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
# The files that ARE this area, as globs relative to the repo root
# (milestone 444). The description says what the area is FOR; these say
# The description says what the area is FOR; these say
# where it lives, so a command or edit touching those files can be
# resolved to the System — and its rulings — without a similarity search.
# NOT NULL with a `[]` default for the reason DesignToken.supersedes gives:
+1 -1
View File
@@ -10,7 +10,7 @@ PULLED = "pulled"
class SystemUsageEvent(Base, CreatedAtMixin):
"""One row per time a System's rulings were SHOWN to the agent because its
files were touched, or the System was PULLED in full (milestone 444).
files were touched, or the System was PULLED in full.
The third of the usage tables, after `note_usage_events` and
`rule_usage_events`, and separate from both for the reason the rule twin
+4 -4
View File
@@ -172,8 +172,8 @@ async def test_smtp():
return jsonify({"error": str(e)}), 500
# The forge CONFIG moved to per-user keyring rows (#2778, Settings → Git
# forges); what stays admin is the webhook secret, because the push endpoint
# The forge CONFIG is per-user keyring rows (Settings → Git forges); what
# stays admin is the webhook secret, because the push endpoint
# is one URL per instance and authenticates deliveries, not users.
@@ -329,13 +329,13 @@ async def run_db_maintenance_now():
return jsonify(summary)
# ── Scheduled backups (milestone 523) ────────────────────────────────────────
# ── Scheduled backups ────────────────────────────────────────────────────────
# One dated, uncompressed tar per run to a folder or an S3 bucket — see
# services/backup_file.py for why it is shaped that way, backup_destination.py
# for where it goes, backup_job.py for when.
# How long "Test" waits on the destination. A folder on a hung NFS mount has
# no timeout of its own (rule 156), and the request must come back either way.
# no timeout of its own, and the request must come back either way.
_BACKUP_CHECK_TIMEOUT = 45.0
+1 -1
View File
@@ -1,5 +1,5 @@
"""Canonical-system routes — the GLOBAL area vocabulary, and the mapping of a
project's Systems onto it (milestone 307, decision note 3026).
project's Systems onto it.
Two shapes live here because they are two halves of one idea:
+1 -1
View File
@@ -1,4 +1,4 @@
"""Design system + token REST endpoints (milestone #254 step 4).
"""Design system + token REST endpoints.
Wraps `services/design_systems.py`, which owns the ACL and the cycle guard.
Design systems are owner-scoped top-level records rather than project-scoped
+3 -3
View File
@@ -1,11 +1,11 @@
"""Family canon routes — the web door to the promotion engine (milestone 463).
"""Family canon routes — the web door to the promotion engine.
The agent decides promotions and each project's answers through the MCP
tools; this door exists so a person can READ every decision and its reasons
— the ideas, the adoption matrix, the log — and retire an idea or undo a
decision when they disagree. The promote and propose endpoints are here for
parity with the agent's door (rule 33), and are recorded as the operator's.
Every write is gated on the idea's note in the service (rule 78).
parity with the agent's door, and are recorded as the operator's.
Every write is gated on the idea's note in the service.
"""
import logging
+7 -11
View File
@@ -14,10 +14,9 @@ logger = logging.getLogger(__name__)
knowledge_bp = Blueprint("knowledge", __name__, url_prefix="/api/knowledge")
# Derived from the service's facet table, never re-listed here. This set was a
# hand-kept copy and had drifted three kinds behind it: it admitted `plan`
# (retired in 0066) and rejected `issue` (shipped in 0065, 435 rows) and
# `snippet` — so the browse surface could not filter to the kinds it was
# already rendering badges for (#3128).
# hand-kept copy drifts behind it — admitting retired kinds and rejecting new
# ones — until the browse surface cannot filter to the kinds it renders
# badges for.
_VALID_TYPES = FACET_TYPES
_VALID_SORTS = {"modified", "created", "alpha", "type"}
@@ -64,13 +63,10 @@ async def list_knowledge():
)
items = await label_shared_items(uid, items)
# The surfaced-vs-opened counts, on the list a person actually browses
# (#4230). `usage_for_notes` always worked on every note row, but only the
# snippet and rule lists ever attached it — and this is the ONLY lesson and
# note list in the UI, so those two kinds had the counter collected and
# shown nowhere. Attaching here rather than teaching `/api/lessons` a
# second time is what closes both holes at once: `/knowledge` is how notes,
# lessons and processes are all browsed.
# The surfaced-vs-opened counts, on the list a person actually browses.
# This is the ONLY lesson and note list in the UI — `/knowledge` is how
# notes, lessons and processes are all browsed — so attaching here is what
# shows those kinds' counters at all.
#
# Mixed kinds is not a problem for this: usage keys on the note row, which
# every facet of this feed is.
+1 -1
View File
@@ -1,5 +1,5 @@
"""Platform routes — the GLOBAL platform catalog, and which platforms a
project is (milestone 463 step 2).
project is.
Two shapes, as with the canonical areas:
+3 -4
View File
@@ -44,7 +44,7 @@ async def list_systems_route(project_id: int):
uid, project_id, include_archived=_truthy(request.args.get("include_archived")),
)
counts = await systems_svc.open_issue_counts_by_system(uid, project_id)
# How often each area's rulings reached a session (#4769) — one GROUP BY
# How often each area's rulings reached a session — one GROUP BY
# for the page, and a zero shape for an area nothing has touched.
usage = await system_usage_svc.usage_for_systems([s.id for s in systems])
out = []
@@ -67,9 +67,8 @@ async def create_system_route(project_id: int):
data = await request.get_json() or {}
if not (data.get("name") or "").strip():
return jsonify({"error": "name is required"}), 400
# The same gate the MCP door enforces. It lived only in the tool layer
# until now, which is exactly how the web UI shipped without gates the
# agent surface had (#2482) — one service call, one answer (rule 33).
# The same gate the MCP door enforces — one service call, one answer, so
# neither door ships without a gate the other has.
assessment = await systems_svc.assess_system_name(uid, project_id, data["name"])
duplicate = assessment["duplicate"]
if duplicate and not data.get("force"):
+2 -3
View File
@@ -242,9 +242,8 @@ async def update_task_route(task_id: int):
if "tags" in data:
fields["tags"] = data["tags"]
# Re-filing a task's kind. The editor has always SENT this field and the
# route silently dropped it — reporting "Task saved" and reverting on the
# next load (#3129). Validated at the same layer as status and priority so
# Re-filing a task's kind. The editor sends this field; dropping it would
# report "Task saved" and revert on the next load. Validated at the same layer as status and priority so
# an unrecognised value is a 400 rather than a database CHECK violation.
if "kind" in data:
try: