diff --git a/src/scribe/models/rule_usage.py b/src/scribe/models/rule_usage.py index 44cb65b9..910dab3c 100644 --- a/src/scribe/models/rule_usage.py +++ b/src/scribe/models/rule_usage.py @@ -7,16 +7,15 @@ from scribe.models.base import CreatedAtMixin, iso SURFACED = "surfaced" PULLED = "pulled" -# The outcome half (#4212, milestone 419). A rule that was read and then -# ignored has always been indistinguishable from one that was read and -# obeyed; these are the two events that can tell them apart. +# The outcome half. Without these, a rule that was read and then ignored is +# indistinguishable from one that was read and obeyed; these are the two +# events that can tell them apart. # # There is deliberately NO third value for "read and ignored". That state is -# real and is the whole point of the milestone, but it cannot be reported: -# an agent that knew it was ignoring a rule would not be ignoring it. It is +# real and is the point of measuring outcomes, but it cannot be reported: an +# agent that knew it was ignoring a rule would not be ignoring it. It is # DERIVED — a pull with no outcome — and an enum member for it would collect -# nothing while reading as though it had measured something, which is #3311's -# failure exactly. +# nothing while reading as though it had measured something. APPLIED = "applied" DEPARTED = "departed" OUTCOMES = (APPLIED, DEPARTED) @@ -26,15 +25,14 @@ class RuleUsageEvent(Base, CreatedAtMixin): """One row per time a rule was SURFACED to the agent, PULLED in full, or ACTED ON — applied, or departed from with a stated reason. - The sibling `note_usage_events` has had since 2026-07, third in the line - after `rule_embeddings` and `rule_versions` — and, like those, it exists - because the rule side kept inheriting machinery built for notes and - quietly getting the weaker version of it. + The rule-side sibling of `note_usage_events`, beside `rule_embeddings` + and `rule_versions` — each exists because machinery built for notes gives + rules the weaker version of it. WHY RULES NEED THEIR OWN AND CANNOT SHARE THE NOTE TABLE. Not squeamishness about a polymorphic column — the row shares no note-specific fields and the aggregate readout is the same shape, which is the strongest case for - sharing that note #3163 admits. What decides it is IDENTITY AT RESTORE. A + sharing there is. What decides it is IDENTITY AT RESTORE. A note id and a rule id are different namespaces resolved through different maps, and `note_usage_events`'s importer maps `note_id` through `note_id_map` and drops what does not resolve. A rule id parked in that @@ -42,11 +40,10 @@ class RuleUsageEvent(Base, CreatedAtMixin): happened to take that number — telemetry that is not merely lost but wrong, and wrong in a way nothing downstream could detect. - WHAT THIS MEASURES, AND WHY IT DID NOT EXIST. The write-path standing-rule - arm is the only retrieval surface in Scribe whose usefulness cannot be - observed — and, not coincidentally, the only one that has never declined to - fire (#3311: 296 calls, zero zero-result, 100% clearing its threshold). - `retrieval_logs` gives it scores; scores say what the ranker thought, never + WHAT THIS MEASURES. A standing-rule arm's usefulness cannot be observed + from its scores: an arm can fire on every call, clearing its threshold + every time, and say nothing about whether it helped. `retrieval_logs` + gives it scores; scores say what the ranker thought, never whether the hint landed. Without a pull counter no install can tune the arm from evidence, only from the shape of a histogram. @@ -58,7 +55,7 @@ class RuleUsageEvent(Base, CreatedAtMixin): forty times and opened never, because that evidence is precisely the case for having deleted it. - Cells left deliberately empty (note #3163's step 3): no share ACL — rules + Cells left deliberately empty: no share ACL — rules have none of their own; no soft delete — nothing restores a telemetry row, and the table is append-only; no embedding — an event is not a document. """ @@ -76,7 +73,7 @@ class RuleUsageEvent(Base, CreatedAtMixin): # 'surfaced' | 'pulled' | 'applied' | 'departed' # # Plain Text with no CHECK, as created in 0094 — which is why 0106 added - # the outcome pair without a DROP/ADD migration. Rule 36 governs + # the outcome pair without a DROP/ADD migration — that step is for # CHECK-whitelisted columns and this is not one. Said here as well as in # the migration because this is where the next person adding a value will # look first. @@ -84,9 +81,9 @@ class RuleUsageEvent(Base, CreatedAtMixin): # Which surface produced it. A CONVENTION, not a fixed vocabulary, and the # note twin's comment explains why this one deliberately does not enumerate - # its members: the previous such list went stale, naming a source nothing - # wrote while omitting ones that existed, and a half-true enumeration reads - # as authoritative in exactly the way that misleads (#2476). + # its members: such a list goes stale, naming a source nothing writes while + # omitting ones that exist, and a half-true enumeration reads as + # authoritative in exactly the way that misleads. # `grep -rn record_rule_pulled\|record_rule_surfaced src/` is the # authoritative list, and unlike a comment it cannot drift. # diff --git a/src/scribe/routes/notes.py b/src/scribe/routes/notes.py index 92ed94b3..9405c98f 100644 --- a/src/scribe/routes/notes.py +++ b/src/scribe/routes/notes.py @@ -131,10 +131,8 @@ async def create_note_route(): # 403, not 400: the request is well-formed and the caller simply # may not write the target. The note itself was created. return jsonify({"error": str(exc), "note": note.to_dict()}), 403 - # #4249: this door could not file a note to a System at all, while the - # MCP door could — the reverse of the asymmetry the same issue records for - # lessons. The pattern is not that one door is richer; it is that the door - # nobody exercised for a kind is the one that never grew the parameter. + # Both doors file a note to Systems: the door nobody exercises for a kind + # is the one that never grows the parameter. if data.get("system_ids") is not None: await systems_svc.set_record_systems(uid, note.id, data["system_ids"]) out = note.to_dict() @@ -235,7 +233,7 @@ async def get_note_route(note_id: int): # join away, but which surface asked is not recoverable after the fact. # Keeping rest_* apart from mcp_* is load-bearing, not tidiness — "was that # injected line useful?" is answered by agent pulls alone, and a human - # clicking a link would inflate exactly the number #1038 and #2085 gate on. + # clicking a link would inflate exactly the number pruning gates on. record_pulled(user_id=uid, note_id=note_id, source="rest_note") await supersession_svc.attach_relations(uid, note_id, data) return jsonify(data) @@ -246,8 +244,8 @@ async def get_note_route(note_id: int): async def update_note_route(note_id: int): """Partial update — only the keys present in the payload change. PUT and PATCH are the same handler on purpose: the form sends the field set it - edited, and the two verbs used to be two near-identical copies of this - function that drifted (one carried the supersedes contract, one did not).""" + edited, and two copies of this function would drift (one carrying the + supersedes contract, one not).""" uid = get_current_user_id() # Share-aware: resolve through the ACL and write as the OWNER, so a shared # editor's save isn't rejected by the owner-scoped update service. @@ -265,7 +263,7 @@ async def update_note_route(note_id: int): "milestone_id", "status", "priority", "note_type", # A cleared form input arrives as "" and the service reads that as # NULL (NULLABLE_NOTE_TEXT), so this door expresses "remove the check" - # with its own idiom and needs no `clear` list (milestone 317). + # with its own idiom and needs no `clear` list. "verify_with", "expires_when", ): if key in data: @@ -298,12 +296,10 @@ async def update_note_route(note_id: int): return jsonify({"error": str(exc)}), 403 # Set-semantics like the above: present-and-empty clears, absent leaves # alone. Scoped by the CALLER rather than owner_uid, for the same reason - # the supersedes call is (#47) — `set_record_systems` links only Systems - # the acting user can read, and an editor-share holder should tag from - # what they can see rather than inherit the owner's reach. This matches - # routes/tasks.py; routes/lessons.py and routes/snippets.py pass the owner - # for the same operation, which is a real inconsistency, recorded in #4249 - # rather than changed here. + # the supersedes call is — `set_record_systems` links only Systems the + # acting user can read, and an editor-share holder should tag from what + # they can see rather than inherit the owner's reach. Every door that + # tags a record (tasks, lessons, snippets) does the same. if "system_ids" in data: await systems_svc.set_record_systems(uid, note_id, data["system_ids"] or []) out = note.to_dict() @@ -521,7 +517,7 @@ async def graph_route(): return jsonify(graph) -# ── The staleness sweep (milestone 317) ────────────────────────────────────── +# ── The staleness sweep ────────────────────────────────────────────────────── # The web half of notes_due_for_verification / mark_note_verified. Same # contract as the MCP door and the rules routes beside it — the service holds # the behaviour, these two just parse and serialise. diff --git a/src/scribe/routes/retrieval.py b/src/scribe/routes/retrieval.py index 4cc5b176..8d9b902c 100644 --- a/src/scribe/routes/retrieval.py +++ b/src/scribe/routes/retrieval.py @@ -1,12 +1,12 @@ -"""The operator's view of retrieval tuning — what the dials are, and who moved them (#4102). +"""The operator's view of retrieval tuning — what the dials are, and who moved them. WHY THIS EXISTS SEPARATELY FROM /api/settings The numbers themselves are ordinary settings rows and could be written through the generic KV endpoint. They must not be, and that is the whole point of this blueprint: a floor changed through `/api/settings` moves silently, leaving the -history saying nothing happened. From milestone 416 those dials are moved by -the model on the operator's behalf, so a trail with holes in it is worse than +history saying nothing happened. Those dials are moved by the model on the +operator's behalf, so a trail with holes in it is worse than no trail — it reads as complete. So every change to a retrieval dial goes through `set_dial`, from the UI as @@ -39,7 +39,7 @@ retrieval_bp = Blueprint("retrieval", __name__, url_prefix="/api/retrieval") # behalf. The operator IS that someone: they are the audience the trail is # written for, they cannot be uninformed about their own decision, and a # mandatory justification textarea on every control would be friction charged -# to the one participant who owes no explanation (rule 24). +# to the one participant who owes no explanation. # # The event is still written, because "the operator set this by hand" is the # single most useful thing the history can tell a later session — it is the one @@ -116,12 +116,12 @@ async def tuning_history_route(): @retrieval_bp.route("/moments", methods=["GET"]) @login_required async def moments_route(): - """The moments of work that rules mount on (milestone 458), with the + """The moments of work that rules mount on, with the actions that reach each one on this install. The same payload `list_moments` returns, from the same services, so the session and the Settings view cannot name different moments — plus what - the view shows beside each one (step 6): `mounted`, how many rules each + the view shows beside each one: `mounted`, how many rules each carries, and `usage`, the deliveries and opens per moment over `days` (the block `retrieval_telemetry` reports as `moment_usage`). """ @@ -147,11 +147,11 @@ async def moments_route(): @retrieval_bp.route("/reply-shapes", methods=["GET"]) @login_required async def reply_shapes_route(): - """The default reply shapes and the moment each rides (milestone 500). + """The default reply shapes and the moment each rides. `list_reply_shapes`' payload from the same service, so the Settings view and the session cannot show different defaults — plus what only the - operator's view needs (step 5): per shape, the preferences mounted on its + operator's view needs: per shape, the preferences mounted on its moment and its deliveries over `days` (default 30, 1–90). """ try: @@ -201,7 +201,7 @@ async def unmap_action_route(): @retrieval_bp.route("/moments/proposals", methods=["GET"]) @login_required async def moment_proposals_route(): - """The moment proposals waiting on a judgment (milestone 458 step 7) — + """The moment proposals waiting on a judgment — `rule_moment_proposals`' payload, from the same service. `?rule_id=` narrows to one rule, for the rule editor.""" try: diff --git a/src/scribe/routes/snippets.py b/src/scribe/routes/snippets.py index e48f90b2..f21b4fbc 100644 --- a/src/scribe/routes/snippets.py +++ b/src/scribe/routes/snippets.py @@ -5,7 +5,7 @@ routes feed the web management UI; the MCP tools (mcp/tools/snippets.py) are the agent-facing surface. Both go through services/snippets.py, so the serialize/parse contract and embedding-on-create live in one place (DRY). -ACL (rule #78): reads/writes of a single snippet resolve through the share-aware +ACL: reads/writes of a single snippet resolve through the share-aware `get_note_for_user` + `can_write_note`, and writes are performed as the OWNER so a shared editor isn't rejected by the owner-scoped service — mirroring routes/notes.py. The list is owner-scoped, matching the note-browse surface. @@ -65,7 +65,7 @@ async def list_snippets_route(): repo = request.args.get("repo", "") path = request.args.get("path", "") symbol = request.args.get("symbol", "") - # Drift check (#2086). "attention" is what the UI's filter chip sends. + # Drift check. "attention" is what the UI's filter chip sends. verification = request.args.get("verification", "") limit, offset = parse_pagination() items, total = await snippets_svc.list_snippets( @@ -115,7 +115,7 @@ async def create_snippet_route(): is_task=False, note_type=snippets_svc.SNIPPET_NOTE_TYPE, # Matched on the artefact — location and code — before prose, the - # same way the MCP create path does (#2518). Both surfaces must + # same way the MCP create path does. Both surfaces must # apply the identical gate or the web UI becomes the way to record # a duplicate the agent would have been stopped from writing. code=data.get("code", ""), @@ -163,7 +163,7 @@ async def get_snippet_route(snippet_id: int): return not_found("Snippet") note, permission = loaded data = snippets_svc.snippet_to_dict(note) - # Forge-checked freshness (#2690) — same decoration the MCP pull gets. + # Forge-checked freshness — same decoration the MCP pull gets. await snippets_svc.attach_live_body(note, data) data["permission"] = permission # Read the association as the OWNER: a shared reader isn't scoped to the @@ -177,7 +177,7 @@ async def get_snippet_route(snippet_id: int): await attach_usage([data]) # Opening the detail view IS a pull — the operator chose to look. Tagged # apart from the MCP sources so "the agent reused it" and "a human read it" - # stay distinguishable; they mean different things for pruning (#2085). + # stay distinguishable; they mean different things for pruning. record_pulled(user_id=uid, note_id=snippet_id, source="rest_snippet") return jsonify(data) @@ -212,7 +212,7 @@ async def update_snippet_route(snippet_id: int): if updated is None: return not_found("Snippet") if data.get("system_ids") is not None: - # The CALLER, not owner_uid — see routes/lessons.py for the why (#4249). + # The CALLER, not owner_uid — see routes/lessons.py for the why. await systems_svc.set_record_systems(uid, snippet_id, data["system_ids"]) out = snippets_svc.snippet_to_dict(updated) out["systems"] = [