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). # the MCP layer doesn't proxy web search (Claude has its own).
SEARXNG_URL: str = os.environ.get("SEARXNG_URL", "") 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 # 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 # 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 # 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. # 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): The binding is also what the shape ledger reads (refresh_pattern_coverage):
`ref` names the branch it follows. Default (""): the repo's default branch `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 — which means the ledger only sees work after a merge. A project that
(rule 1: dev is home) should bind with ref="dev" so classification follows 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; the push, not the merge. Re-binding with ref="" keeps the standing ref;
pass ref="-" to clear it back to the default branch. 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): class CanonicalSystem(Base, TimestampMixin, SoftDeleteMixin):
"""A GLOBAL area name — the shared vocabulary every project's Systems """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 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 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 # and the token's own value are usually different colours entirely. No
# value-matching rule could ever connect them. # 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. # literals live in component CSS, which the panel cannot see and says so.
supersedes: Mapped[list] = mapped_column( supersedes: Mapped[list] = mapped_column(
JSONB, nullable=False, default=list, server_default=text("'[]'::jsonb") 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): 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 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 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): 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 moments are the product's vocabulary and the shipped defaults say how
the common actions reach them (`services/moment_actions.py`). What no 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 say "not here", and saying so must survive an upgrade that ships the same
default again. default again.
Text rather than a CHECK on `effect`, for retrieval_tuning_events' reason 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 the service validates it, and a constraint would buy nothing but
a migration the day a third effect is wanted. a migration the day a third effect is wanted.
ONE ROW PER (user, tool, match, moment), so mapping the same thing twice 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, # 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 # `rest_<kind>` for a human opening a detail view, and a bare name for a
# hook or background surface ('auto_inject', 'write_path_place', # hook or background surface ('auto_inject', 'write_path_place',
# 'write_path_semantic'). This comment deliberately no longer lists the # 'write_path_semantic'). This comment deliberately does not list every
# members — the previous list had gone stale, naming 'rest_note' that # member — such a list goes stale, naming sources nothing writes while
# nothing wrote while omitting sources that existed, and a half-true # omitting ones that exist, and a half-true enumeration reads as
# enumeration reads as authoritative in exactly the way that misleads # authoritative in exactly the way that misleads. `grep -rn record_pulled\\\|record_surfaced src/` is the
# (#2476). `grep -rn record_pulled\\\|record_surfaced src/` is the
# authoritative list, and unlike a comment it cannot drift. # authoritative list, and unlike a comment it cannot drift.
# #
# The mcp_/rest_ split is load-bearing. "Is this dead weight?" is served by # 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, # 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) source: Mapped[str] = mapped_column(Text, nullable=False)
# The project the READER was in when this happened — not the project the # 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 # record belongs to, which is already on `notes.project_id`. The whole
# point is the comparison between the two: a record opened somewhere other # 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 # 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. # RetrievalLog could half-answer and this table could not answer at all.
# #
# Nullable, and null is ordinary rather than historical. A surfacing arm # 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( design_system_id: Mapped[int | None] = mapped_column(
BigInteger, ForeignKey("design_systems.id", ondelete="SET NULL"), nullable=True 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 # reads resolve against the owner's keyring by repo host. When set, the
# project's forge reads use ONLY this connection — an explicit, auditable # project's forge reads use ONLY this connection — an explicit, auditable
# choice, constrained by the service layer to a connection the project # 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"), BigInteger, ForeignKey("forge_connections.id", ondelete="SET NULL"),
nullable=True, 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, # inherit, when, and through which door — {decided_at, decided_by, via,
# choices: {design_system_id, seed_systems}}. NULL means nobody has # choices: {design_system_id, seed_systems}}. NULL means nobody has
# decided yet, and enter_project asks; the effects themselves live in # 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 Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
) )
repo_key: Mapped[str] = mapped_column(Text, 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 # 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. # can have its ledger follow dev instead of waiting for the merge.
ref: Mapped[str | None] = mapped_column(Text, nullable=True) 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): class RetrievalJudgment(Base, CreatedAtMixin):
"""One reviewer's verdict on one candidate of one logged retrieval call """One reviewer's verdict on one candidate of one logged retrieval call.
(#4772).
The relevance half the usage tables cannot hold. `note_usage_events` says 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 whether a line was OPENED, and because every menu line carries its matched
passage (#4364) "not opened" covers an unrelated line, a line whose passage passage, "not opened" covers an unrelated line, a line whose passage
was enough, and a line the reader already had. Only someone reading the 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 — 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. 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 — # suppressed". `result_count == 0` alone conflates two different events —
# the ranker found nothing above threshold, and the ranker found something # 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. # 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; # Reading a zero as a ranker decline sends a reader to move a floor that
# an unmeasured value that renders as 0 is the same mistake with a nicer # 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. # face, so surfaces that filter INSIDE the search leave this null.
suppressed_count: Mapped[int | None] = mapped_column(Integer, nullable=True) suppressed_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
top_score: Mapped[float | None] = mapped_column(Float, 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 # 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 # 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 # 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 # 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. # all, which is an artifact standing in for a measurement.
best_available_score: Mapped[float | None] = mapped_column(Float, nullable=True) best_available_score: Mapped[float | None] = mapped_column(Float, nullable=True)
# WHICH record scored that, so a reader can judge what the bar refused # 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 # 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. # 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 — # 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 # 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 JUDGMENT_STATES = LINK_STATES
__all__ = [ __all__ = [
"CONFIRMED", "JUDGMENT_STATES", "NO_MOMENT", "REJECTED", "SUGGESTED", "CONFIRMED", "JUDGMENT_STATES", "NO_MOMENT", "REJECTED", "SUGGESTED",
@@ -31,7 +31,7 @@ SOURCES = ("pass", "signal", "edit")
class RuleMomentJudgment(Base, CreatedAtMixin): 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 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): 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) note: Mapped[str | None] = mapped_column(Text, nullable=True)
# The co-occurrence evidence (signal source), in lesson_rules' shape. # The co-occurrence evidence (signal source), in lesson_rules' shape.
evidence: Mapped[dict | None] = mapped_column(JSONB, nullable=True) 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' # MOUNTED rule arrived at the moment and did not apply. lesson_rules'
# evidence shape keyed per day, plus the reasons given and the actions # evidence shape keyed per day, plus the reasons given and the actions
# that reached the moment; `kept_at` once the operator kept the mount. # 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") Integer, ForeignKey("projects.id", ondelete="CASCADE")
) )
name: Mapped[str] = mapped_column(Text, default="", server_default="") 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 # 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 # 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 # 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") status: Mapped[str] = mapped_column(Text, default="active", server_default="active")
order_index: Mapped[int] = mapped_column(Integer, default=0, server_default="0") 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 # 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 # where it lives, so a command or edit touching those files can be
# resolved to the System — and its rulings — without a similarity search. # resolved to the System — and its rulings — without a similarity search.
# NOT NULL with a `[]` default for the reason DesignToken.supersedes gives: # 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): class SystemUsageEvent(Base, CreatedAtMixin):
"""One row per time a System's rulings were SHOWN to the agent because its """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 The third of the usage tables, after `note_usage_events` and
`rule_usage_events`, and separate from both for the reason the rule twin `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 return jsonify({"error": str(e)}), 500
# The forge CONFIG moved to per-user keyring rows (#2778, Settings → Git # The forge CONFIG is per-user keyring rows (Settings → Git forges); what
# forges); what stays admin is the webhook secret, because the push endpoint # stays admin is the webhook secret, because the push endpoint
# is one URL per instance and authenticates deliveries, not users. # is one URL per instance and authenticates deliveries, not users.
@@ -329,13 +329,13 @@ async def run_db_maintenance_now():
return jsonify(summary) return jsonify(summary)
# ── Scheduled backups (milestone 523) ──────────────────────────────────────── # ── Scheduled backups ────────────────────────────────────────────────────────
# One dated, uncompressed tar per run to a folder or an S3 bucket — see # 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 # services/backup_file.py for why it is shaped that way, backup_destination.py
# for where it goes, backup_job.py for when. # for where it goes, backup_job.py for when.
# How long "Test" waits on the destination. A folder on a hung NFS mount has # 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 _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 """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: 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. 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 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 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 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 — 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 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. 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 (rule 78). Every write is gated on the idea's note in the service.
""" """
import logging import logging
+7 -11
View File
@@ -14,10 +14,9 @@ logger = logging.getLogger(__name__)
knowledge_bp = Blueprint("knowledge", __name__, url_prefix="/api/knowledge") knowledge_bp = Blueprint("knowledge", __name__, url_prefix="/api/knowledge")
# Derived from the service's facet table, never re-listed here. This set was a # 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` # hand-kept copy drifts behind it — admitting retired kinds and rejecting new
# (retired in 0066) and rejected `issue` (shipped in 0065, 435 rows) and # ones — until the browse surface cannot filter to the kinds it renders
# `snippet` — so the browse surface could not filter to the kinds it was # badges for.
# already rendering badges for (#3128).
_VALID_TYPES = FACET_TYPES _VALID_TYPES = FACET_TYPES
_VALID_SORTS = {"modified", "created", "alpha", "type"} _VALID_SORTS = {"modified", "created", "alpha", "type"}
@@ -64,13 +63,10 @@ async def list_knowledge():
) )
items = await label_shared_items(uid, items) items = await label_shared_items(uid, items)
# The surfaced-vs-opened counts, on the list a person actually browses # 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 # This is the ONLY lesson and note list in the UI — `/knowledge` is how
# snippet and rule lists ever attached it — and this is the ONLY lesson and # notes, lessons and processes are all browsed — so attaching here is what
# note list in the UI, so those two kinds had the counter collected and # shows those kinds' counters at all.
# 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.
# #
# Mixed kinds is not a problem for this: usage keys on the note row, which # Mixed kinds is not a problem for this: usage keys on the note row, which
# every facet of this feed is. # every facet of this feed is.
+1 -1
View File
@@ -1,5 +1,5 @@
"""Platform routes — the GLOBAL platform catalog, and which platforms a """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: 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")), uid, project_id, include_archived=_truthy(request.args.get("include_archived")),
) )
counts = await systems_svc.open_issue_counts_by_system(uid, project_id) 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. # 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]) usage = await system_usage_svc.usage_for_systems([s.id for s in systems])
out = [] out = []
@@ -67,9 +67,8 @@ async def create_system_route(project_id: int):
data = await request.get_json() or {} data = await request.get_json() or {}
if not (data.get("name") or "").strip(): if not (data.get("name") or "").strip():
return jsonify({"error": "name is required"}), 400 return jsonify({"error": "name is required"}), 400
# The same gate the MCP door enforces. It lived only in the tool layer # The same gate the MCP door enforces — one service call, one answer, so
# until now, which is exactly how the web UI shipped without gates the # neither door ships without a gate the other has.
# agent surface had (#2482) — one service call, one answer (rule 33).
assessment = await systems_svc.assess_system_name(uid, project_id, data["name"]) assessment = await systems_svc.assess_system_name(uid, project_id, data["name"])
duplicate = assessment["duplicate"] duplicate = assessment["duplicate"]
if duplicate and not data.get("force"): 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: if "tags" in data:
fields["tags"] = data["tags"] fields["tags"] = data["tags"]
# Re-filing a task's kind. The editor has always SENT this field and the # Re-filing a task's kind. The editor sends this field; dropping it would
# route silently dropped it — reporting "Task saved" and reverting on the # report "Task saved" and revert on the next load. Validated at the same layer as status and priority so
# next load (#3129). Validated at the same layer as status and priority so
# an unrecognised value is a 400 rather than a database CHECK violation. # an unrecognised value is a 400 rather than a database CHECK violation.
if "kind" in data: if "kind" in data:
try: try: