From 33142db3edcff378a69ee4771d79d24f00f22747 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 21 Oct 2026 19:52:55 -0400 Subject: [PATCH] docs(6205): plugin_context comments state the reason, not the instance that taught it Record ids, milestone numbers, dated telemetry readings and rule numbers leave the retrieval-arm comments and docstrings. Each bar keeps its present-tense reason and the probe-score calibrations, which describe the model on any install. Co-Authored-By: Claude Opus 5.5 --- src/scribe/services/plugin_context.py | 747 ++++++++++---------------- 1 file changed, 287 insertions(+), 460 deletions(-) diff --git a/src/scribe/services/plugin_context.py b/src/scribe/services/plugin_context.py index 8da91879..1dfdab91 100644 --- a/src/scribe/services/plugin_context.py +++ b/src/scribe/services/plugin_context.py @@ -62,8 +62,8 @@ logger = logging.getLogger(__name__) _MAX_CHARS = 9000 # The notes renderer — a record's name, kind, System and matched passage — -# lives in `retrieval_pipeline` with the rest of the notes stages (milestone -# 456 step 4) and is re-exported above. +# lives in `retrieval_pipeline` with the rest of the notes stages and is +# re-exported above. # How much of a named record's body sits under its line. The opening, not an @@ -97,7 +97,7 @@ async def _named_records(user_id: int, ids: list[int]) -> list: _PROC_PREVIEW_CHARS = 200 # Max chars of the project goal on the session-start Goal line. The full goal is -# one enter_project away; a cut says so rather than ending mid-word (#4036). +# one enter_project away; a cut says so rather than ending mid-word. _GOAL_CHARS = 200 # --- Knowledge auto-inject (Path A: per-turn awareness push) ----------------- @@ -107,16 +107,15 @@ _GOAL_CHARS = 200 # a higher bar than a search the agent chose to run. # # The defaults below are STARTING POINTS, and correcting them is the model's -# job, not the operator's (#4102): `retrieval_surfaces` says what is in force, +# job, not the operator's: `retrieval_surfaces` says what is in force, # `retrieval_telemetry(near_miss_samples=N)` says what it refused, and # `tune_retrieval` moves it with the reason attached. The operator can set any # of them in Settings and their change is recorded the same way — but nobody # has to read a log to get correct behaviour out of this. AUTOINJECT_ENABLED_KEY = "kb_autoinject_enabled" -# The key and the value both live in the registry now (#4102); these names -# survive because the comments above them are where each number's measurement -# is recorded, and because the Settings UI agreement test pairs them with the -# Vue refs by name. One definition, two readable places. +# The key and the value both live in the registry; these names exist because +# the comments beside them record why each number is what it is, and because +# the Settings UI agreement test pairs them with the Vue refs by name. AUTOINJECT_THRESHOLD_KEY = SURFACES["auto_inject"].floor_key AUTOINJECT_TOP_K_KEY = SURFACES["auto_inject"].budget_key @@ -124,56 +123,43 @@ AUTOINJECT_DEFAULT_ENABLED = True AUTOINJECT_DEFAULT_THRESHOLD = SURFACES["auto_inject"].floor_default AUTOINJECT_DEFAULT_TOP_K = SURFACES["auto_inject"].budget_default -# The write-path trigger (#2082) gets its own on/off switch, its own threshold, -# and shares only top-k. It originally shared the threshold too, on the argument -# that one "how loud may Scribe be" knob beats two that drift — and reserved the -# split for when telemetry showed the two surfaces wanted different values. -# -# #2223 is that evidence. Measured against the live instance, the semantic arm's -# scores for CODE sit far above what the same threshold means for PROSE: +# The write-path trigger gets its own on/off switch, its own threshold, and +# shares only top-k. Its threshold differs because the semantic arm's scores +# for CODE sit far above what the same threshold means for PROSE (probe +# payloads against a recorded helper): # near-duplicate of a recorded helper 0.73-0.74 (true positive) # unrelated colour math / Vue SFC / CSS 0.55-0.63 (false positive) # `x = 1` 0.58 (false positive) # Any two Python-shaped payloads share keywords, indentation and structure, so # the floor for "some code" is ~0.55-0.63 — auto-inject's 0.55 lands INSIDE that -# noise band, and 6 of 8 probe payloads produced a nudge (4 of them noise). The -# margin gate can't rescue it either: the notes band (retrieval_pipeline._NOTE_BAND) is relative to the top +# noise band. The margin gate can't rescue it either: the notes band (retrieval_pipeline._NOTE_BAND) is relative to the top # hit, so with a single hit it never engages. # # 0.68 clears every measured false positive with margin and still sits 0.05 -# below both true positives. Auto-inject keeps 0.55 — it was tuned on prose and -# is not implicated. Tune from retrieval_logs (source='write_path') + note_usage -# pull-through (#2085) once a real corpus accrues; a cross-encoder rerank -# (#1038) would subsume this bump. +# below both true positives. Auto-inject keeps 0.55, which is tuned on prose. +# Tune from retrieval_logs (source='write_path') and note_usage pull-through; a +# cross-encoder rerank would subsume this bump. WRITEPATH_ENABLED_KEY = "kb_writepath_enabled" WRITEPATH_THRESHOLD_KEY = SURFACES["write_path"].floor_key WRITEPATH_DEFAULT_ENABLED = True WRITEPATH_DEFAULT_THRESHOLD = SURFACES["write_path"].floor_default -# The standing-rule arm (milestone 307) gets its own bar — the split #2223 made -# one surface down, now made for the THIRD corpus. It inherited 0.68 above, and -# that number was measured against code-vs-note-PROSE. It was never re-derived -# for code-vs-RULE-TEXT. +# The standing-rule arm gets its own bar, for a third corpus: 0.68 above is +# calibrated on code against note PROSE, not code against RULE TEXT. # -# THE STRUCTURAL ARGUMENT, which is the only kind admissible here (rule 115). -# Two facts hold on any install, including one with six rules and no telemetry: +# THE STRUCTURAL ARGUMENT, which holds on any install, including one with six +# rules and no telemetry: # # 1. The eligible corpus is SMALL — every rule an install owns, still only # a few dozen documents against thousands of notes. A top-k over a small # pool always returns something, so "the best match cleared the bar" # drifts from "a good match exists" toward "N things were ranked". A bar # calibrated for best-of-thousands is cleared by best-of-forty as -# arithmetic rather than relevance. -# This argument WEAKENED when the arms stopped filtering to one tier -# (see the note on that below): a larger pool makes clearing the bar -# mean more, not less. The threshold was deliberately left where it was -# anyway — moving two variables at once would make the resulting -# distribution unreadable, and this one errs toward silence on purpose. +# arithmetic rather than relevance. (The arms search every tier, which +# makes the pool larger; the bar still errs toward silence on purpose.) # 2. Rules are short imperative technical English — a far more HOMOGENEOUS -# corpus than note prose. #2223 measured the floor for code against prose -# at 0.55-0.63 and set 0.68 above it. A more homogeneous corpus has a -# HIGHER floor, so 0.68 is not merely inherited, it is below where this -# corpus's noise sits. +# corpus than note prose, and a more homogeneous corpus has a HIGHER +# noise floor than prose's 0.55-0.63. # # WHY 0.72 AND NOT A NUMBER OFF A HISTOGRAM. The exact offset between prose's # floor and rule-text's is not derivable in general — it depends on how an @@ -182,42 +168,29 @@ WRITEPATH_DEFAULT_THRESHOLD = SURFACES["write_path"].floor_default # hint fires on EVERY write. A missed rule is recoverable, because the rule is # still in Scribe and the agent can search it. A hint that cries wolf is not: # it teaches the reader to skip the whole block, and the surface is lost along -# with the true positives it would have carried. The arm's own comment already -# says "noise on a hint that fires on every write is how a hint gets ignored". +# with the true positives it would have carried. # -# TUNE IT FROM YOUR OWN INSTANCE, which is now possible: `retrieval_telemetry` -# reports `rule_usage.pull_through` (milestone 333 step 3). Raise this if rules -# arrive unread; lower it if rules you needed never arrived. What would RETIRE -# it: a cross-encoder rerank (#1038), which would make a similarity bar the -# wrong control entirely. -# SCOPED TO THE WRITE-PATH ARM SINCE #3853. The command arm has its own bar -# below, and the measurement that separated them is recorded there. Everything -# above still holds for THIS arm: a code payload is long and rich, which is the -# case 0.72 was calibrated on, and the telemetry says it is working — the -# write-path rule arm speaks on 37% of its calls and its refused mass sits at -# p50 0.6989, comfortably under the bar rather than piled against it. +# TUNE IT FROM YOUR OWN INSTANCE: `retrieval_telemetry` reports +# `rule_usage.pull_through`. Raise this if rules arrive unread; lower it if +# rules you needed never arrived. What would RETIRE it: a cross-encoder rerank, +# which would make a similarity bar the wrong control entirely. +# +# This bar is the WRITE-PATH arm's. A code payload is long and rich, which is +# the case 0.72 suits; the command arm has its own bar below. RULEHINT_THRESHOLD_KEY = SURFACES["write_path_rule"].floor_key RULEHINT_DEFAULT_THRESHOLD = SURFACES["write_path_rule"].floor_default -# THE COMMAND ARM'S OWN BAR, AND WHY IT IS NOT THE WRITE PATH'S (#3853). +# THE COMMAND ARM'S OWN BAR, AND WHY IT IS NOT THE WRITE PATH'S. # -# One bar served both act arms until this. They are not the same problem: a -# write-path query is a code payload, long and rich, while a pre-tool query is -# a shell command — often under a dozen words. Less text, less signal, lower -# scores for the same relevance. At a shared 0.72 the two arms measured like -# different subsystems: +# A write-path query is a code payload, long and rich, while a pre-tool query +# is a shell command — often under a dozen words. Less text, less signal, lower +# scores for the same relevance. At the write path's 0.72 the command arm +# turns nearly everything away, with its refused mass piled just under the +# line, which is the shape a bar set too high leaves. # -# write_path_rule 2,325 calls, speaks on 37%, near-miss p50 0.6989 -# pre_tool_rule 11,768 calls, speaks on 2%, near-miss p50 0.6794 -# -# The second is not a quiet surface, it is a mute one: 11,530 of 11,768 calls -# said nothing, with near-miss p90 at 0.7097 — refused mass piled one -# hundredth under the line, which is the shape a bar set too high leaves. The -# note arms are the control and look nothing like it (auto_inject refuses at -# p90 0.5463, write_path at 0.6738, both far below their bars). -# -# WHAT 0.68 IS MEASURED AGAINST. Eight replayed queries, consequential acts -# against innocuous ones, scored on the post-#3855 corpus: +# WHAT 0.68 IS CALIBRATED AGAINST. Replayed queries, consequential acts +# against innocuous ones, scored against a rule corpus with symptom-bearing +# triggers: # # 0.7571 git push origin dev consequential # 0.7245 cd ...; git fetch; git add -A consequential @@ -229,77 +202,49 @@ RULEHINT_DEFAULT_THRESHOLD = SURFACES["write_path_rule"].floor_default # 0.6099 sed -n '120,160p' package.json innocuous # 0.6056 ls -la && cat README.md innocuous # -# At 0.72 three of the four consequential acts retrieved NOTHING, including -# `git pull --rebase origin dev`, where rules 153, 1 and 2 all ranked -# correctly and all sat between 0.7126 and 0.7193. +# At 0.72 three of the four consequential acts would retrieve nothing, though +# the right rules ranked first for each. # -# THE SEPARATION IS 0.0115 WIDE, and that is a caveat, not a result. Eight -# probes set a direction; they do not settle a number. `near_miss_samples` on -# a few days of post-#3855 traffic is what settles it, and this is the bar to -# re-read first. +# THE SEPARATION IS NARROW (about 0.01), and that is a caveat, not a result. +# Eight probes set a direction; they do not settle a number. +# `near_miss_samples` on real traffic is what settles it, and this is the bar +# to re-read first. # -# This also CORRECTS an assumption stated above. That comment argued 0.68 was -# "below where this corpus's noise sits", inferring a higher floor from the -# corpus being homogeneous. Measured, the command arm's noise ceiling is -# 0.6735 — so 0.68 clears it, barely, rather than sitting under it. The -# inference was reasonable and the measurement disagrees. -# -# WHY LOWERING IS SAFER NOW THAN IT WOULD HAVE BEEN. Until #3851 this arm had -# a single slot, so its one line had to be right and a high bar was the only -# control. The band now does noise control downstream: a marginal hit that +# WHY A LOWER BAR IS SAFE HERE. The band does noise control downstream: a marginal hit that # clears the bar still has to score within `_RULEHINT_BAND` of the top to be # rendered. The bar's job shrank, so the bar can. # -# The noise floor above is set by CROSS-PROJECT BLEED rather than bad ranking -# — 0.6735 is another project's shell-command rule matching a shell command in -# this one, which is a correct match to a rule that should never have been -# eligible. Retrieval is ownership-scoped, not project-scoped. Scoping it -# would drop that ceiling and widen the 0.0115, which is the larger fix and -# the reason to settle project scoping before tuning this number twice. +# The innocuous scores near the line are largely CROSS-PROJECT BLEED rather +# than bad ranking: another project's shell-command rule matching a shell +# command in this one. Project scoping of rules lowers that ceiling. TOOLRULE_THRESHOLD_KEY = SURFACES["pre_tool_rule"].floor_key TOOLRULE_DEFAULT_THRESHOLD = SURFACES["pre_tool_rule"].floor_default -# A SET OF RULES PER ACT, NOT THE SINGLE BEST ONE (#3851). +# A SET OF RULES PER ACT, NOT THE SINGLE BEST ONE. # -# This was 1, and the reasoning for that is kept below rather than deleted -# because it was correct for the world it was written in and the half of it -# that still holds is what shapes the replacement. +# Nothing is preloaded, so this arm is the whole delivery, and an act is often +# governed by several rules at once — a push can be bound by where to push, +# what never to push to, polling CI and letting one action land before the +# next, and each alone permits the mistake the others catch. "The single best" +# is not a coherent answer when four rules bind. # -# THE OLD ARGUMENT. "With a corpus this small, top-k does as much damage as -# the threshold: k=2 over a few dozen candidates means the second line is -# almost always the second-best noise, arriving with the same confident -# framing as the first." True — and note the premise. Retrieval was then a -# SUPPLEMENT to a 33-rule resident set, so the arm's job was to add one -# salient rule beside everything the session already held. One was the right -# number for an accent. -# -# WHAT CHANGED. Milestone 394 removes residency, and then this arm is not the -# accent, it is the whole delivery. Moments genuinely need several rules at -# once: `git push origin dev` is governed by 1 (`dev` is home), 2 (never -# `main` unasked), 9 (poll CI) and 140 (let each action land) simultaneously, -# and each alone permits the mistake the others catch. One slot cannot serve -# that, and "the single best" is not a coherent answer when four rules bind. -# -# WHY A CAP PLUS A BAND, RATHER THAN A BIGGER CAP. The old argument's real -# content is that a fixed k invents lines — it fills slots whether or not -# anything deserves them. A band does not: it keeps what is close to the top -# and nothing else, so a moment with one clearly-relevant rule still shows +# WHY A CAP PLUS A BAND, RATHER THAN A BIGGER CAP. A fixed k invents lines — it +# fills slots whether or not anything deserves them, and over a small corpus +# the second slot is usually the second-best noise. A band does not: it keeps +# what is close to the top and nothing else, so a moment with one clearly-relevant rule still shows # one, and a moment with four shows four. The corpus decides, not a constant. -# The cap survives as a ceiling on the worst case, not as the usual answer. -# NOW A DEFAULT RATHER THAN A CAP (#4102): both rule act-arms read their own -# budget from the registry, so this is the value an install starts at and not -# the value it is stuck with. The reasoning below is why 5 is where it starts. +# The cap is a ceiling on the worst case, not the usual answer. It is a +# DEFAULT: both rule act-arms read their own budget from the registry, so this +# is the value an install starts at, not the value it is stuck with. RULEHINT_LIMIT = SURFACES["write_path_rule"].budget_default -# ── The pre-act checkpoint (#4214, milestone 419) ─────────────────────── +# ── The pre-act checkpoint ─────────────────────────────────────────────── # # WHAT A CHECKPOINT IS, AND WHY IT IS NOT A LOUDER HINT. Every arm above # appends text to an act the agent has already composed. Claude Code delivers # `additionalContext` alongside the tool result, so by the time the line is # read the call is written and the rule reads as commentary on a decision -# already made. That is milestone 419's central finding, measured over a -# session where three misses were caught by the operator and none by this -# system. +# already made. # # A checkpoint is the same retrieval, spent differently: the hook returns a # `deny` and the act does not run, so the rule's own text is read BEFORE the @@ -308,34 +253,24 @@ RULEHINT_LIMIT = SURFACES["write_path_rule"].budget_default # session has not recorded an outcome for it". An outcome can be satisfied # with one cheap call that asserts compliance without producing any, and a # checkpoint that can be dismissed that way manufactures exactly the -# compliance data step 2 exists to measure. Reading a rule cannot be faked in +# compliance data the outcome telemetry exists to measure. Reading a rule cannot be faked in # that direction: after `get_rule` the statement is in context, which is the # whole of what was wanted. # # WHAT "CONSEQUENTIAL" MEANS HERE, AND WHY IT IS NOT A LIST. The obvious # implementation enumerates act kinds — a write to product code, a schema -# change, a bulk classification, a merge. Every one of those is consequential -# because THIS operator wrote rules about it, and a shipped list would be this -# instance's corpus hard-coded into the product (rule 115). So the corpus -# decides: an act is consequential when the install's own rules speak to it +# change, a bulk classification, a merge. Those are consequential only where +# an operator wrote rules about them, and a shipped list would hard-code one +# install's corpus into the product. So the corpus decides: an act is consequential when the install's own rules speak to it # with high confidence. A fresh install with no rules never stops anything, # and an install whose rules are about something else entirely stops on that # instead. _CHECKPOINT_THRESHOLD_KEY = "kb_checkpoint_threshold" -# MEASURED, not chosen (2026-09-21, `retrieval_telemetry(days=30)` on the -# instance this was built on — recorded as provenance for the number, not as -# a defence of it under rule 115): -# -# write_path_rule floor 0.72 p10 0.6984 p50 0.7319 p90 0.7628 max 0.8817 -# pre_tool_rule floor 0.68 p10 0.6838 p50 0.7018 p90 0.7373 max 0.8293 -# -# 0.80 sits above p90 on BOTH arms and below max on both, so it selects from -# the top decile of an already-selective arm rather than from its bulk — and -# it is reachable, which a bar above 0.8293 would not be for the busier arm. -# Over that window the two arms returned ~3,584 non-empty calls between them, -# so an upper bound of one tenth of those is ~12 a day across a very heavy -# install, and the true rate is lower because 0.80 is not p90 but above it. +# 0.80 is meant to sit above the p90 of what both rule act-arms return and +# below their max: it selects from the top decile of an already-selective arm +# rather than from its bulk, and it stays reachable. On a heavily used install +# that is on the order of ten stops a day across all sessions. # # A SETTING, because the number above is a distance in one embedding model's # geometry over one corpus and cannot transfer (retrieval_surfaces' opening @@ -351,106 +286,78 @@ _CHECKPOINT_DEFAULT = 0.80 CHECKPOINT_SESSION_CAP = 5 -# AND A REPEAT COMPETES ON RANK ALONE (#3750), WHICH SURVIVES THE BAND. +# AND A REPEAT COMPETES ON RANK ALONE, WHICH SURVIVES THE BAND. # -# Since #3750 a hit already on the session's exclusion ledger is RENDERED -# rather than dropped, which raises a question the old behaviour never had to -# answer: when the top-ranked hit is one the session has already seen, does it -# take its place, or step aside for a fresh rule behind it? -# -# It keeps its place, and nothing is promoted past it. #3851 widened the arm -# from one slot to a banded set and did NOT reopen this: a repeat still ranks -# where it ranks, and the band is applied to scores with no regard for what -# the session has seen. The two are independent, exactly as `kind` and `seen` +# A hit already on the session's exclusion ledger is RENDERED rather than +# dropped. When the top-ranked hit is one the session has already seen, it +# keeps its place, and nothing is promoted past it: the band is applied to +# scores with no regard for what the session has seen. The two are independent, exactly as `kind` and `seen` # are independent in the renderer — rank answers "what is relevant now" and # the ledger answers "have you been told", and neither is evidence about the -# other. Two reasons, both unchanged by the widening. +# other. Two reasons. # # RANK IS THE ANSWER TO "WHAT IS RELEVANT NOW". If the repeat scores 0.85 and # the best fresh candidate 0.73, the repeat is the better match for the action # actually being taken. Overfetching in order to promote the fresh one past it -# would reinstate exactly the withholding this milestone exists to remove, one -# rank deeper and harder to see — recency is not a reason to show a worse +# would be withholding, one rank deeper and harder to see — recency is not a reason to show a worse # match, and "you have seen this" is not the same claim as "you are holding # this". # -# AND THE COST OF A SECOND LINE IS NOW PAID DIFFERENTLY, NOT WISHED AWAY. -# This paragraph used to read "a second line is the one thing the limit above -# forbids", on the strength of "a reference costs the same ~40 tokens as a -# first surfacing". Both halves are now wrong and the second was already -# wrong when written: a full line is ~143 tokens once the trigger is rendered, -# and #3855's rewrite of the corpus roughly tripled trigger length, so five -# full lines on a push probe measure ~646 tokens before EVERY Bash call. -# -# That number is why the widening pairs with a compact rendering rather than -# arriving alone (see `_rule_hint_line`). The old paragraph's instinct — that -# a fourth voice which speaks at full volume twice is where a reader stops -# reading — is the half worth keeping, and it is answered by making the -# later lines quieter rather than by refusing to have them. Top hit full, -# the rest as references: ~299 tokens against ~568 for five full lines, so -# roughly 2x the old single line for four more rules. +# AND THE COST OF A SECOND LINE IS PAID BY RENDERING, NOT WISHED AWAY. +# A full line runs to a hundred-odd tokens once the trigger is rendered, so +# five full lines before EVERY Bash call is several hundred tokens. So the +# set pairs with a compact rendering (see `_rule_hint_line`): top hit full, +# the rest as references, about half the cost of five full lines. A voice +# that speaks at full volume every time is where a reader stops reading; +# the answer is quieter later lines, not fewer rules. # # The reference keeps its TAIL and loses only its trigger, which is both the -# cheaper and the safer cut — see `_rule_hint_line`, where the first attempt -# dropped the tail as well and #3750's guard caught it within one commit. +# cheaper and the safer cut — see `_rule_hint_line`. # # The consequence is deliberate and worth naming: a rule that keeps ranking # first for a recurring situation keeps being referenced, every time the # situation recurs. That is the intended behaviour — the situation recurring -# IS the trigger — and its decay belongs to exclusion ageing (#3751), not to +# IS the trigger — and its decay belongs to exclusion ageing, not to # a rule that ranks first being quietly demoted for having won before. -# WHY THE ARMS NO LONGER FILTER TO ONE TIER (#3702). +# WHY THE ARMS SEARCH EVERY TIER. # -# Both arms used to pass `tier="conditional"`, on the reasoning that an -# always-on rule is already in the session, so surfacing it again is pure -# noise. That reasoning conflates two different things: +# Filtering out always-on rules, on the reasoning that they are already in +# the session, conflates two different things: # # PRESENT IN CONTEXT — the rule was delivered at session start. # SALIENT AT THE MOMENT — the rule is in front of the reader when the # action it governs is about to be taken. # # A rule handed over in a list at turn zero is present while a session writes -# a config value three hundred turns later. It is not surfaced. So the filter -# did not merely skip a redundant hint — it made a whole class of rules -# permanently ineligible for the only mechanism that puts a rule in front of -# an agent AT the moment, and the more important a rule is, the more likely -# it was in that class. +# a config value three hundred turns later. It is not surfaced. A tier filter +# would make a whole class of rules permanently ineligible for the only +# mechanism that puts a rule in front of an agent AT the moment, and the more +# important a rule is, the more likely it is in that class. # -# The deeper defect is that the filter was doing the THRESHOLD's job. Whether +# And such a filter does the THRESHOLD's job. Whether # a rule belongs in this hint is a relevance question, and a similarity bar is # the control for relevance. A categorical exclusion standing in for a # relevance judgment cannot be tuned, cannot be measured, and cannot be wrong # in a way anybody notices. # -# THIS IS A MEASURED CHANGE, NOT A SETTLED ONE. The old comment's fear is -# real — a hint that fires on every write and says obvious things teaches the -# reader to skip the block, and the surface is then lost along with its true -# positives. That fear had simply never been checked. `retrieval_logs` already -# records top_score, result_count and the query for every call, so the -# evidence now arrives on its own: -# -# - rules clear the bar often and at high scores -> the fear was justified, -# the filter was a crude proxy for a bar set too low, and the WORK IS THE -# BAR. Any reinstated filter should then carry a measured reason. -# - rules clear rarely, in a thin band near the bar -> the filter was never -# the right instrument and relevance was always sufficient. -# -# Only the eligibility moved. The bar and k=1 were both left exactly where -# they were, so the resulting distribution has one cause. +# The risk is real — a hint that fires on every write and says obvious things +# teaches the reader to skip the block. `retrieval_logs` records top_score, +# result_count and the query for every call, so it is measurable: rules +# clearing the bar often and at high scores mean the WORK IS THE BAR, not a +# tier filter. -# How much of a command reaches the embedding (#3476). A shell call is not a +# How much of a command reaches the embedding. A shell call is not a # file: most are short, and the ones that are not are usually a heredoc or a # pasted script whose bulk says nothing about which rule applies. The VERB AND -# ITS TARGET sit at the front — `curl https://git.fabledsword.com/api/...`, +# ITS TARGET sit at the front — `curl https://git.example.com/api/...`, # `docker compose up`, `git checkout -b` — and that head is the whole signal. # Sending the tail as well would push it out of a 512-token window and let a # heredoc's prose decide the match. _TOOL_QUERY_CHARS = 400 # Minimum SUBSTANCE (non-whitespace chars) a payload must carry before the -# semantic arm will run at all — the cheap half of the operator's #89 idea -# ("a sliding scale between number of characters and semantic threshold"). +# semantic arm will run at all. # # Deliberately NOT a settings knob and deliberately conservative. Its job is # only to drop payloads too small to carry meaning, where an embedding is noise @@ -459,39 +366,37 @@ _TOOL_QUERY_CHARS = 400 # single-line. 48 sits below the smallest plausible reusable helper (a one-line # `def` with a body runs ~60), so it errs toward keeping recall and leaves # precision to the threshold above, which is where the measured separation is. -# The full length↔threshold CURVE is still open in #89 — the operator flagged it -# as wanting a brainstorm, so this stays a flat floor rather than an invented -# scale. It also saves a pointless embedding round-trip on trivial edits. +# A flat floor rather than a length-to-threshold curve, and it saves a +# pointless embedding round-trip on trivial edits. WRITEPATH_MIN_CODE_CHARS = 48 -# --- concept extraction for the semantic arm's query (#2242) ------------------ +# --- concept extraction for the semantic arm's query ------------------------- # A snippet's embedded text is f"{title}\n{body}", and for a snippet that body is # composed markdown: **When to use:**, **Signature:**, **Location:**, then the # fenced code. So `when_to_use` — the description of what the thing is FOR — # appears twice in the vector, and the document is prose-forward. # -# The arm used to query it with raw code and no prose at all. Measured on the -# deployed instance against snippet #2222, same corpus: +# Querying it with raw code and no prose matches poorly. Measured against one +# recorded snippet: # query built from score best unrelated separation # raw code body 0.743 0.630 0.11 # name + docstring 0.823 0.602 0.22 # hand-written concept prose 0.835 0.583 0.25 # A 12-word description beats a near-verbatim reimplementation of the function, -# and code-as-query RAISES the noise floor. It is also the cleanest explanation -# for the fragment miss recorded on #2223: a short code excerpt has almost no +# and code-as-query RAISES the noise floor. A short code excerpt has almost no # prose to match against a document that is mostly prose. # # So we send the concept instead — and shape it like a snippet's own title, # "{name} — {when_to_use}", because that is the form the 0.823 measurement used. # Undocumented code yields little, and a Vue SFC or a config file yields nothing; -# those fall back to the raw payload and behave exactly as before. This raises +# those fall back to the raw payload. This raises # the ceiling for documented helpers rather than fixing every case. # Declaration forms, one pattern per shape, every pattern exposing (name, params) # so composition doesn't have to care which matched. Deliberately regex and not a # real parser: this runs on a PreToolUse hook's critical path, the payload is # frequently a FRAGMENT that no parser would accept (an Edit's new_string is -# rarely a valid module), and a miss costs only a fallback to today's behaviour. +# rarely a valid module), and a miss costs only a fallback to the raw payload. _CONCEPT_DECL_PATTERNS = ( # python: def / async def, and class with optional bases re.compile(r"^[ \t]*(?:async[ \t]+)?def[ \t]+([A-Za-z_]\w*)[ \t]*(\([^)]*\))", re.M), @@ -528,17 +433,14 @@ _CONCEPT_MAX_DECLS = 4 _CONCEPT_MIN_CHARS = 16 # Hard ceiling on top-k regardless of the user's setting — this is an -# awareness menu (titles only), never a content dump. -# The budget ceiling, now shared by every surface rather than owned by this -# one (#4102). It bounds the same thing everywhere — how many lines a single -# unsolicited injection may occupy — so one surface having a private ceiling -# was an accident of which arm got a configurable budget first. +# awareness menu, never a content dump. The ceiling is shared by every +# surface: it bounds the same thing everywhere — how many lines a single +# unsolicited injection may occupy. _AUTOINJECT_MAX_TOP_K = MAX_BUDGET -# THE CONVERSATION BESIDE THE PROMPT (#4364). The operator's message is the -# only query this arm had, and mid-session it is mostly a follow-up — "yes do -# that", "now fix the filter" — that names nothing a record could match. The -# hook now sends the tail of the last assistant reply as `context`, and it is +# THE CONVERSATION BESIDE THE PROMPT. The operator's message alone is a weak +# query mid-session: it is mostly a follow-up — "yes do that", "now fix the +# filter" — that names nothing a record could match. The hook sends the tail of the last assistant reply as `context`, and it is # appended to a SHORT prompt only: a prompt that already says what it is about # is the better query on its own, and diluting it is the one way this can make # retrieval worse. @@ -565,7 +467,7 @@ def _autoinject_query(prompt: str, context: str) -> str: return prompt return f"{prompt}\n\n{ctx}" -# --- the prompt-boundary rule arm (#3852) ------------------------------------ +# --- the prompt-boundary rule arm ------------------------------------------- # # Both existing rule arms are keyed on something the session is about to DO — # a file write, a command. A rule that governs what to SAY has no such moment. @@ -574,10 +476,9 @@ def _autoinject_query(prompt: str, context: str) -> str: # RESPONSE, and no tool call precedes a response. # # The operator's message is the only query that exists before one is composed, -# and this arm is what runs against it. Until now that hook searched notes -# alone, so no rule had ever been retrieved against a thing the operator said. +# and this arm is what runs against it. PROMPTRULE_THRESHOLD_KEY = SURFACES["prompt_rule"].floor_key -# INHERITED FROM THE ACT ARMS, AND NOT YET EARNED HERE. 0.72 was tuned against +# INHERITED FROM THE ACT ARMS. 0.72 is tuned against # code and shell commands. An operator's prose is a different query shape # against the same documents, and nothing yet says the two distributions line # up — triggers are written in the vocabulary of the MOMENT, which for most @@ -585,20 +486,16 @@ PROMPTRULE_THRESHOLD_KEY = SURFACES["prompt_rule"].floor_key # # Starting at the act arms' number anyway is deliberate: it is the only value # with evidence behind it, and guessing lower would put an unmeasured bar in -# front of a corpus that binds. Every call is logged under `prompt_rule` from -# the first deploy, so a few days of real traffic settles it — read -# `near_miss_samples` (#3807) before moving this, not the percentile alone. +# front of a corpus that binds. Every call is logged under `prompt_rule`, so +# real traffic settles it — read `near_miss_samples` before moving this, not +# the percentile alone. PROMPTRULE_DEFAULT_THRESHOLD = SURFACES["prompt_rule"].floor_default -# MORE THAN THE ACT ARMS' SINGLE SLOT, anchored on this hook's budget rather -# than theirs. RULEHINT_LIMIT is 1 because that arm fires before EVERY Bash -# call, where a second line is a second interruption per command. This arm -# fires once per TURN, on the same hook whose notes menu already spends -# AUTOINJECT_DEFAULT_TOP_K slots — so that is the comparable budget. -# -# And a prompt genuinely contains more than one act. "Merge to main and then -# start on X" is two, governed by different rules; k=1 cannot serve that case -# at all, where the act arms never face it because a command is one thing. +# ANCHORED ON THIS HOOK'S BUDGET rather than the act arms'. This arm fires +# once per TURN, on the same hook whose notes menu already spends +# AUTOINJECT_DEFAULT_TOP_K slots — so that is the comparable budget. And a +# prompt often contains more than one act: "merge to main and then start on +# X" is two, governed by different rules. PROMPTRULE_LIMIT = SURFACES["prompt_rule"].budget_default @@ -622,8 +519,8 @@ async def build_process_manifest(user_id: int) -> dict: entry becomes a skill file on the operator's machine that auto-surfaces and is followed as written. It therefore uses the BROWSE scope (via the no-query knowledge list): a Process shared directly with the operator is - never installed here, only one they own or reach through a shared project - (decision note 2094). Project-shared entries are labelled with their owner so + never installed here, only one they own or reach through a shared project. + Project-shared entries are labelled with their owner so the stub can't pass off someone else's procedure as the operator's own. Returns {"processes": [{id, name, slug, description, shared?, owner?}], @@ -685,10 +582,9 @@ async def build_process_manifest(user_id: int) -> dict: entry["owner"] = it.get("owner") procs.append(entry) - # The most consequential passive surface Scribe has (see SCOPE above), and - # it emitted nothing — a Process installed as a skill, matched on every - # relevant turn and never once opened, was indistinguishable from one never - # installed (#2477). The honest event is "installed on the operator's + # The most consequential passive surface Scribe has (see SCOPE above): a + # Process installed as a skill and never once opened must be + # distinguishable from one never installed. The honest event is "installed on the operator's # machine", which is a surfacing in effect: the skill description is in # front of the model each session. AMBIENT source — installation is not a # ranked choice — so it lands in ambient_count, not surfaced_count. @@ -705,13 +601,9 @@ async def get_autoinject_config(user_id: int) -> dict: Returns {"enabled": bool, "threshold": float, "top_k": int}. - THE TWO NUMBERS COME FROM THE REGISTRY NOW (#4102). They used to be read and - clamped here, and identically again in `get_writepath_config`, and again in - three rule arms, and once more in the completion-report arm (since - retired, milestone 500). That was - tolerable while the values were shipped constants. It stops being tolerable - once a tool is expected to MOVE them, because a tuning surface cannot be - consistent across arms that each spell their configuration differently. + THE TWO NUMBERS COME FROM THE REGISTRY, like every arm's: a tool is + expected to MOVE them, and a tuning surface cannot be consistent across + arms that each spell their configuration differently. `enabled` stays here: it is this surface's own switch, not a tunable number, and the registry deliberately holds only the pair a floor-tuner touches. @@ -738,11 +630,11 @@ async def build_autoinject_hint( ) -> dict: """Title-first awareness hint for the plugin's UserPromptSubmit hook. - The four anti-bloat gates (see the module + milestone-93 design): + The four anti-bloat gates: 1. high-confidence threshold (stricter than pull) — set per-user; 2. margin gate — keep only hits within retrieval_pipeline._NOTE_BAND of the top score; 3. session marking — caller passes already-injected ids as `exclude_ids` - and they are rendered again with `[seen]`, never withheld (#4101); + and they are rendered again with `[seen]`, never withheld; 4. title-first payload — id + kind + title + score only, never bodies. Disabled, blank-query, or nothing-clears-the-gates all return empty context, so most turns inject nothing. @@ -756,7 +648,7 @@ async def build_autoinject_hint( q = (query or "").strip() if not cfg["enabled"] or not q: return empty - # RECORDS NAMED BY NUMBER (#4796), read from the operator's own words before + # RECORDS NAMED BY NUMBER, read from the operator's own words before # the query is enriched: a number in the assistant's reply is not one the # operator named. "go ahead with 4448" says which record is meant, and a # number means nothing to an embedding, so these are looked up rather than @@ -764,28 +656,18 @@ async def build_autoinject_hint( named = await _named_records(user_id, record_refs_svc.named_record_ids(q)) named_ids = [int(n.id) for n in named] # Everything below searches, logs and fills its slots on the ENRICHED - # query, so the telemetry row records what was actually asked (#4364). + # query, so the telemetry row records what was actually asked. q = _autoinject_query(q, context) - # THE LEDGER LEAVES THE SEARCH (#4101). `exclude_ids` used to go into - # `semantic_search_notes` itself, so a record this session had already been - # shown was removed from the candidate set — which is #3750's defect, on the - # arm that fires most. Three things followed from it, none of them intended: - # - # - the second time a note was the best answer, the session got SILENCE, - # indistinguishable from "nothing matched"; - # - a compaction made that permanent, because the ledger outlived the - # context it described (fixed one layer down in this same step); - # - and the score the search reported was measured against a candidate set - # the caller had already edited, so `best_available` could name a bar - # that turned nothing away (#3739). - # - # Now the ledger is a RENDERING fact, not a retrieval one: every repeat is - # still ranked, still shown, and carries a marker saying it was surfaced - # before. A note line is a title and a score — the repeat costs about as + # THE LEDGER IS A RENDERING FACT, NOT A RETRIEVAL ONE. Excluding seen ids + # from the search would make the second time a note is the best answer + # SILENCE, indistinguishable from "nothing matched", and would measure + # `best_available` against a candidate set the caller had already edited. + # So every repeat is still ranked, still shown, and carries a marker saying + # it was surfaced before. A note line is a title and a score — the repeat costs about as # much as the comma in this sentence — so there is nothing here to save. already = {int(i) for i in (exclude_ids or [])} - # The ranked half is `retrieval_pipeline.run_note_arm` (milestone 456): + # The ranked half is `retrieval_pipeline.run_note_arm`: # search, the fresh/repeat split, the call row, the band, the reuse and # lesson slots in that order, and the surfacing rows are written there. # What is decided HERE is what makes this moment this moment — the query @@ -810,7 +692,7 @@ async def build_autoinject_hint( if n.user_id != user_id }) - # A superseded record is DEMOTED, not removed (#278) — `menu_entry` says + # A superseded record is DEMOTED, not removed — `menu_entry` says # so on the line. One query for the whole menu. stale = await superseded_ids([*named_ids, *(int(n.id) for _s, n in kept)]) systems = await system_names_for( @@ -851,10 +733,9 @@ async def build_autoinject_hint( # "records", not "notes" — the menu can hold snippets, processes and tasks # too, and the kind marker on each line is only legible if the header doesn't # already claim they're all one thing. - # "injected once per session" was true and is not any more (#4101): a repeat - # is shown again with a marker rather than withheld, so the header must stop - # promising the old contract. It now says what the marker means instead, - # once, rather than each repeated line having to explain itself. + # A repeat is shown again with a marker rather than withheld, so the header + # says what the marker means, once, rather than each repeated line having + # to explain itself. if kept: lines.append( "> Possibly relevant from your Scribe records — open any in full with " @@ -863,11 +744,10 @@ async def build_autoinject_hint( "the passage that matched; a line marked `seen` is a pointer to one " "already shown this session, so it is in your context:" ) - # THE REGISTER, SAID ONCE AND ONLY WHEN IT APPLIES (milestone 385 step 5). + # THE REGISTER, SAID ONCE AND ONLY WHEN IT APPLIES. # - # The operator's requirement for this kind was "they don't always have to - # be followed", and the risk is not that a reader mistakes a lesson for a - # rule — this menu's voice is already the non-binding one, deliberately + # A lesson does not always have to be followed, and the risk is not that a + # reader mistakes a lesson for a rule — this menu's voice is already the non-binding one, deliberately # (see the `seen` marker below). The risk is the opposite: read as one more # title in a list of MATERIAL, a lesson looks like something to open if # curious, when it is advice someone paid for. The line has to say "weigh @@ -891,10 +771,10 @@ async def build_autoinject_hint( for score, note in kept: nid = int(note.id) note_ids.append(nid) - # The NAME, not the title (#4364): a snippet's or lesson's title is its - # embedding shape, trigger and all, and ran past 1,500 characters here. + # The NAME, not the title: a snippet's or lesson's title is its + # embedding shape, trigger and all, and can run past 1,500 characters. name = _menu_name(note.title, note.note_type, note.data, note.body) - # The passage that earned the line, WHOLE, indented under it (#4364). + # The passage that earned the line, WHOLE, indented under it. # Absent when the record has no stored chunk — an un-embedded row, or # the reserved lesson and reuse slots, which are fetched by their own # queries and so are not in this search's report. No fallback to the @@ -924,7 +804,7 @@ async def build_autoinject_hint( ) # The lessons this menu put in front of the reader, repeats included — for - # the soft-link recorder (#4637), which pairs them with the rule arm's + # the soft-link recorder, which pairs them with the rule arm's # lines in the same response. Relevance, not the session ledger, is what # makes a co-arrival, so a `seen` lesson counts — and so does one the # operator named, which is relevance by their own say-so. @@ -985,15 +865,13 @@ async def _rule_moment( checkpoint_floor: float = 0.0, ) -> rp.RuleResult: """One rule moment, composed: the direct arm, then the rules its matching - lessons bring in (#4633) — after the band and never in place of it. + lessons bring in — after the band and never in place of it. - Every rule builder below calls this ONE function, where each used to run - the arm and then the via-lesson step by hand, passing the query between - them through a key on the payload. The via-lesson step searches with the + Every rule builder below calls this ONE function. The via-lesson step searches with the query the arm searched with, and skips what the arm named plus what the session's ledger holds — suppression is by RULE, whichever lesson reached it. Its lines and fresh ids join the arm's; `shown_rule_ids` stays the - direct arm's band, which is what the soft-link recorder pairs (#4637). + direct arm's band, which is what the soft-link recorder pairs. """ result = await rp.run_rule_arm( arm, moment, floor=floor, budget=budget, io=_rule_io(), @@ -1020,12 +898,11 @@ async def build_prompt_rule_hint( ) -> dict: """Rules and preferences that may apply to what the operator just asked. - The third rule arm, and the one that closes a gap the other two cannot - reach. `write_path_rule` is keyed on code, `pre_tool_rule` on a command — - both are things the session is about to DO. A rule that governs what to - SAY has no such trigger, and residency was the only surface it ever had. - Removing residency (milestone 394) without this would drop that half of - the corpus on the floor. + The third rule arm, and the one that reaches what the other two cannot. + `write_path_rule` is keyed on code, `pre_tool_rule` on a command — both + are things the session is about to DO. A rule that governs what to SAY has + no such trigger; nothing is preloaded, so without this arm that half of the + corpus would never arrive. A SEPARATE FUNCTION, not a branch inside build_autoinject_hint, and the reason is its early returns. That arm bails when auto-inject is disabled, @@ -1035,15 +912,14 @@ async def build_prompt_rule_hint( coupling nothing downstream could see. Two functions, two sets of gates, composed by the caller. - THE OUTPUT IS DELIBERATELY NOT QUOTED, where the notes menu is. The task - asked whether the two share a header; the answer is that neither needs - one. A note line is a bare title and needs the menu's header to say what + THE OUTPUT IS DELIBERATELY NOT QUOTED, where the notes menu is, and the + two share no header. A note line is a bare title and needs the menu's header to say what it is doing there, while a rule line names itself in its opening words ("Standing rule that may apply…" / "Preference that may apply…"). Leaving rules unquoted separates the two claims visually with no extra prose, and matches how a rule line already renders on both act arms. - NO `checkpoint` KEY (#4214): the act arms can hold a call because there + NO `checkpoint` KEY: the act arms can hold a call because there is a composed act to hold, while this one fires before anything has been decided. @@ -1054,13 +930,13 @@ async def build_prompt_rule_hint( q = (query or "").strip() if not q: return out - # The same enrichment the notes arm gets (#4364). A rule is meant to + # The same enrichment the notes arm gets. A rule is meant to # arrive while the work it governs is under way, not once the operator # names it — and "yes, go ahead" names nothing a trigger can match, while # the reply it answers ("commit this to dev and push") does. q = _autoinject_query(q, context) try: - # SCOPED TO THIS SESSION'S PROJECT (milestone 414): global rules plus + # SCOPED TO THIS SESSION'S PROJECT: global rules plus # the bound project's own. An unbound session (project_id 0) gets # global rules only — this surface speaks unasked, and a whole-rulebook # answer is only right for someone who asked the whole rulebook. @@ -1079,7 +955,7 @@ async def build_prompt_rule_hint( out["context"] = "\n".join(result.lines) out["rule_ids"] = result.rule_ids # Every rule LINE the direct arm showed, repeats and the reserved - # slot included — for the soft-link recorder (#4637). Distinct + # slot included — for the soft-link recorder. Distinct # from `rule_ids`, which is telemetry's fresh-only cut. out["shown_rule_ids"] = result.shown_rule_ids except Exception: @@ -1101,16 +977,16 @@ def _rule_io() -> rp.RuleIO: ) -# --- Write-path trigger (#2082): prior art at the moment code is written ------ +# --- Write-path trigger: prior art at the moment code is written ------------- # Auto-inject above fires on the operator's prompt. The moment reuse is actually -# lost is later — when the AGENT decides mid-task to write a helper — and nothing -# fired there. This is that trigger: the plugin's PreToolUse hook on Write/Edit +# lost is later — when the AGENT decides mid-task to write a helper. This is +# that trigger: the plugin's PreToolUse hook on Write/Edit # asks what prior art is already recorded for the file being written. # # Two arms, deliberately different in kind: # - BY PLACE — a snippet recorded at this path (or in its directory) is prior # art by definition, not by resemblance, so it isn't scored or thresholded. -# This is what the reverse lookup (#2083) was built to answer. +# The reverse lookup answers it. # - BY MEANING — semantic search over snippets only, using the code about to be # written, under the same gates as auto-inject. # Place beats meaning in the menu because "there is already a canonical helper in @@ -1124,8 +1000,8 @@ def _prior_art_line(item: dict, marker: str, owner: str | None, foreign_lang: st rather than appended after the title, so the reader sees it while still reading the score — the two together are the judgement being offered. """ - # The NAME, not the composed title (#4364) — a snippet's title carries its - # whole trigger and ran to kilobytes on this line, again on every repeat. + # The NAME, not the composed title — a snippet's title carries its whole + # trigger and can run to kilobytes, again on every repeat. # `snippet` is a snippet record's field dict — but on a search hit it is # the matched excerpt, a string, under the same key. fields = item.get("snippet") @@ -1140,15 +1016,13 @@ def _prior_art_line(item: dict, marker: str, owner: str | None, foreign_lang: st return line -# --- cross-language prior art (#2244) ---------------------------------------- -# Retrieval is concept-shaped now, and concepts are language-agnostic: a query -# about a TypeScript union-find matches a PYTHON snippet at 0.72-0.73, comfortably -# over the bar. That is a feature — the operator's framing is "borrow the shape of -# the solution even when the code isn't directly reusable" — but only if the line -# SAYS so. An unlabelled Python hit offered while writing TypeScript either gets -# dismissed as irrelevant or, worse, pasted into a .ts file. Measured note: this -# cross-language matching predates concept queries; it was always happening, just -# never disclosed. +# --- cross-language prior art ------------------------------------------------ +# Retrieval is concept-shaped, and concepts are language-agnostic: a query about +# a TypeScript union-find matches a PYTHON snippet comfortably over the bar. That +# is a feature — borrow the shape of the solution even when the code isn't +# directly reusable — but only if the line SAYS so. An unlabelled Python hit +# offered while writing TypeScript either gets dismissed as irrelevant or, worse, +# pasted into a .ts file. # # Deliberately NOT gated behind a stricter threshold for foreign-language hits: a # higher bar would suppress exactly the shape-borrowing this is for. Label, don't @@ -1246,12 +1120,12 @@ def concept_query(code: str) -> str: Returns something shaped like a snippet's own title — "name(params) — what it does" — because that is the form that measured best against the prose-forward - snippet documents (#2242; see the table at _CONCEPT_DECL_PATTERNS). + snippet documents (see the table above _CONCEPT_DECL_PATTERNS). Returns "" rather than raising or guessing whenever there's nothing worth sending: no declarations and no doc, or a result too thin to beat the code it would replace. The caller treats "" as "use the payload as-is", so every - unhandled language degrades to exactly the previous behaviour. + unhandled language degrades to the raw payload. """ if not code or not code.strip(): return "" @@ -1272,7 +1146,8 @@ def concept_query(code: str) -> str: # NO DOC, NO REWRITE. An identifier alone is not a concept, and it measured # WORSE than the code it would replace: `collapse_into_clusters(edges)` scored - # 0.671 against #2222 where the full code body scored 0.743. Separation from + # 0.671 against its recorded snippet where the full code body scored 0.743. + # Separation from # the noise floor is identical (0.113 either way), but the absolute value # drops below the 0.68 bar — so preferring a bare name would convert a # comfortable hit into a miss. Undocumented code keeps the raw payload. @@ -1293,17 +1168,15 @@ async def get_writepath_config(user_id: int) -> dict: Three surfaces' worth of configuration arrives in one call because one hook request drives all three arms. They are still three SURFACES with three - independent pairs, resolved from the registry (#4102). + independent pairs, resolved from the registry. - The floors were split apart one at a time, each on its own measurement, and - those measurements are recorded where the defaults are: WRITEPATH (#2223 — - code embeddings sit on a much higher similarity floor than prose), RULEHINT - (a third corpus again), TOOLRULE (#3853 — a shell command is a different - query shape from a code payload and scores lower for the same relevance). + Each floor's reason is recorded where its default is: WRITEPATH (code + embeddings sit on a much higher similarity floor than prose), RULEHINT (a + third corpus again), TOOLRULE (a shell command is a different query shape + from a code payload and scores lower for the same relevance). - THE BUDGET IS NOW SPLIT TOO. `top_k` used to be auto-inject's outright, on - the argument that "how many titles at once" means the same thing on both - surfaces. It does not: this arm fires before every Write and Edit while + THE BUDGET IS SPLIT TOO. "How many titles at once" does not mean the same + thing on both surfaces: this arm fires before every Write and Edit while auto-inject fires once a turn, so the same number buys wildly different amounts of attention. `write_path` inherits auto-inject's value when it has none of its own, so no install that tuned the shared knob loses it. @@ -1337,8 +1210,8 @@ async def _checkpoint_floor(user_id: int) -> float: A floor read out of settings reaches here as operator-typed text. Below zero it would stop every act with a rule anywhere near it; above one it - can never fire and the feature is silently dead, which is the failure mode - #3430 found and the reason this clamps rather than validating at the door. + can never fire and the feature is silently dead — which is why this clamps + rather than trusting a validation at the door. """ raw = await get_setting( user_id, _CHECKPOINT_THRESHOLD_KEY, str(_CHECKPOINT_DEFAULT), @@ -1370,18 +1243,16 @@ async def build_write_path_hint( written, used only as the semantic query. A hit recorded AT this exact path is not a reuse suggestion — it IS the - record of the file being changed, so it renders as the SYNC class (#2708): + record of the file being changed, so it renders as the SYNC class: "this snippet records the file you're editing; if the edit changes the - recorded shape, updating the record is part of the edit." That is the - operator's chosen alternative to server-side drift flagging (decision - #2707): the record gets corrected in the session that has the context, - at the moment of change. Nearby and semantic hits stay the REUSE menu. + recorded shape, updating the record is part of the edit." The record gets + corrected in the session that has the context, at the moment of change. Nearby and semantic hits stay the REUSE menu. The two classes track the session on SEPARATE channels — `exclude_ids` (reuse) and `exclude_sync_ids` (sync) — because they answer different questions: a title shown as "consider reusing this" twenty turns ago must - not silence "you are editing the recorded file right now" (#2708). The two - channels also now ACT differently, which is #4101: a reuse repeat is + not silence "you are editing the recorded file right now". The two + channels also ACT differently: a reuse repeat is rendered again with a `seen` marker, while the sync class still shows once, because its claim is about an edit in progress rather than about a record's continuing relevance and repeating it would be nagging. @@ -1391,7 +1262,7 @@ async def build_write_path_hint( with a lot of recorded history can't turn one edit into a wall of text. Two gates are its OWN, because code is not prose: a stricter similarity threshold, and a minimum-substance floor on `code` below which the - semantic arm doesn't run at all (#2223 — see WRITEPATH_DEFAULT_THRESHOLD and + semantic arm doesn't run at all (see WRITEPATH_DEFAULT_THRESHOLD and WRITEPATH_MIN_CODE_CHARS). Returns empty context when disabled, when there's no path, or when nothing is recorded — which is the common case, and the point. @@ -1408,17 +1279,17 @@ async def build_write_path_hint( source, so its precision is tunable separately from auto-inject's. Location hits still carry no score and so stay out of retrieval_logs, whose - score distribution they would corrupt. What closed the gap (#2085) is that - un-scored surfacing now has its own home: every arm emits note_usage_events, + score distribution they would corrupt. Un-scored surfacing has its own + home instead: every arm emits note_usage_events, tagged 'write_path_sync' vs 'write_path_place' vs 'write_path_semantic', so each claim's pull-through rate is measurable on its own. - ``stamp_shapes`` turns the same request into the ledger's write-path feed - (#2791): the (kind, name) definitions the hook saw in — or enclosing — + ``stamp_shapes`` turns the same request into the ledger's write-path feed: + the (kind, name) definitions the hook saw in — or enclosing — the payload. When the session has PULLED a snippet recently and this payload references or resembles it, those shapes carry it as a PROPOSAL (see shape_ledger.suggest_write_path_instances) and the result's - ``suggested`` lists them. Never a verdict since milestone 439: the agent + ``suggested`` lists them. Never a verdict: the agent that wrote the code judges it at the end of the turn, shown this as evidence. The route passes it only for a caller allowed to write — a read-scoped key gets the hint, never a ledger write. ``repo_key`` (the @@ -1462,11 +1333,10 @@ async def build_write_path_hint( # they join `seen`, so the reuse arms (where the directory query would # surface them again) never re-list a record the sync block owns. # - # `seen` IS THIS CALL'S OWN MENU, and nothing else (#4101). It used to start - # from `excluded` — the session ledger — which folded two different claims - # into one variable: "already listed a few lines above" and "shown at some - # earlier point in the session". Only the first is a reason to stay quiet. - # The ledger is now a marker instead, on both reuse arms. + # `seen` IS THIS CALL'S OWN MENU, and nothing else. "Already listed a few + # lines above" is a reason to stay quiet; "shown at some earlier point in + # the session" is not — the session ledger is a marker instead, on both + # reuse arms. seen: set[int] = set() synced: list[dict] = [] for item in here: @@ -1489,7 +1359,7 @@ async def build_write_path_hint( item["seen"] = nid in excluded placed.append(("nearby · seen" if nid in excluded else "nearby", item)) - # The stamping feed's "actually pulled it" half (#2791). Read once, before + # The stamping feed's "actually pulled it" half. Read once, before # the semantic arm, because the arm's query doubles as the resemblance # test: a pulled snippet this session already saw (so it sits in `seen`) # must still be SCORED for this payload — it just isn't re-listed. @@ -1504,14 +1374,14 @@ async def build_write_path_hint( query = (code or "").strip() # Drop payloads too small to carry meaning before spending an embedding on # them — a one-line Edit is not a helper being rewritten, and its embedding - # scores off the corpus floor rather than off any real resemblance (#2223). + # scores off the corpus floor rather than off any real resemblance. # Whitespace doesn't count: code is indentation-heavy, so raw length would # let a deeply-nested one-liner through on padding alone. if len("".join(query.split())) < WRITEPATH_MIN_CODE_CHARS: query = "" # ORDER MATTERS: the floor above judges the RAW payload, this rewrites it. # Snippet documents are prose-forward, so a concept query out-scores the code - # itself by a wide margin (#2242). The rewritten query is allowed to be + # itself by a wide margin. The rewritten query is allowed to be # short — "slugify(t) — turn text into a url slug" is a fine query at 38 # chars, and it only exists because the raw payload already cleared the # floor. Applying the floor after this would throw away the best queries. @@ -1524,8 +1394,8 @@ async def build_write_path_hint( wp_chunks: dict[int, dict] = {} if remaining > 0 and query: # The ranked half is `retrieval_pipeline.run_note_arm` with the - # WRITE_PATH spec (milestone 456): which kinds it asks for and why, - # the withheld-menu accounting (#3739), the fresh/repeat split, the + # WRITE_PATH spec: which kinds it asks for and why, the withheld-menu + # accounting, the fresh/repeat split, the # call row and the surfacing rows are written there. What is decided # HERE is the menu above it — what place and sync already listed is # withheld, and a PULLED snippet among those is still scored, because @@ -1556,8 +1426,8 @@ async def build_write_path_hint( else f"similar {score:.2f} · {kind}" ) # The repeat marker rides the same dotted list as the kind, so a - # reference costs four characters and needs no second line - # (#4101). Same word as the auto-inject menu deliberately: a + # reference costs four characters and needs no second line. + # Same word as the auto-inject menu deliberately: a # reader meeting `seen` on two different surfaces should not # have to work out whether they mean the same thing. if int(note.id) in excluded: @@ -1567,8 +1437,8 @@ async def build_write_path_hint( { "id": int(note.id), "title": note.title, "user_id": note.user_id, # The name the line shows, and whether this session has - # it already — carried as data for the reason `kind` is - # (#4364): the line is built from facts, not from + # it already — carried as data for the reason `kind` is: + # the line is built from facts, not from # re-reading its own marker. "name": _menu_name(note.title, note.note_type, note.data, note.body), # What its chunks are prefixed with, for stripping. @@ -1583,8 +1453,8 @@ async def build_write_path_hint( # in it would break the next time a marker is reworded, # silently and in the direction of saying nothing. "kind": kind, - # Carried so the line can disclose a cross-language hit - # (#2244). The semantic arm is where these actually arise — + # Carried so the line can disclose a cross-language hit. + # The semantic arm is where these actually arise — # a snippet recorded at the path you're editing is almost # never in another language, but a concept match easily is. "language": (note.data or {}).get("language") if note.data else None, @@ -1605,7 +1475,7 @@ async def build_write_path_hint( ) except Exception: logger.warning("Write-path ledger suggestion failed", exc_info=True) - # The in-band button-B check (#2793): the hook named the shapes being + # The in-band button-B check: the hook named the shapes being # written; if this directory+kind is canon-dense and a named shape isn't # (about to be) an instance of that canon, say so NOW — at the write, # not at the next audit. @@ -1617,7 +1487,7 @@ async def build_write_path_hint( ) except Exception: logger.warning("write-time divergence check failed", exc_info=True) - # The in-band DERIVE check (#2900): the ledger's own knowledge of the + # The in-band DERIVE check: the ledger's own knowledge of the # names being written — a duplicate family with no canon, or a canon # recorded elsewhere. This is the arm the by-name local grep could not # be: it knows whether the other copies are canon or stray. Keyed per @@ -1631,35 +1501,11 @@ async def build_write_path_hint( except Exception: logger.warning("write-time derive check failed", exc_info=True) staleness: list[str] = [] - # ── Have the rules moved under this session? (milestone 323) ─────── - # - # THE RULES-ETAG STALENESS ARM IS GONE (milestone 394). - # - # It took a marker the session had been given at SessionStart, compared it - # against the resident set as it stood now, and said which rules had moved - # or fallen out of force. That was worth doing while a session held a - # fixed set of rules from turn zero and could be holding a stale copy of - # it hours later. - # - # Nothing is resident now. A rule is retrieved at the moment it applies, - # so a session cannot be holding an out-of-date one — the next act that - # needs it fetches it again. The staleness this arm reported was an - # artifact of the delivery model rather than a fact about the corpus, and - # it goes with the model. - # - # `staleness` survives as the list the arms below still append to. - - # The guard sits BELOW the staleness arm on purpose. A rules change is - # unconditional news — it does not become less true because this - # particular write happened to match no prior art — and this arm is one - # indexed query, only when the session actually sent a marker. - # # The standing-rule arm further down is deliberately left on the far side - # of this guard: that one runs a SEMANTIC search, and moving it here would - # run an embedding query on every write in the session. Its gating is a - # separate question from this one (see the note on #3244). - # The design arm (#4256) is decided HERE, above the guard, so a UI write + # of the guard below: it runs a SEMANTIC search, and moving it above would + # run an embedding query on every write in the session. + # The design arm is decided HERE, above the guard, so a UI write # that matched no prior art still carries it — and it returns on its own # rather than joining the guard's condition, because joining it would let # a design line switch the standing-rule arm below on for writes where it @@ -1667,7 +1513,7 @@ async def build_write_path_hint( design_text, design_dedup = await _design_arm( user_id, project_id, path, set(exclude_derive or []), ) - # The rulings arm (milestone 444), decided here for the design arm's + # The rulings arm, decided here for the design arm's # reasons: a lookup by path, so a write that matched no prior art still # carries its area's rulings, and it never switches the ranked arms on. rulings = await system_rulings_svc.rulings_for_paths( @@ -1700,17 +1546,15 @@ async def build_write_path_hint( for marker, item in menu: rendered.append((item, marker, _owner_of(item), _foreign_language(item, target_lang))) - # Seeded with the staleness line, which is decided above the early - # return and so cannot wait for this list to exist. lines: list[str] = list(staleness) - # First after staleness: rulings and the design system BIND, where - # everything below is prior art. + # Rulings and the design system come first: they BIND, where everything + # below is prior art. lines.extend(rulings["lines"]) if design_text: lines.append(design_text) sync_note_ids: list[int] = [] if synced: - # The sync framing (#2708). Deliberately imperative about the record — + # The sync framing. Deliberately imperative about the record — # the reuse claim ("start from this shape") is still implied by the # title being right there, but the load-bearing sentence is the one no # other surface says: keeping the record true is part of THIS edit. @@ -1770,7 +1614,7 @@ async def build_write_path_hint( # is no matching passage and the body's opening would be a fabricated # reason. Absence here is meaningful: a line with no passage under it is # one that earned its place by where it lives, not by what it says. - # WHOLE, and only on first sight (#4364): a `seen` line is a pointer to + # WHOLE, and only on first sight: a `seen` line is a pointer to # a record already in context, and its passage is already there too. if item.get("seen"): continue @@ -1790,19 +1634,18 @@ async def build_write_path_hint( lines.append(_derive_line(path, derive)) # Split by arm, which is the whole reason this table exists. The place arm - # carries no score and so has no home in retrieval_logs; before #2085 a - # snippet surfaced BY PLACE left no trace anywhere, making the arm that - # fires on the strongest possible claim ("there is already a canonical - # helper in this exact file") the one arm nobody could measure. The sync - # class gets its own tag: its pull-through rate is the number that says - # whether edit-time record-sync actually happens (#2708's success measure). + # carries no score and so has no home in retrieval_logs, yet it fires on + # the strongest possible claim ("there is already a canonical helper in + # this exact file") and must be measurable. The sync class gets its own + # tag: its pull-through rate is the number that says whether edit-time + # record-sync actually happens. by_arm: dict[str, list[int]] = {} if sync_note_ids: by_arm["write_path_sync"] = list(sync_note_ids) for marker, item in menu: - # A rendered repeat is not a new surfacing (#4101) — same cut the log - # row takes, so this table and `retrieval_logs` keep agreeing about the - # same call (#3668). The semantic arm's rows (`write_path_semantic`) + # A rendered repeat is not a new surfacing — same cut the log row + # takes, so this table and `retrieval_logs` keep agreeing about the + # same call. The semantic arm's rows (`write_path_semantic`) # were written by the pipeline beside its call row, so only the # lookups are booked here. if int(item["id"]) in excluded or not marker.startswith("nearby"): @@ -1813,32 +1656,28 @@ async def build_write_path_hint( user_id=user_id, note_ids=ids, source=arm, project_id=project_id, ) - # ── Standing rules that may apply here (milestone 307) ────────────── + # ── Standing rules that may apply here ─────────────────────────────── # - # A SUGGESTION, not a binding surface, and the distinction is the design - # (D7): a rule BINDS by being tagged to an area the project works in, + # A SUGGESTION, not a binding surface, and the distinction is the design: + # a rule BINDS by being tagged to an area the project works in, # resolved deterministically at enter_project. This arm reaches for # something weaker and still useful — a conditional rule whose trigger # resembles what is being written, noticed at the moment it is relevant # rather than by being resident in every session. # - # EVERY TIER, since #3702 — see the note at RULEHINT_LIMIT. This comment - # used to read "CONDITIONAL ONLY: an always-on rule is already in the - # session, so repeating it here would be noise". That conflated being - # PRESENT in context with being SALIENT at the moment the action is taken, - # and it is the same conflation #3750 corrects one layer up: a rule the + # EVERY TIER — see the note on tiers near RULEHINT_LIMIT: a rule the # session was told about an hour ago is not a rule in front of the reader # now. # - # The arm itself is `retrieval_pipeline.run_rule_arm` (milestone 456): the - # band, the fresh/repeat split, the unconditional call row (#3497) and the - # fresh-only surfacing rows (#3752) are written there once for all three + # The arm itself is `retrieval_pipeline.run_rule_arm`: the band, the + # fresh/repeat split, the unconditional call row and the fresh-only + # surfacing rows are written there once for all three # rule arms. What is decided HERE is only what makes this moment this # moment — the query is the code being written (or the path, for an edit # with no body), and a line says the rule may apply "here". # # Fails open like every other arm: a rule hint must never break a write. - # The direct band, then a rule reached through a linked lesson (#4633), + # The direct band, then a rule reached through a linked lesson, # composed by `_rule_moment` exactly as the other two rule builders are. rule_ids: list[int] = [] shown_rule_ids: list[int] = [] @@ -1858,13 +1697,13 @@ async def build_write_path_hint( lines.extend(result.lines) rule_ids.extend(result.rule_ids) # Every rule line the band showed, repeats included — what the - # soft-link recorder pairs with this response's lessons (#4637). + # soft-link recorder pairs with this response's lessons. shown_rule_ids = result.shown_rule_ids checkpoint = result.checkpoint except Exception: logger.debug("write-path rule arm failed", exc_info=True) - # A lesson and a rule on this one response (#4637): the soft-link + # A lesson and a rule on this one response: the soft-link # recorder counts the pair, keyed on the FILE — every edit to one file is # one situation — and asks about it once the evidence holds. Fails open. shown_lessons = [ @@ -1906,19 +1745,18 @@ async def build_tool_rule_hint( cwd: str = "", seen_ruling_systems: list[int] | None = None, ) -> dict: - """Standing rules that may apply to the ACTION about to be taken (#3476) — + """Standing rules that may apply to the ACTION about to be taken — matched directly, then reached through a linked lesson (`_rule_moment`) — - and the rulings of any area whose files the command names (milestone 444). + and the rulings of any area whose files the command names. - The sibling of the write-path rule arm, and the surface that was missing. - That arm is keyed on `code or path`, so a rule can only be retrieved at the - moment of a code WRITE. Every rule about which tool to reach for — don't - curl the forge, don't stand up a stack, don't run the suite locally, don't - branch — was therefore unreachable at the moment it mattered, and residency - in the always-on preload was the only surface it had. + The sibling of the write-path rule arm. That arm is keyed on `code or + path`, so it retrieves only at the moment of a code WRITE. Every rule about + which tool to reach for — don't curl the forge, don't stand up a stack, + don't run the suite locally, don't branch — arrives here instead, at the + moment it matters. - WHY A MECHANICAL TRIGGER AND NOT AN INSTRUCTION. Note #3089's finding is - that a reflex generates no query: you reach for `curl` confidently, with no + WHY A MECHANICAL TRIGGER AND NOT AN INSTRUCTION. A reflex generates no + query: you reach for `curl` confidently, with no moment of doubt, so any surface that waits to be asked never fires. Here nothing has to be asked — the tool call IS the query, and the reflex has to become a tool call before it can do anything. @@ -1927,7 +1765,7 @@ async def build_tool_rule_hint( which tools it watches, so widening the matcher is a `hooks.json` edit with no change here. - EVERY TIER, since #3702 — present in context and salient at the moment are + EVERY TIER — present in context and salient at the moment are different properties, and only the second is what this arm is for. `root` and `cwd` are the repo's absolute root and the command's working @@ -1999,11 +1837,11 @@ async def build_tool_rule_hint( return out -# --- the design-guidance arm (#4256) ---------------------------------------- -# A design system BINDS like a rule, and until this it had one channel: the -# session-start block, which names it and the call that reads its prose. That -# is complete for a session that knows to ask and silent for one that is -# writing a component — the same gap every unasked arm exists to close. +# --- the design-guidance arm ------------------------------------------------ +# A design system BINDS like a rule. The session-start block names it and the +# call that reads its prose, which is complete for a session that knows to ask +# and silent for one that is writing a component — the gap every unasked arm +# exists to close. # # A TRIGGER, NOT A SEARCH. A project has exactly one design system # (projects.design_system_id is a single FK), so there is nothing to rank and @@ -2100,7 +1938,7 @@ async def _design_arm( def _derive_line(path: str, derive: list[dict]) -> str: - """The ledger's word on the names being written (#2900): a duplicate + """The ledger's word on the names being written: a duplicate family to derive, or a canon to reuse — said at the write.""" parts = [] for d in derive: @@ -2121,10 +1959,10 @@ def _derive_line(path: str, derive: list[dict]) -> str: what = f"is a duplicate family with no canon — identical body in {n} other file(s)" else: # A name family: the same definition name living in several - # files. CSS is only ever grouped this way (note 2917) — a class + # files. CSS is only ever grouped this way — a class # is a recipe, and the recipe is what gets derived or dismissed. what = f"is a repeated name with no canon — defined in {n} other file(s)" - # What renders a css family (milestone 302): the consumer count is + # What renders a css family: the consumer count is # the datum that separates a shared recipe from a scoped convention. cons = f.get("consumers") if cons is not None: @@ -2151,7 +1989,7 @@ def _derive_line(path: str, derive: list[dict]) -> str: def _divergence_line(path: str, divergence: list[dict]) -> str: - """Button B where button A is canon — named at the write (#2793).""" + """Button B where button A is canon — named at the write.""" parts = [ f"`{('.' if d['kind'] == 'css' else '') + d['symbol']}` → #{d['canon_snippet_id']} " f"({d['instances']} of {d['judged']} judged siblings are its instances)" @@ -2168,8 +2006,8 @@ def _divergence_line(path: str, divergence: list[dict]) -> str: def _suggest_line(path: str, suggested: list[dict]) -> str: """One line naming the recorded shape this code looks like, as evidence - for the judgment the agent gives at the end of the turn (milestone 439) - — not a record of one already made.""" + for the judgment the agent gives at the end of the turn — not a record of + one already made.""" by_snippet: dict[int, list[str]] = {} for row in suggested: label = f".{row['symbol']}" if row["kind"] == "css" else row["symbol"] @@ -2191,8 +2029,8 @@ def _suggest_line(path: str, suggested: list[dict]) -> str: def _goal_line(goal: str, project_id: int) -> str: """The Goal line, trimmed at a word break with a visible cut. - A raw slice ended mid-word with nothing to say more existed, so a reader - took half a sentence for the whole goal (#4036). + A raw slice would end mid-word with nothing to say more exists, and a + reader would take half a sentence for the whole goal. """ if not goal: return "" @@ -2216,32 +2054,24 @@ async def build_session_context( project_id: the resolved active project (0 = none). The endpoint resolves this from the working repo's remote, or from a `.scribe` marker file naming the project directly — the only key a session - outside a git repo has (#4085). A non-zero id that does not + outside a git repo has. A non-zero id that does not resolve is reported rather than silently dropped. unbound_repo: when the hook sent a repo remote that maps to no project, its normalized key — triggers a one-line "bind this repo" hint so the binding is self-healing. source / session_id: the host's SessionStart `source` and the session's id, when the adapter sends them. They decide what the - claim section says (milestone 381 step 3, `task_claims. - render_claims`): a compaction gets back the work it had claimed + claim section says (`task_claims.render_claims`): a compaction gets back the work it had claimed with its latest logs; a new session hears about other sessions' live and abandoned claims; a resume hears nothing. Returns {"context": str, "project": dict | None}. - It carried `rule_count` and `rules_etag` until milestone 394, when the - preload it described was removed. The etag let the hook hand a marker back - on each write so the server could say whether the resident rules had - moved; nothing is resident now, so nothing can have moved, and a rule is - re-retrieved at the moment it applies rather than held and aged. `context` is markdown an adapter can drop into its session verbatim; it is capped at _MAX_CHARS with an explicit truncation note. - LIVE STATE ONLY (decision #4027, milestone 410). This used to open with the - rules reflex and close with a recall reflex — a fifth copy of guidance the - using-scribe skill owns, arriving in every session beside the other four. - It now says only what the server alone knows about THIS session: the + LIVE STATE ONLY. It says only what the server alone knows about THIS + session: the active project, its open work, its design system, or that the working repo is unbound. How to work with Scribe is the skill's to say, and it says it once. @@ -2264,9 +2094,9 @@ async def build_session_context( f"Open todo tasks: {open_count}", ] - # A design system binds the same way a rule does, and until this - # existed it had no push channel — the standards were reachable only - # by an agent that already knew to look for them. Summary only: the + # A design system binds the same way a rule does, and without this + # the standards would reach only an agent that already knew to look + # for them. Summary only: the # token VALUES are a tool call away, and pasting a hundred of them # into every session would crowd out the context they inform. if project.design_system_id: @@ -2304,12 +2134,9 @@ async def build_session_context( except Exception: # noqa: BLE001 - context is best-effort logger.warning("claim section skipped", exc_info=True) - # Nothing loaded — say which nothing (#4085). This used to hang off the - # `if project_id:` above as an `elif`, which meant an id that was SENT and - # did not resolve produced no message at all: the outer branch was taken, - # the inner one was not, and the caller got a context that simply omitted - # the project it had asked for. That is the one case worth being loudest - # about, because the caller is holding a pointer it believes in. + # Nothing loaded — say which nothing. An id that was SENT and did not + # resolve is the case worth being loudest about, because the caller is + # holding a pointer it believes in. if project_dict is None: if project_id: lines += [