diff --git a/src/scribe/services/db_maintenance_scheduler.py b/src/scribe/services/db_maintenance_scheduler.py index f6b673ad..e95eb23d 100644 --- a/src/scribe/services/db_maintenance_scheduler.py +++ b/src/scribe/services/db_maintenance_scheduler.py @@ -25,16 +25,14 @@ logger = logging.getLogger(__name__) _JOB_ID = "db_maintenance_vacuum" _DEFAULT_HOUR = 4 -# HOW LONG A COLD START WILL WAIT FOR THIS ONE SETTING (#4181). +# HOW LONG A COLD START WILL WAIT FOR THIS ONE SETTING. # # This read is the FIRST database call in the app's `before_serving` hook, and -# a lifespan hook that does not return is a worker that never serves. On -# 2026-09-19 a host storage stall made one Postgres checkpoint of 14 buffers -# take 281 seconds against a 1.3-second baseline; the app restarted into the -# tail of it, this query hung with no deadline, Hypercorn killed the worker at -# its 60-second lifespan timeout, and nothing retries a failed lifespan. A -# five-minute disk hiccup became a three-hour outage that only a human restart -# could clear. +# a lifespan hook that does not return is a worker that never serves. If the +# app starts while the database host is stalled on storage, a query with no +# deadline hangs, Hypercorn kills the worker at its 60-second lifespan timeout, +# and nothing retries a failed lifespan — a few minutes of disk trouble becomes +# an outage that only a human restart clears. # # Three seconds because the honest requirement is "don't hold up the boot", # not "get the right hour". The value is one small indexed row on a local @@ -47,12 +45,11 @@ _STARTUP_READ_TIMEOUT = 3.0 async def get_maintenance_hour() -> int: """The configured run-hour (UTC, 0–23), clamped; default 04:00. - BOUNDED, because the caller is a lifespan hook (rule 156). The fallback is - not new behaviour invented for the timeout — this function already answers - `_DEFAULT_HOUR` for a value it cannot parse, and a database that will not - answer in three seconds is the same class of "no usable value here". What - changes is that the failure is now a logged line and a default hour rather - than the application failing to start. + BOUNDED, because the caller is a lifespan hook. The fallback is the same + one this function gives a value it cannot parse: a database that will not + answer in three seconds is the same class of "no usable value here". The + failure is a logged line and a default hour rather than the application + failing to start. Degrading to the default is the right trade in both directions: the cost of being wrong is that a VACUUM runs at 04:00 instead of the configured hour, @@ -66,7 +63,7 @@ async def get_maintenance_hour() -> int: ) except (TimeoutError, asyncio.TimeoutError): # WARNING, not debug: this is never normal, and it is the breadcrumb - # that would have named #4181 in seconds instead of requiring + # that names a stalled database in seconds instead of requiring # Postgres's own log to be read. logger.warning( "db maintenance: reading db_maintenance_hour exceeded %.1fs; " diff --git a/src/scribe/services/design_starter_roles.py b/src/scribe/services/design_starter_roles.py index ab9c456e..34b768cf 100644 --- a/src/scribe/services/design_starter_roles.py +++ b/src/scribe/services/design_starter_roles.py @@ -3,18 +3,17 @@ WHY THIS EXISTS --------------- A literal gets written into a stylesheet when there is no role to reach for. -That is the mechanism, and this codebase produced a clean demonstration of it: -the house style had no "text on a filled colour" role, so 76 call sites wrote -a pure-white literal — not out of defiance, but because nothing existed to write -instead (#2275). The correction was not a better ban list. It was declaring the -missing role. +A house style with no "text on a filled colour" role gets a pure-white literal +at every call site that needs one — not out of defiance, but because nothing +exists to write instead. The correction is not a better ban list. It is +declaring the missing role. So the useful moment is CREATION. A system whose roles are named on day one never presents the occasion for a literal, and never needs a list of values it forbids. -WHAT SHIPS AND WHAT DOES NOT (rule #115) ----------------------------------------- +WHAT SHIPS AND WHAT DOES NOT +---------------------------- The ROLES ship: `surface-page`, `text-primary`, `action-destructive` are generic CSS-design vocabulary, not one operator's kit. Every install that has a page has a page background. @@ -25,21 +24,21 @@ appears anywhere in this file, and none should ever be added to it — a default palette would be this operator's palette wearing product clothes. A valueless token is already legible downstream: `render_stylesheet` emits it as -a commented-out declaration in its group (#2299), and `stylesheet_for_system` +a commented-out declaration in its group, and `stylesheet_for_system` reports it under `valueless`. So a blank role reads as "to be decided" rather than as breakage, without anything new. THE PREFIX IS THE INSTALL'S --------------------------- -`--fs-` is FabledSword's convention, not the product's. The prefix is a -parameter with a neutral default; a caller that has a house convention passes -it. Baking `--fs-` in would put one family's naming into every install. +A house prefix such as `--fs-` is one family's convention, not the +product's. The prefix is a parameter with a neutral default; a caller that has +a house convention passes it. Baking one in would put one family's naming into +every install. FLAT, NOT PRESET ---------------- -One list, every group individually skippable, all on by default (operator's -call, 2026-08-03). Presets keyed to app shape — web / CLI / docs — were -considered and rejected: they would require the product to hold opinions about +One list, every group individually skippable, all on by default. Presets +keyed to app shape — web / CLI / docs — are not offered: they would require the product to hold opinions about app categories, and a wrong category is worse than a generic list someone prunes once. """ diff --git a/src/scribe/services/reply_shapes.py b/src/scribe/services/reply_shapes.py index f20965a4..03c28824 100644 --- a/src/scribe/services/reply_shapes.py +++ b/src/scribe/services/reply_shapes.py @@ -1,4 +1,4 @@ -"""The default shapes of a reply, and the moment each one rides (milestone 500). +"""The default shapes of a reply, and the moment each one rides. WHY THIS EXISTS @@ -33,7 +33,7 @@ the operator asked for. HOW TO WRITE ONE -Domain-neutral (rule 115): the work may be software, configuration, +Domain-neutral: the work may be software, configuration, infrastructure or anything else a person drives through an agent. "Evidence" is what the reader could open to check; "verified" is whatever checking means for that work. The test beside this module refuses software-only vocabulary. @@ -80,12 +80,11 @@ class ReplyShape: CORE_KEY = "core" # ~550 tokens at four characters a token. The core is paid for in every -# session, so this is the ceiling the test holds it to — not a target. It sits -# at ~2,050 with the judgement line (milestone 500 step 2): close to full, so a -# sentence added now has to replace one. +# session, so this is the ceiling the test holds it to — not a target. The +# core sits close to it, so a sentence added has to replace one. CORE_BUDGET_CHARS = 2200 # A slice is paid for only at its moment, and once per session; it can afford -# more than the core and still has to stay a slice, not the old skill again. +# more than the core and still has to stay a slice, not a whole skill. # The asks slice is the largest (~1,530) because it carries who decides what in # full: handing a question back is what an ask is tempted to do. SLICE_BUDGET_CHARS = 1600 @@ -289,7 +288,7 @@ async def record_delivery(user_id: int | None, forms: dict[str, str], *, via: st logger.debug("reply shape delivery not recorded", exc_info=True) -# ── The operator's view (milestone 500 step 5) ────────────────────────── +# ── The operator's view ───────────────────────────────────────────────── # The window the Settings view counts deliveries over — the same 30 days the # moments panel beside it reads, so the two count the same stretch of work. diff --git a/src/scribe/services/task_claims.py b/src/scribe/services/task_claims.py index 636c542a..752cd1ee 100644 --- a/src/scribe/services/task_claims.py +++ b/src/scribe/services/task_claims.py @@ -1,4 +1,4 @@ -"""A task's claim — which session is working it right now (milestone 381 step 2). +"""A task's claim — which session is working it right now. `status` says where THE WORK stands and is durable: `in_progress` means committed to, not finished. It cannot also say "someone is on this now", @@ -38,7 +38,7 @@ if TYPE_CHECKING: # session gone overnight reads as gone. The cost either way is stated rather # than hidden: readers show the age beside `live`, never the boolean alone. # -# NOT A SETTING, deliberately (#4385 weighed it). Settings are per user, and a +# NOT A SETTING, deliberately. Settings are per user, and a # claim is read by everyone who can see the task: a per-user lease would make # one shared task read as live to one collaborator and dead to another, and # "is anyone on this?" only means something if every reader gets the same @@ -131,9 +131,9 @@ async def bind_session(user_id: int, task_id: int, session_id: str) -> dict | No return claim_state(note, now) -# --- The readers (milestone 381 step 3) --------------------------------------- +# --- The readers -------------------------------------------------------------- # -# A claim nobody reads is the state before this milestone. SessionStart is the +# A claim nobody reads is worth nothing. SessionStart is the # reader that pays rent to the session that set it, and it branches on the # `source` the host sends, because the same claim means different things # depending on what just happened to the context: @@ -290,7 +290,7 @@ async def claims_for_session_start( async def release_session(user_id: int, session_id: str) -> int: - """Release every claim the caller holds under `session_id` (milestone 381 step 4). + """Release every claim the caller holds under `session_id`. Called by the plugin's SessionEnd hook: the session's context is about to stop existing, so the attention its claims record is ending too. A TIDY-UP, diff --git a/src/scribe/services/task_logs.py b/src/scribe/services/task_logs.py index fc96d3e5..cca127f5 100644 --- a/src/scribe/services/task_logs.py +++ b/src/scribe/services/task_logs.py @@ -16,11 +16,10 @@ _UNSET = object() async def _refresh_task_document(session, task_id: int) -> None: - """Re-embed the task whose work log just changed (#4251). + """Re-embed the task whose work log just changed. A task's embedded document carries its logs, so a log written and not - indexed is #4241's half-surface wearing different clothes: the entry is - readable and unfindable, and the next session rebuilds what this one ruled + indexed is half a surface: the entry is readable and unfindable, and the next session rebuilds what this one ruled out. Create, edit and delete all go through here — an edited log that keeps matching its old wording is the stale-vector problem `upsert_note_embedding` already refuses to leave behind for a body. @@ -51,9 +50,9 @@ async def create_log( content: str, duration_minutes: int | None = None, ) -> TaskLog: - # Whoever may WRITE the task may log on it (rule #78) — a collaborator on a - # shared project included. This used to be a bare owner filter, which - # refused exactly the person a shared task exists for. + # Whoever may WRITE the task may log on it — a collaborator on a shared + # project included. An owner-only filter would refuse exactly the person a + # shared task exists for. if not await can_write_note(user_id, task_id): raise ValueError(f"Task {task_id} not found") async with async_session() as session: @@ -61,7 +60,7 @@ async def create_log( task = result.scalars().first() if task is None: raise ValueError(f"Task {task_id} not found") - # Logging IS working the task, so it stamps the claim (milestone 381) — + # Logging IS working the task, so it stamps the claim — # unless the work is over: a retrospective note on a closed task is not # a session picking it up. if task.status not in (TaskStatus.done.value, TaskStatus.cancelled.value): @@ -101,7 +100,7 @@ async def logs_for_task( exactly what a collaborator needs — `list_logs`' `TaskLog.user_id == user_id` returns them an empty list, which reads as "no work has been done" rather than "not yours". The permission question is asked of the - note through `can_read_note` (rule #78), so this is not an unscoped + note through `can_read_note`, so this is not an unscoped read with the check left to the caller. 2. Newest first, because the question a reader asks of a work log is "where does this actually stand", and the answer is the last entry.