docs(6205): the remaining services files state the reason, not the instance that taught it
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m46s
CI & Build / Python tests (push) Successful in 2m36s
CI & Build / Build & push image (push) Successful in 31s

The 1-2 hit tail: access, background, backup, backup_job, canonical_systems,
design_stylesheet, forge_connections, moment_actions, notes, notifications,
placement, planning, platforms, projects, record_batch, recurrence,
report_check, retrieval_review, rule_scope, rule_versions, scheduler,
shape_check, supersession, system_rulings, system_usage, text, trash,
verification, version_pinning.

Also corrected while reading:
- backup.py said restore step 16 fills the System map; step 15 does.
- version_pinning.py pointed at a design doc that is not in the repo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:34:24 -04:00
co-authored by Claude Opus 5.5
parent 172e784cb2
commit fa4f88747e
29 changed files with 82 additions and 100 deletions
+1 -1
View File
@@ -260,7 +260,7 @@ async def can_write_design_system(user_id: int, design_system_id: int) -> bool:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Set-based visibility (for LIST queries) # Set-based visibility (for LIST queries)
# #
# Two scopes, deliberately different (decision note 2094): # Two scopes, deliberately different:
# #
# readable_* — everything the ACL permits, including a record reachable ONLY # readable_* — everything the ACL permits, including a record reachable ONLY
# through a direct or group note share. For EXPLICIT acts: a # through a direct or group note share. For EXPLICIT acts: a
+5 -5
View File
@@ -2,7 +2,7 @@
The event loop holds only a WEAK reference to a task, so a bare The event loop holds only a WEAK reference to a task, so a bare
``create_task`` with no other holder can be garbage-collected mid-flight — a ``create_task`` with no other holder can be garbage-collected mid-flight — a
write that never errors and never lands (the #2663 GC footgun). This module is write that never errors and never lands. This module is
the one place that gets the pattern right: strong references in ``_pending``, the one place that gets the pattern right: strong references in ``_pending``,
discarded on completion, with failures logged at WARNING instead of vanishing. discarded on completion, with failures logged at WARNING instead of vanishing.
@@ -36,10 +36,10 @@ async def report_telemetry_failure(subsystem: str, site: str) -> None:
WARNING to the process log every time; one AppLog error row per process per WARNING to the process log every time; one AppLog error row per process per
(subsystem, site) so the admin UI shows the outage without host access. (subsystem, site) so the admin UI shows the outage without host access.
THIS IS NOT DECORATION. #2663 is the record of a telemetry subsystem running THIS IS NOT DECORATION. A telemetry subsystem whose every failure goes to
at zero for weeks — every counter reading empty, indistinguishable from ``logger.debug`` can run at zero — every counter reading empty,
"nobody uses this" — because every failure went to ``logger.debug``. A indistinguishable from "nobody uses this". A subsystem whose failures are
subsystem whose failures are all invisible cannot report its own death. all invisible cannot report its own death.
The AppLog write is itself guarded: when the database is down it fails too, The AppLog write is itself guarded: when the database is down it fails too,
and that is fine. The WARNING already said so, and a canary must never take and that is fine. The WARNING already said so, and a canary must never take
+1 -1
View File
@@ -2781,7 +2781,7 @@ async def _restore_v2(data: dict) -> dict:
session.add(event) session.add(event)
stats["rule_usage_events"] += 1 stats["rule_usage_events"] += 1
# And the System twin, after the Systems (step 16 fills their map). # And the System twin, after the Systems (step 15 fills their map).
for ev in data.get("system_usage_events", []): for ev in data.get("system_usage_events", []):
event = _build_system_usage_event(ev, maps) event = _build_system_usage_event(ev, maps)
if event is None: if event is None:
+3 -3
View File
@@ -3,8 +3,8 @@
ONE APScheduler job (services/scheduler.py) fires at the top of every hour and ONE APScheduler job (services/scheduler.py) fires at the top of every hour and
reads the config at fire time: when backups are on and the hour is the chosen reads the config at fire time: when backups are on and the hour is the chosen
one, it runs. Reading at fire time rather than scheduling the chosen hour one, it runs. Reading at fire time rather than scheduling the chosen hour
keeps the app's startup free of a database read (#4181 — a lifespan hook that keeps the app's startup free of a database read (a lifespan hook that waits
waits on the database is a worker that never serves), and a changed hour on the database is a worker that never serves), and a changed hour
needs no reschedule. needs no reschedule.
A run exports the full backup, writes it as the dated tar A run exports the full backup, writes it as the dated tar
@@ -40,7 +40,7 @@ KEEP_RUNS = 30
# room for one slow run, so a backup that lands at 02:40 instead of 02:05 # room for one slow run, so a backup that lands at 02:40 instead of 02:05
# does not flicker the page into a warning. # does not flicker the page into a warning.
STALE_AFTER = timedelta(hours=26) STALE_AFTER = timedelta(hours=26)
# The whole run's deadline (rule 156: a wait with no deadline is a bug). The # The whole run's deadline, because a wait with no deadline is a bug. The
# S3 upload has its own 15-minute timeout, but a folder on a hung NFS mount # S3 upload has its own 15-minute timeout, but a folder on a hung NFS mount
# has none, and a run that never returns holds the lock — every later backup # has none, and a run that never returns holds the lock — every later backup
# would be skipped as "already running", silently, forever. # would be skipped as "already running", silently, forever.
+3 -3
View File
@@ -1,5 +1,5 @@
"""The global canonical area vocabulary, and the mapping from a project's """The global canonical area vocabulary, and the mapping from a project's
Systems onto it (milestone 307 step 1, decision note 3026). Systems onto it.
A `System` is per-project. Nothing outside a project can reference one, so a A `System` is per-project. Nothing outside a project can reference one, so a
rule that spans projects has no way to say "this is about CI" without chaining rule that spans projects has no way to say "this is about CI" without chaining
@@ -210,8 +210,8 @@ async def set_system_canonical(
"""Map (or unmap) one project System onto a catalog entry. """Map (or unmap) one project System onto a catalog entry.
Authorised by the PROJECT, not the catalog: mapping changes the project's Authorised by the PROJECT, not the catalog: mapping changes the project's
row, so project write access is the right gate (rule 78 — never a bare row, so project write access is the right gate — never a bare owner
owner filter). Passing None clears the mapping. filter. Passing None clears the mapping.
Touches `canonical_id` and nothing else — the System's own name, charter Touches `canonical_id` and nothing else — the System's own name, charter
and record associations are left exactly as they are. and record associations are left exactly as they are.
+3 -3
View File
@@ -24,7 +24,7 @@ with every new element, where a purpose name (`--action-primary`) is reused.
SAFETY SAFETY
------ ------
Values are interpolated into a stylesheet, and design systems are shareable Values are interpolated into a stylesheet, and design systems are shareable
records (rule #47). A value of `red; } body { display: none` in a system someone records. A value of `red; } body { display: none` in a system someone
shared with you would otherwise inject CSS into your page. Everything rendered shared with you would otherwise inject CSS into your page. Everything rendered
here is validated or dropped — never escaped-and-hoped. here is validated or dropped — never escaped-and-hoped.
""" """
@@ -75,8 +75,8 @@ def selector_for_mode(mode: str, root_selector: str = ":root") -> str:
attribute selector, matching the convention already in the codebase. attribute selector, matching the convention already in the codebase.
The root selector is a PARAMETER because a container-scoped preview cannot The root selector is a PARAMETER because a container-scoped preview cannot
use `:root` — #251 recorded that mode scoping is one-way (light lives on use `:root` — mode scoping is one-way (light lives on `:root`, dark layers
`:root`, dark layers over it), so a generator that hardcoded `:root` could over it), so a generator that hardcoded `:root` could
not serve a preview at all. not serve a preview at all.
""" """
if mode == BASE_MODE: if mode == BASE_MODE:
+1 -1
View File
@@ -1,4 +1,4 @@
"""User-level forge connection CRUD — the keyring rows get_forges reads (#2778). """User-level forge connection CRUD — the keyring rows get_forges reads.
A connection is a user's read-only credential for one forge host; one row per A connection is a user's read-only credential for one forge host; one row per
(user, host) keeps host-keyed resolution deterministic with no default-pointer (user, host) keeps host-keyed resolution deterministic with no default-pointer
+2 -2
View File
@@ -1,4 +1,4 @@
"""Which actions reach which moment: shipped defaults plus each install's own (milestone 458 step 2). """Which actions reach which moment: shipped defaults plus each install's own.
The catalog (`services/moments.py`) says what the moments ARE. This module says The catalog (`services/moments.py`) says what the moments ARE. This module says
how the work gets there. One call can reach several moments: `kubectl apply` how the work gets there. One call can reach several moments: `kubectl apply`
@@ -63,7 +63,7 @@ COMMAND_TOOLS = frozenset({"bash"})
# procedures' own. # procedures' own.
SKILL_TOOL, SKILL_FIELD = "skill", "skill" SKILL_TOOL, SKILL_FIELD = "skill", "skill"
# A procedure also reaches the moment it is FOR (step 5): loading the # A procedure also reaches the moment it is FOR: loading the
# reporting procedure is a report as surely as the reply that follows it. # reporting procedure is a report as surely as the reply that follows it.
# #
# The product's own skills declare theirs in their SKILL.md frontmatter # The product's own skills declare theirs in their SKILL.md frontmatter
+1 -1
View File
@@ -845,7 +845,7 @@ async def mark_note_verified(
So a failed check leaves `verified_at` untouched and the note stays at the So a failed check leaves `verified_at` untouched and the note stays at the
top of the sweep until someone actually deals with it. top of the sweep until someone actually deals with it.
Write access, not read (rules 47/78): stamping is a mutation, and an Write access, not read: stamping is a mutation, and an
editor-share holder may make it while a viewer may not. editor-share holder may make it while a viewer may not.
Returns None when the note is not found, not writable, or carries no Returns None when the note is not found, not writable, or carries no
+4 -5
View File
@@ -149,7 +149,7 @@ async def send_invitation_email(email: str, invite_url: str, invited_by_username
def utc_day_start(day: date) -> datetime: def utc_day_start(day: date) -> datetime:
"""Midnight UTC on `day`, as an AWARE datetime — the reminder dedup window. """Midnight UTC on `day`, as an AWARE datetime — the reminder dedup window.
Deliberately a `datetime` and not the bare `date` (#1727). Comparing a Deliberately a `datetime` and not the bare `date`. Comparing a
`timestamptz` column against a `date` does work, via an implicit cast — but `timestamptz` column against a `date` does work, via an implicit cast — but
Postgres resolves that cast in the SESSION's TimeZone, so the window would Postgres resolves that cast in the SESSION's TimeZone, so the window would
move with a server setting nobody remembers is load-bearing. An aware UTC move with a server setting nobody remembers is load-bearing. An aware UTC
@@ -200,10 +200,9 @@ async def check_due_tasks() -> None:
# Dedup: check if we already sent a reminder today. # Dedup: check if we already sent a reminder today.
# `window_start` is an aware datetime, NOT `today.isoformat()`. # `window_start` is an aware datetime, NOT `today.isoformat()`.
# asyncpg binds a str as VARCHAR and Postgres has no # asyncpg binds a str as VARCHAR and Postgres has no
# `timestamptz >= text` operator, so this raised on every run # `timestamptz >= text` operator, so a string would raise on
# that got this far — swallowed by the per-user `except` below, # every run — swallowed by the per-user `except` below, leaving
# which is why the only symptom was reminders silently never # reminders silently never sending.
# sending plus an hourly traceback in the DB log (#1727).
dedup_result = await session.execute( dedup_result = await session.execute(
select(func.count(AppLog.id)).where( select(func.count(AppLog.id)).where(
AppLog.action == "task_reminder", AppLog.action == "task_reminder",
+4 -5
View File
@@ -1,13 +1,12 @@
"""Where a task sits — its project, its milestone, its step position, what is next. """Where a task sits — its project, its milestone, its step position, what is next.
WHY THIS EXISTS (milestone 409 step 1) WHY THIS EXISTS
An agent reporting finished work to the operator is asked to place it: which An agent reporting finished work to the operator is asked to place it: which
milestone, which step of how many, what comes next. Without those facts in milestone, which step of how many, what comes next. Without those facts in
hand it reconstructs them from memory, and a reconstruction reads exactly like hand it reconstructs them from memory, and a reconstruction reads exactly like
the real thing while being wrong — the drafting of the feature note that the real thing while being wrong — an invented milestone title, a "next" step
started this milestone invented a milestone title and named a "next" step that that is already done. So the facts come back on the write that changes a task,
was already done. So the facts come back on the write that changes a task,
where the report is about to be written, instead of being left to recall. where the report is about to be written, instead of being left to recall.
THE SHAPE THE SHAPE
@@ -57,7 +56,7 @@ from scribe.services import access as access_svc
from scribe.services.milestones import OPEN_STEP_STATUSES, _progress_from_counts from scribe.services.milestones import OPEN_STEP_STATUSES, _progress_from_counts
# Imported, not restated. The milestone summary names a plan's next open step # Imported, not restated. The milestone summary names a plan's next open step
# too (#4154), and the two answers must agree about what "open" means. # too, and the two answers must agree about what "open" means.
_OPEN = OPEN_STEP_STATUSES _OPEN = OPEN_STEP_STATUSES
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+1 -1
View File
@@ -9,7 +9,7 @@ kind=plan task is retired going forward — start_planning never creates one.
Steps passed up front are created in the SAME transaction as the milestone Steps passed up front are created in the SAME transaction as the milestone
(services/record_batch.py), so the plan body and the steps can cite each other (services/record_batch.py), so the plan body and the steps can cite each other
through `{{ref:N}}` placeholders instead of predicted ids (#4016). through `{{ref:N}}` placeholders instead of predicted ids.
""" """
from __future__ import annotations from __future__ import annotations
+2 -2
View File
@@ -1,5 +1,5 @@
"""Platforms — what a project is built on or ships as, and which projects are """Platforms — what a project is built on or ships as, and which projects are
which (milestone 463 step 2). which.
Membership is what makes "is this idea in family for that project?" a lookup Membership is what makes "is this idea in family for that project?" a lookup
rather than a judgment: a family idea is for some platforms, and it reaches rather than a judgment: a family idea is for some platforms, and it reaches
@@ -266,7 +266,7 @@ async def set_project_platforms(
user_id: int, project_id: int, updates: dict[str, str | None], user_id: int, project_id: int, updates: dict[str, str | None],
) -> list[dict]: ) -> list[dict]:
"""Apply a person's answers about a project's platforms. Write-gated on """Apply a person's answers about a project's platforms. Write-gated on
the project (rule 78). Platforms not named are left exactly as they are. the project. Platforms not named are left exactly as they are.
Raises ValueError, naming the problem, on a malformed update, an unknown Raises ValueError, naming the problem, on a malformed update, an unknown
slug, or no write access — before anything is written, so the update slug, or no write access — before anything is written, so the update
+9 -12
View File
@@ -132,19 +132,16 @@ async def get_project_summaries(
) -> dict[int, dict]: ) -> dict[int, dict]:
"""Summaries for MANY projects — four queries and one session, total. """Summaries for MANY projects — four queries and one session, total.
Replaces an `asyncio.gather` over the per-project version below, which was Not an `asyncio.gather` over the per-project version below: that is a
a nested fan-out: each project opened its own session for three queries, nested fan-out — each project opens its own session for three queries,
then called the milestone summary, which opened one more per milestone. For then calls the milestone summary, which opens one more per milestone. For
25 projects that asked for roughly 250 pooled connections at once against a a few dozen projects that asks for far more pooled connections than the
pool of 15 (SQLAlchemy's default 5 + 10 overflow), so most of them sat out pool holds, so most of them sit out the checkout timeout and everything
the 30-second checkout timeout and everything else on the instance queued else on the instance queues behind them, unrelated routes included.
behind them — including unrelated routes, which is why /api/settings
returned 500 while /api/projects took 30.9s (#2384).
The comment it replaced said "one backend pass instead of N+1 frontend Moving an N+1 from the browser to the server does not remove it; it moves
calls". It did remove the N+1 from the network — and recreated it against it onto the connection pool, where it is worse — the browser at least
the connection pool, where it is worse: the browser had at least been serialises its calls.
serialising those calls.
""" """
if not project_ids: if not project_ids:
return {} return {}
+4 -5
View File
@@ -1,9 +1,8 @@
"""Create several records at once, so they can cite each other without guessing ids. """Create several records at once, so they can cite each other without guessing ids.
Why this exists is in services/record_refs.py (#4016): a session that needs Why this exists is in services/record_refs.py: a session that creates records
records to reference one another used to create them one by one and predict one by one and predicts the ids of the ones not yet made loses those numbers
the ids of the ones not yet made — and any concurrent create, from any session to any concurrent create, from any session or any user.
or any user, took those numbers.
Here the whole batch is ONE transaction: insert every record, flush so the Here the whole batch is ONE transaction: insert every record, flush so the
sequence assigns the real ids, rewrite each `{{ref:N}}` with the id and title sequence assigns the real ids, rewrite each `{{ref:N}}` with the id and title
@@ -128,7 +127,7 @@ async def create_batch(
project_id = existing.project_id project_id = existing.project_id
if milestone is not None and not project_id: if milestone is not None and not project_id:
raise ValueError("a new milestone needs a project_id") raise ValueError("a new milestone needs a project_id")
# Share-aware (rule 78): a collaborator with write access to a shared # Share-aware: a collaborator with write access to a shared
# project may plan in it; a bare owner filter would refuse them. # project may plan in it; a bare owner filter would refuse them.
if project_id and not await access_svc.can_write_project(user_id, project_id): if project_id and not await access_svc.can_write_project(user_id, project_id):
raise ValueError(f"project {project_id} not found") raise ValueError(f"project {project_id} not found")
+1 -2
View File
@@ -138,8 +138,7 @@ async def spawn_recurring_tasks() -> int:
recurrence_rule=task.recurrence_rule, recurrence_rule=task.recurrence_rule,
) )
# No explicit embed here: create_note calls embed_note itself, so # No explicit embed here: create_note calls embed_note itself, so
# the spawn path stopped being a second copy of the embedding rule # the spawn path is not a second copy of the embedding rule.
# the moment that moved into the service (#2056, #280).
except Exception: except Exception:
logger.exception("Failed to spawn recurring task %d", task.id) logger.exception("Failed to spawn recurring task %d", task.id)
continue continue
+5 -8
View File
@@ -1,7 +1,7 @@
"""The report-shape check: does a reply that closed a task carry the completion """The report-shape check: does a reply that closed a task carry the completion
sections, and what is it sent back with when it does not. sections, and what is it sent back with when it does not.
WHY THIS EXISTS (milestone 409 step 5) WHY THIS EXISTS
Everything that helps an agent write a readable completion report arrives Everything that helps an agent write a readable completion report arrives
BEFORE the reply is written. The Stop hook is the one moment the finished reply BEFORE the reply is written. The Stop hook is the one moment the finished reply
@@ -13,13 +13,10 @@ exists, so a reply that closed a task is checked for the completion sections
- OWNING THE WORDS the agent is sent back with, so every client's hook says - OWNING THE WORDS the agent is sent back with, so every client's hook says
the same thing (plugin/PACKAGING.md: hooks carry timing and transport). the same thing (plugin/PACKAGING.md: hooks carry timing and transport).
ONE END-OF-TURN REQUEST (milestone 500 step 4). This used to be a Stop hook of ONE END-OF-TURN REQUEST. The Stop hook sends the reply once, with the ids of
its own that checked the sections in shell and reported here; the reply-moment the tasks the turn closed, and the section check runs here beside the reply
check sent the same reply to `/reply-rules` a moment later. The hook now sends hold. The patterns match the words that carry the meaning rather than exact
the reply once, with the ids of the tasks the turn closed, and the section headings, so a shape's wording can change without breaking this.
check runs here beside the reply hold. The patterns are the ones the shell
used, matched on the words that carry the meaning rather than exact headings,
so a shape's wording can change without breaking this.
app_logs rather than a table of its own: one small event with a JSON detail is app_logs rather than a table of its own: one small event with a JSON detail is
what that table holds, it already has retention and an admin viewer, and what that table holds, it already has retention and an admin viewer, and
+3 -3
View File
@@ -1,8 +1,8 @@
"""Judging what a retrieval arm offered — the review pass (#4772). """Judging what a retrieval arm offered — the review pass.
The usage tables record what the reader DID with a line: opened it or not. The usage tables record what the reader DID with a line: opened it or not.
Since every menu line carries its matched passage (#4364), that action no Because every menu line carries its matched passage, that action does not
longer stands in for relevance — a line can be unrelated, related and enough stand in for relevance — a line can be unrelated, related and enough
as shown, related and already in context, or related and ignored, and all four as shown, related and already in context, or related and ignored, and all four
are "not opened". Only someone reading the query beside the line can tell are "not opened". Only someone reading the query beside the line can tell
them apart. This module is how that reading gets done and kept. them apart. This module is how that reading gets done and kept.
+3 -4
View File
@@ -1,4 +1,4 @@
"""Which rules a caller may be shown — a rule's HOME, stated once (milestone 414). """Which rules a caller may be shown — a rule's HOME, stated once.
A rule lives in a rulebook topic — GLOBAL, it applies wherever its owner A rule lives in a rulebook topic — GLOBAL, it applies wherever its owner
works — or on one project, where it applies to that project and nowhere else. works — or on one project, where it applies to that project and nowhere else.
@@ -13,9 +13,8 @@ home, so it is answered here and nowhere else:
- `everywhere=True`: every rule the caller owns, in any home — the explicit - `everywhere=True`: every rule the caller owns, in any home — the explicit
whole-rulebook question. whole-rulebook question.
It was written inline in `semantic_search_rules` until the moment lookup Rule search and the moment lookup both need this answer. Two copies of a
(milestone 458) needed the same answer. Two copies of a scope clause is how scope clause is how one of them starts surfacing another project's rules.
one of them starts surfacing another project's rules, so it moved here first.
""" """
from __future__ import annotations from __future__ import annotations
+1 -1
View File
@@ -8,7 +8,7 @@ lifecycle: a soft delete keeps the rule row whole and restorable, so there is
nothing to preserve at delete time, and anything that outlived a purge would be nothing to preserve at delete time, and anything that outlived a purge would be
data the operator explicitly asked to be gone. data the operator explicitly asked to be gone.
WHAT IS DELIBERATELY NOT COPIED FROM note_versions (milestone 323). Both of its WHAT IS DELIBERATELY NOT COPIED FROM note_versions. Both of its
guards defend against note autosave, which fires every 60 seconds and would guards defend against note autosave, which fires every 60 seconds and would
otherwise burn every slot. Rules have no autosave — every version here comes otherwise burn every slot. Rules have no autosave — every version here comes
from a deliberate `update_rule` — so both guards would only ever discard a real from a deliberate `update_rule` — so both guards would only ever discard a real
+2 -2
View File
@@ -1,6 +1,6 @@
"""One APScheduler job bridged into the asyncio loop — the shape the four """One APScheduler job bridged into the asyncio loop — the shape the
*_scheduler modules (recurrence spawn, auto-pin scan, trash purge, DB *_scheduler modules (recurrence spawn, auto-pin scan, trash purge, DB
maintenance) each used to carry a private copy of. maintenance) share instead of each carrying a private copy.
APScheduler's BackgroundScheduler fires from a worker thread; the work is APScheduler's BackgroundScheduler fires from a worker thread; the work is
async and must run on the app's loop, so the fire is bridged with async and must run on the app's loop, so the fire is bridged with
+8 -9
View File
@@ -1,15 +1,14 @@
"""The end-of-turn shape check: what a turn wrote that nobody has judged, and what the agent is asked. """The end-of-turn shape check: what a turn wrote that nobody has judged, and what the agent is asked.
WHY THIS EXISTS (milestone 439) WHY THIS EXISTS
The shape ledger used to be filled by machinery: extraction found the The agent that WROTE the code is the one participant who knows what it is.
definitions, framework rules decided which were one-offs, and the write-path Machinery can find definitions and stamp "instance of #N", but it cannot say
hook stamped "instance of #N" with nobody reading. The agent that WROTE the that a first instance is a reusable piece — and a piece never recorded is
code — the one participant who knows what it is — was asked only in an audit, rebuilt by the next session. An audit run later asks too late, if anyone runs
weeks later, if anyone ran one. A first instance of a reusable piece was never one.
asked about at all, so it was never recorded, and the next session rebuilt it.
So the question moves to the moment the knowledge exists. The client's write So the question is asked at the moment the knowledge exists. The client's write
hooks keep a ledger of the definitions each turn wrote; its Stop hook sends hooks keep a ledger of the definitions each turn wrote; its Stop hook sends
them here at the end of the turn. Two jobs live on this side, as with the them here at the end of the turn. Two jobs live on this side, as with the
report check (services/report_check.py): report check (services/report_check.py):
@@ -66,7 +65,7 @@ def _evidence_note(evidence: dict) -> str:
def block_reason(unjudged: list[dict], *, project_id: int, repo: str) -> str: def block_reason(unjudged: list[dict], *, project_id: int, repo: str) -> str:
"""What the agent is told at the end of a turn that wrote unjudged shapes. """What the agent is told at the end of a turn that wrote unjudged shapes.
Phrased as the practice wanted (note #3565): what to decide and the one Phrased as a practice: what to decide and the one
call that records it, not a prohibition. The reusing-code skill owns the call that records it, not a prohibition. The reusing-code skill owns the
longer version of what each verdict means. longer version of what each verdict means.
""" """
+3 -3
View File
@@ -1,6 +1,6 @@
"""Which records have been overtaken by which — the claim, not the ranking. """Which records have been overtaken by which — the claim, not the ranking.
Step 2 of milestone #278. This module only records and reads the relation; the This module only records and reads the relation; the
demotion that makes it matter lives in the retrieval layer. demotion that makes it matter lives in the retrieval layer.
WHY THE CLAIM POINTS FORWARD WHY THE CLAIM POINTS FORWARD
@@ -202,8 +202,8 @@ async def attach_relations(user_id: int, note_id: int, data: dict, *, hint: bool
acted on confidently, which is worse than never surfacing it. acted on confidently, which is worse than never surfacing it.
Omitted entirely when empty, so an ordinary note's payload doesn't grow Omitted entirely when empty, so an ordinary note's payload doesn't grow
two permanently-empty lists (#2483 — a field that always says nothing two permanently-empty lists — a field that always says nothing trains
trains readers to skip fields). `hint=True` (the agent surface) also readers to skip fields. `hint=True` (the agent surface) also
attaches `superseded_note`, the one-sentence reading instruction. attaches `superseded_note`, the one-sentence reading instruction.
""" """
rel = await get_relations(user_id, note_id) rel = await get_relations(user_id, note_id)
+2 -2
View File
@@ -1,4 +1,4 @@
"""An area's rulings, delivered when its files are touched (milestone 444). """An area's rulings, delivered when its files are touched.
A RULING is the operator's decision about how one area of the work must behave. A RULING is the operator's decision about how one area of the work must behave.
It lives in a `Rulings` section at the end of the area's System description It lives in a `Rulings` section at the end of the area's System description
@@ -13,7 +13,7 @@ patterns or it does not. It takes no budget and no floor from the retrieval
arms beside it, and writes no retrieval_logs row — there is no score arms beside it, and writes no retrieval_logs row — there is no score
distribution for it to join. Its surfacings go to `system_usage_events`. distribution for it to join. Its surfacings go to `system_usage_events`.
ONCE IN FULL, THEN A REFERENCE (the #3750 shape). The hook keeps the Systems ONCE IN FULL, THEN A REFERENCE. The hook keeps the Systems
already shown this session and passes them back; a repeat renders as one short already shown this session and passes them back; a repeat renders as one short
line rather than the rulings again, and is not counted as a surfacing. line rather than the rulings again, and is not counted as a surfacing.
+2 -3
View File
@@ -1,6 +1,6 @@
"""System usage telemetry — were an area's rulings read once shown? """System usage telemetry — were an area's rulings read once shown?
The twin of `note_usage` and `rule_usage` for Systems (milestone 444). The The twin of `note_usage` and `rule_usage` for Systems. The
rulings arm shows a System's rulings when a command or edit touches the rulings arm shows a System's rulings when a command or edit touches the
System's files; a pull is somebody then opening the System (`get_system`). The System's files; a pull is somebody then opening the System (`get_system`). The
ratio says whether delivering rulings by path earns its line. ratio says whether delivering rulings by path earns its line.
@@ -97,8 +97,7 @@ async def usage_for_systems(system_ids: list[int]) -> dict[int, dict]:
One GROUP BY for the whole list, never a query per row — this decorates a One GROUP BY for the whole list, never a query per row — this decorates a
list view. Systems with no events come back as `empty_system_usage()`. list view. Systems with no events come back as `empty_system_usage()`.
Fails open: a readout that could break the Systems list is worse than a Fails open: a readout that could break the Systems list is worse than a
zero, but the failure is reported so the zero is not mistaken for silence zero, but the failure is reported so the zero is not mistaken for silence.
(#2663).
""" """
ids = [int(s) for s in system_ids] ids = [int(s) for s in system_ids]
out: dict[int, dict] = {sid: empty_system_usage() for sid in ids} out: dict[int, dict] = {sid: empty_system_usage() for sid in ids}
+3 -5
View File
@@ -25,7 +25,7 @@ def elide(text: str, budget: int) -> tuple[str, bool]:
This is the fallback, not the goal. Where the system knows WHICH span of a This is the fallback, not the goal. Where the system knows WHICH span of a
record is the relevant one — a semantic search knows exactly that, and record is the relevant one — a semantic search knows exactly that, and
stores it (#4243) — show that span and say so. Reach for this only when stores it — show that span and say so. Reach for this only when
nothing identifies a better part than "all of it". nothing identifies a better part than "all of it".
""" """
if budget <= 0 or len(text) <= budget: if budget <= 0 or len(text) <= budget:
@@ -41,11 +41,9 @@ def elide(text: str, budget: int) -> tuple[str, bool]:
# What a search result shows a reader, as one decision made in one place. # What a search result shows a reader, as one decision made in one place.
# #
# Every door onto a semantic search faces the same question — which span of a # Every door onto a semantic search faces the same question — which span of a
# record do I show someone deciding whether to open it? — and each used to # record do I show someone deciding whether to open it? A bare head cut shows
# answer it separately with a bare head cut of a different length: 240
# characters in the MCP search, 200 in the web's knowledge search. Both showed
# the document's OPENING, which is not the span that matched and not the span # the document's OPENING, which is not the span that matched and not the span
# the ranking was built on. # the ranking was built on, and a cut per door gives each a different answer.
# #
# Doors keep their own field names (the web UI renders `snippet`, the MCP # Doors keep their own field names (the web UI renders `snippet`, the MCP
# surface returns `excerpt`), because renaming a field a frontend consumes is # surface returns `excerpt`), because renaming a field a frontend consumes is
+1 -1
View File
@@ -170,7 +170,7 @@ async def restore_entity(user_id: int, entity_type: str, entity_id: int) -> int
The inverse of `delete(user_id, entity_type, entity_id)`, which returns a The inverse of `delete(user_id, entity_type, entity_id)`, which returns a
batch id the caller usually doesn't keep. Callers that need to undo their own batch id the caller usually doesn't keep. Callers that need to undo their own
soft-delete later — snippet un-merge (#2165) — know the entity id, not the soft-delete later — snippet un-merge — know the entity id, not the
batch, and looking it up here keeps them from reaching into the column set. batch, and looking it up here keeps them from reaching into the column set.
Restores the whole batch on purpose, not just the row: the batch is the Restores the whole batch on purpose, not just the row: the batch is the
+4 -5
View File
@@ -1,11 +1,10 @@
"""What a record's own check MEANS — shared by rules and notes. """What a record's own check MEANS — shared by rules and notes.
Two record types carry `verify_with` / `expires_when` / `verified_at`: rules Two record types carry `verify_with` / `expires_when` / `verified_at`: rules
(milestone 312) and notes (milestone 317). What they share is BEHAVIOUR — how and notes. What they share is BEHAVIOUR — how a stamp is read, how old it is,
a stamp is read, how old it is, what "never" means — not storage and not the what "never" means — not storage and not the query. The two sweeps stay
query. Note 3163 is the rule this file is an instance of: `semantic_search_*` separate siblings, because a rule scopes by rulebook ownership and a note
could never have been shared between them because a rule scopes by rulebook scopes by the note ACL.
ownership and a note scopes by the note ACL, so the two sweeps are siblings.
These functions are the part that genuinely is common, factored so it exists These functions are the part that genuinely is common, factored so it exists
once rather than twice. once rather than twice.
-2
View File
@@ -8,8 +8,6 @@ Tiers (pin_kind values):
None → rolling autosave; capped at MAX_VERSIONS, FIFO. None → rolling autosave; capped at MAX_VERSIONS, FIFO.
"auto" → system-declared via the stability scan; capped at MAX_AUTO_PINS, FIFO. "auto" → system-declared via the stability scan; capped at MAX_AUTO_PINS, FIFO.
"manual" → user-declared; unlimited, never pruned. "manual" → user-declared; unlimited, never pruned.
Design: docs/superpowers/specs/2026-05-13-note-version-pinning-design.md
""" """
from __future__ import annotations from __future__ import annotations