docs(6209): rulebooks, pull telemetry, lesson usage, duplicate arm, cited status, trigger shape
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m3s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 13s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m3s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 13s
Six more test files state their pinned properties without the issue and milestone history: the pull-telemetry guard explains enumerating kinds instead of listing two getter incidents, cited-record status explains why a hint's id needs its status instead of recounting one misreported step, and the trigger-shape guard keeps its reasoning about why prose heuristics fail while dropping the dated corpus audit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,12 @@
|
|||||||
"""A record you merely CITE carries its status (#4154, milestone 409 step 8).
|
"""A record you merely CITE carries its status.
|
||||||
|
|
||||||
WHY THIS EXISTS
|
WHY THIS EXISTS
|
||||||
|
|
||||||
Step 1 (#4010) made placement cheap for a task whose status CHANGES: the write
|
`placement` is cheap for a task whose status CHANGES: the write returns where
|
||||||
returns where it sits, and the report is written from that. It does nothing for
|
it sits, and the report is written from that. It does nothing for a task a
|
||||||
a task a reply only mentions. In this milestone's own step-6 review the session
|
reply only mentions. An id taken from a retrieval hint carries an id, a kind
|
||||||
reported "#4014 is the open step of milestone 409". #4014 had been done for
|
and a title and says nothing about status, and a count like "8 of 9" does not
|
||||||
four days; the open step was #4015. The id did not come from a read — it came
|
say which one is open — so a finished step can be reported as the open one.
|
||||||
from a retrieval hint, which carries an id, a kind and a title and says nothing
|
|
||||||
about status, while `list_milestones` said "8 of 9" and would not say which one.
|
|
||||||
|
|
||||||
Two surfaces, one principle: the status arrives with the id.
|
Two surfaces, one principle: the status arrives with the id.
|
||||||
|
|
||||||
@@ -23,7 +21,7 @@ THE ONE THAT MATTERS MOST
|
|||||||
answer "what is next" and they must not drift. It is written as a behavioural
|
answer "what is next" and they must not drift. It is written as a behavioural
|
||||||
cross-check rather than `assert milestones.OPEN_STEP_STATUSES is placement._OPEN`,
|
cross-check rather than `assert milestones.OPEN_STEP_STATUSES is placement._OPEN`,
|
||||||
which shares one object today and would therefore pass no matter what either
|
which shares one object today and would therefore pass no matter what either
|
||||||
side did with it (rule 167: a guard has to be able to fail). This one fails if
|
side did with it — a guard has to be able to fail. This one fails if
|
||||||
either side changes its ordering or its notion of "open" alone.
|
either side changes its ordering or its notion of "open" alone.
|
||||||
"""
|
"""
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
@@ -164,7 +162,7 @@ async def test_the_earliest_open_step_wins_not_the_earliest_step():
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_the_batch_does_not_fan_out_per_milestone():
|
async def test_the_batch_does_not_fan_out_per_milestone():
|
||||||
"""#2384's shape must not come back through the new query.
|
"""The step query must not fan out per milestone.
|
||||||
|
|
||||||
Three queries for one milestone and three for forty — the cost is in the
|
Three queries for one milestone and three for forty — the cost is in the
|
||||||
number of QUERIES, not the number of plans. A per-milestone "what's next"
|
number of QUERIES, not the number of plans. A per-milestone "what's next"
|
||||||
@@ -182,7 +180,7 @@ async def test_the_batch_does_not_fan_out_per_milestone():
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_both_step_queries_take_the_same_visibility_clause():
|
async def test_both_step_queries_take_the_same_visibility_clause():
|
||||||
"""Rule 78, and a correctness point on top of it.
|
"""Access control, and a correctness point on top of it.
|
||||||
|
|
||||||
`next_step` names a step; the counts beside it say how many there are. Read
|
`next_step` names a step; the counts beside it say how many there are. Read
|
||||||
through different clauses, a row could name a step its own progress numbers
|
through different clauses, a row could name a step its own progress numbers
|
||||||
@@ -265,7 +263,7 @@ def test_a_record_with_no_status_gains_no_parenthesis():
|
|||||||
|
|
||||||
|
|
||||||
def test_the_listing_tools_say_what_next_step_is_and_how_to_use_it():
|
def test_the_listing_tools_say_what_next_step_is_and_how_to_use_it():
|
||||||
"""The contract every MCP client reads (rule 119, decision #4027).
|
"""The contract every MCP client reads.
|
||||||
|
|
||||||
The field is only worth adding if a caller knows it is there. Without this
|
The field is only worth adding if a caller knows it is there. Without this
|
||||||
the docstring can be tidied to a parameter list and the one surface that
|
the docstring can be tidied to a parameter list and the one surface that
|
||||||
@@ -290,7 +288,7 @@ def test_the_reply_skill_says_a_cited_record_still_gets_read():
|
|||||||
|
|
||||||
Presence, not absence — the skill legitimately discusses recall in order to
|
Presence, not absence — the skill legitimately discusses recall in order to
|
||||||
warn against it, so an absence check here would be satisfied by the warning
|
warn against it, so an absence check here would be satisfied by the warning
|
||||||
itself (snippet #3352).
|
itself.
|
||||||
"""
|
"""
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""The by-name duplicate arm confirms its grep hits against the real extractor (#4227).
|
"""The by-name duplicate arm confirms its grep hits against the real extractor.
|
||||||
|
|
||||||
#4222 fixed one end of this defect: the extractors that decide what a payload
|
The extractors that decide what a payload DEFINES blank comment and string
|
||||||
DEFINES now blank comment and string spans before any line matcher runs. This
|
spans before any line matcher runs. This is the other end — `scribe_local_dups`,
|
||||||
is the other end — `scribe_local_dups`, which decides which OTHER files already
|
which decides which OTHER files already define that name — and a plain
|
||||||
define that name — and it was a plain `git grep`.
|
`git grep` cannot do that.
|
||||||
|
|
||||||
A grep sees lines, not spans. The line
|
A grep sees lines, not spans. The line
|
||||||
|
|
||||||
@@ -13,18 +13,16 @@ is prose from a module docstring in this repo, and it matches the arm's pattern
|
|||||||
for `name=with`. So the arm would tell a writer their genuine symbol was
|
for `name=with`. So the arm would tell a writer their genuine symbol was
|
||||||
already defined, and point at a docstring.
|
already defined, and point at a docstring.
|
||||||
|
|
||||||
WHAT THE FIX COSTS, measured rather than guessed, over 141 payload files of
|
WHAT CONFIRMATION COSTS, measured rather than guessed: it drops string
|
||||||
this repo: the arm removed 15 of 210 report lines, and every one was a string
|
literals, comments, import statements and Vue's `const emit = defineEmits()`
|
||||||
literal, a comment, an import statement, or Vue's `const emit = defineEmits()`
|
boilerplate, and no real definition — where a name has both a real definition
|
||||||
boilerplate. No real definition was lost — where a name had both a real
|
and a phantom, the phantom goes and the definition stays. The arm is
|
||||||
definition and a phantom, the phantom was dropped and the definition kept.
|
dominated by its `git grep` calls, so confirming adds about 15% (a mean of
|
||||||
Timing: mean 193ms → 222ms, worst 569ms → 572ms. The arm was already dominated
|
~30ms) rather than the second a per-(name, file) shape would cost.
|
||||||
by its twelve `git grep` calls, so confirming costs about 15% rather than the
|
|
||||||
second a per-(name, file) shape would have cost.
|
|
||||||
|
|
||||||
These cases are written against real git repositories rather than mocks,
|
These cases are written against real git repositories rather than mocks,
|
||||||
because what is being pinned is the interaction between `git grep`, `head` and
|
because what is being pinned is the interaction between `git grep`, `head` and
|
||||||
the extractor — which is where #4042 lived too.
|
the extractor — which is also where a `pipefail` + `head` interaction lives.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -77,7 +75,7 @@ def repo(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_a_sentence_about_a_definition_is_not_a_definition(repo):
|
def test_a_sentence_about_a_definition_is_not_a_definition(repo):
|
||||||
"""The verify case from #4227: one real `def change`, one docstring that
|
"""One real `def change`, one docstring that
|
||||||
wraps onto a line beginning `function change`. The writer is told about
|
wraps onto a line beginning `function change`. The writer is told about
|
||||||
the first file and not the second."""
|
the first file and not the second."""
|
||||||
build, run = repo
|
build, run = repo
|
||||||
@@ -162,15 +160,14 @@ def test_an_import_is_not_a_definition(repo):
|
|||||||
assert "card.vue" not in out, "an import site is not a definition site"
|
assert "card.vue" not in out, "an import site is not a definition site"
|
||||||
|
|
||||||
|
|
||||||
# ── the caps, which is where #4042 lived ─────────────────────────────────────
|
# ── the caps ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def test_the_candidate_cap_is_raised_above_the_display_cap():
|
def test_the_candidate_cap_is_raised_above_the_display_cap():
|
||||||
"""Confirmation REMOVES hits, so a cap applied before it runs lets phantom
|
"""Confirmation REMOVES hits, so a cap applied before it runs lets phantom
|
||||||
matches crowd real definitions out of the window — hits dropped before
|
matches crowd real definitions out of the window — hits dropped before
|
||||||
anyone looked at them, which is #4042's bug in a new place. Asserted on
|
anyone looked at them. Asserted on structure because the ordering is what
|
||||||
structure (rule 167) because the ordering is what matters, not the
|
matters, not the numbers."""
|
||||||
numbers."""
|
|
||||||
src = DEFS.read_text()
|
src = DEFS.read_text()
|
||||||
for const in ("_SCRIBE_DUP_CANDIDATES=", "_SCRIBE_DUP_SHOWN="):
|
for const in ("_SCRIBE_DUP_CANDIDATES=", "_SCRIBE_DUP_SHOWN="):
|
||||||
assert const in src, f"{const.rstrip(chr(61))} is gone — the two caps were folded back together"
|
assert const in src, f"{const.rstrip(chr(61))} is gone — the two caps were folded back together"
|
||||||
@@ -196,7 +193,7 @@ def test_phantoms_ahead_of_the_display_cap_do_not_hide_a_real_definition(repo):
|
|||||||
|
|
||||||
|
|
||||||
def test_the_early_exiting_head_still_keeps_its_own_output(repo):
|
def test_the_early_exiting_head_still_keeps_its_own_output(repo):
|
||||||
"""#4042, re-run through the new shape: under `pipefail` a `head` that
|
"""Under `pipefail` a `head` that
|
||||||
exits while git grep is still writing must not void the hits it printed.
|
exits while git grep is still writing must not void the hits it printed.
|
||||||
Every file here holds a REAL definition, so confirmation keeps them all
|
Every file here holds a REAL definition, so confirmation keeps them all
|
||||||
and only the display cap applies."""
|
and only the display cap applies."""
|
||||||
@@ -211,7 +208,8 @@ def test_the_early_exiting_head_still_keeps_its_own_output(repo):
|
|||||||
|
|
||||||
|
|
||||||
def test_a_selector_named_inside_a_css_comment_is_not_a_rule(repo):
|
def test_a_selector_named_inside_a_css_comment_is_not_a_rule(repo):
|
||||||
"""#2990's defect, on the lookup side rather than the extraction side."""
|
"""A selector inside a CSS comment, on the lookup side rather than the
|
||||||
|
extraction side."""
|
||||||
build, run = repo
|
build, run = repo
|
||||||
build({
|
build({
|
||||||
"real.css": ".badge { color: red; }\n",
|
"real.css": ".badge { color: red; }\n",
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
"""A lesson's surfaced-vs-opened counts reach a reader (#4196).
|
"""A lesson's surfaced-vs-opened counts reach a reader.
|
||||||
|
|
||||||
The lesson slot has recorded usage since it shipped, and until this nothing
|
Every door — the REST list and detail, both MCP doors, and the view — carries
|
||||||
showed it. `get_lesson`'s REST door attached it; the list did not, neither MCP
|
the counts, because the one kind whose central question is "is this trigger
|
||||||
door did, and no view rendered it — so the one kind whose central question is
|
firing on the right situation" needs that answer readable.
|
||||||
"is this trigger firing on the right situation" was the one kind whose answer
|
|
||||||
was unreadable.
|
|
||||||
|
|
||||||
WHY THE COUNTS MATTER MORE HERE THAN ON A SNIPPET. #4196 asks whether a lesson
|
WHY THE COUNTS MATTER MORE HERE THAN ON A SNIPPET. Whether a lesson repeatedly
|
||||||
repeatedly followed should become a rule, and names the trap: raw frequency
|
followed should become a rule has a trap: raw frequency
|
||||||
cannot tell "this should bind" from "this trigger is too broad", which is the
|
cannot tell "this should bind" from "this trigger is too broad", which is the
|
||||||
commoner reading. Surfaced-AND-opened can. Neither question is answerable by
|
commoner reading. Surfaced-AND-opened can. Neither question is answerable by
|
||||||
anything that cannot see the numbers, which is why this is the first step and
|
anything that cannot see the numbers, which is why this is the first step and
|
||||||
not the threshold.
|
not the threshold.
|
||||||
|
|
||||||
NO THRESHOLD IS ASSERTED ANYWHERE HERE, deliberately. At the time of writing
|
NO THRESHOLD IS ASSERTED ANYWHERE HERE, deliberately. A promotion rule fitted
|
||||||
the corpus is 10 lessons with 7 recorded surfacings and 3 opens; a promotion
|
to a small corpus would be fitting noise, and `UsageBadge` already declines
|
||||||
rule fitted to that would be fitting noise, and `UsageBadge` already declines
|
|
||||||
to render a verdict on fewer than three surfacings for the same reason.
|
to render a verdict on fewer than three surfacings for the same reason.
|
||||||
"""
|
"""
|
||||||
import inspect
|
import inspect
|
||||||
@@ -115,11 +112,10 @@ async def test_get_lesson_reads_the_count_before_recording_its_own_pull():
|
|||||||
assert out["usage"]["pull_count"] == 1
|
assert out["usage"]["pull_count"] == 1
|
||||||
|
|
||||||
|
|
||||||
# ── the REST door, on structure (rule 167) ───────────────────────────────────
|
# ── the REST door, on structure ──────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# MOVED 2026-09-21 (#4230), not weakened. These used to look for
|
# `usage_for_notes(` and `empty_usage()` live in ONE seam,
|
||||||
# `usage_for_notes(` and `empty_usage()` in the route body. Both now live in
|
# `note_usage.attach_usage`, which seven doors share — so the
|
||||||
# ONE seam, `note_usage.attach_usage`, which seven doors share — so the
|
|
||||||
# zero-fill and the single aggregate are pinned once, against the seam, in
|
# zero-fill and the single aggregate are pinned once, against the seam, in
|
||||||
# tests/test_usage_attach_seam.py. What stays HERE is what only this route can
|
# tests/test_usage_attach_seam.py. What stays HERE is what only this route can
|
||||||
# get wrong: that it calls the seam at all, once, and before it records a pull.
|
# get wrong: that it calls the seam at all, once, and before it records a pull.
|
||||||
@@ -166,7 +162,7 @@ def test_the_rest_detail_door_also_reads_before_it_records():
|
|||||||
|
|
||||||
|
|
||||||
def test_these_guards_can_fail():
|
def test_these_guards_can_fail():
|
||||||
"""Rule 167: falsify the shape they assert against, so a guard that has
|
"""Falsify the shape they assert against, so a guard that has
|
||||||
quietly stopped describing anything cannot pass by describing nothing."""
|
quietly stopped describing anything cannot pass by describing nothing."""
|
||||||
src = _route_source("list_lessons_route")
|
src = _route_source("list_lessons_route")
|
||||||
assert "def list_lessons_route" in src
|
assert "def list_lessons_route" in src
|
||||||
@@ -174,21 +170,21 @@ def test_these_guards_can_fail():
|
|||||||
|
|
||||||
|
|
||||||
def test_the_detail_view_renders_the_badge_rather_than_respelling_it():
|
def test_the_detail_view_renders_the_badge_rather_than_respelling_it():
|
||||||
"""Snippet #3460: reach for UsageBadge and pass the kind's own advice; a
|
"""Reach for UsageBadge and pass the kind's own advice; a view's scoped
|
||||||
view's scoped re-spelling of `.usage-tag` is what that component replaced.
|
re-spelling of `.usage-tag` is what that component exists to prevent.
|
||||||
|
|
||||||
The advice is the kind-specific half. For a lesson it points at the
|
The advice is the kind-specific half. For a lesson it points at the
|
||||||
trigger, not at deletion — a lesson nobody opens is usually keyed to a
|
trigger, not at deletion — a lesson nobody opens is usually keyed to a
|
||||||
situation nobody is in, which is `update_lesson`'s own words and the
|
situation nobody is in, which is `update_lesson`'s own words and the
|
||||||
reading #4196 calls the commoner one.
|
commoner reading.
|
||||||
"""
|
"""
|
||||||
view = (Path(__file__).resolve().parents[1]
|
view = (Path(__file__).resolve().parents[1]
|
||||||
/ "frontend/src/views/LessonDetailView.vue").read_text()
|
/ "frontend/src/views/LessonDetailView.vue").read_text()
|
||||||
assert "UsageBadge" in view
|
assert "UsageBadge" in view
|
||||||
assert "usage-tag" not in view, "re-spelled the chip instead of reusing it"
|
assert "usage-tag" not in view, "re-spelled the chip instead of reusing it"
|
||||||
|
|
||||||
# The sentence moved out of this template into the shared table (#4230,
|
# The sentence lives in the shared table, not this template. Assert BOTH
|
||||||
# recorded on #3460). Assert BOTH halves, so the guard cannot pass by the
|
# halves, so the guard cannot pass by the
|
||||||
# view pointing at an entry that no longer says the right thing.
|
# view pointing at an entry that no longer says the right thing.
|
||||||
assert "DEAD_WEIGHT_ADVICE.lesson" in view, "the view no longer reads the lesson advice"
|
assert "DEAD_WEIGHT_ADVICE.lesson" in view, "the view no longer reads the lesson advice"
|
||||||
table = (Path(__file__).resolve().parents[1]
|
table = (Path(__file__).resolve().parents[1]
|
||||||
|
|||||||
@@ -11,19 +11,12 @@ surfaced:pulled ratio is what makes dead weight visible and prunable. A getter
|
|||||||
that opens a record without recording it leaves that kind permanently at zero
|
that opens a record without recording it leaves that kind permanently at zero
|
||||||
pulls, so it looks like dead weight beside kinds that merely had a counter.
|
pulls, so it looks like dead weight beside kinds that merely had a counter.
|
||||||
|
|
||||||
That has now happened twice:
|
A fix that enumerates the kinds someone thought of, rather than the kinds that
|
||||||
|
exist, leaves the next kind's getter silent — and the REST detail views are a
|
||||||
#2245 `get_task` recorded nothing while auto-inject surfaced mostly tasks.
|
second half that is easy to forget.
|
||||||
Fixed by adding the call to notes, tasks and snippets.
|
|
||||||
#2476 `get_process` recorded nothing — and the auto-inject menu header names
|
|
||||||
`get_process` as the way to open that kind. Processes were embedded
|
|
||||||
when #2245 was fixed; the fix enumerated the kinds someone thought of
|
|
||||||
rather than the kinds that exist. The same issue found the REST half:
|
|
||||||
`rest_snippet` was recorded, note and task detail were not, and the
|
|
||||||
model's own comment named a `'rest_note'` source nothing wrote.
|
|
||||||
|
|
||||||
A missing call is the shape no behavioural test catches: it changes no return
|
A missing call is the shape no behavioural test catches: it changes no return
|
||||||
value (#2278, shape 4). Source inspection is the only thing that sees it.
|
value. Source inspection is the only thing that sees it.
|
||||||
|
|
||||||
WHAT MAKES THIS DERIVED RATHER THAN A LIST
|
WHAT MAKES THIS DERIVED RATHER THAN A LIST
|
||||||
|
|
||||||
@@ -203,14 +196,14 @@ def test_every_named_loader_still_exists():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── the reading project travels with the event (#4196, #3735) ────────────────
|
# ── the reading project travels with the event ───────────────────────────────
|
||||||
#
|
#
|
||||||
# The same sibling-drift shape as above, one dimension over. `RetrievalLog` has
|
# The same sibling-drift shape as above, one dimension over. `RetrievalLog`
|
||||||
# always carried `project_id`, so "surfaced on project B" was answerable; the
|
# carries `project_id`, so "surfaced on project B" is answerable; without it on
|
||||||
# usage table had none, so "opened on project B" was not, and the two cannot be
|
# the usage row, "opened on project B" is not, and the two cannot be joined (no
|
||||||
# joined (no session identity server-side — NoteUsageEvent's own docstring).
|
# session identity server-side — NoteUsageEvent's own docstring). That is
|
||||||
# That gap sat exactly on milestone 385's acceptance: a lesson's claim is that
|
# exactly a lesson's claim: it reaches a session on a project it was NOT
|
||||||
# it reaches a session on a project it was NOT written on.
|
# written on.
|
||||||
#
|
#
|
||||||
# A getter that takes the project and forgets to pass it fails the same way a
|
# A getter that takes the project and forgets to pass it fails the same way a
|
||||||
# missing record_pulled did: silently, changing no return value, and showing up
|
# missing record_pulled did: silently, changing no return value, and showing up
|
||||||
@@ -246,7 +239,7 @@ def _takes_reading_project(body: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def test_every_getter_that_pulls_takes_the_reading_project():
|
def test_every_getter_that_pulls_takes_the_reading_project():
|
||||||
"""Asserted on structure (rule 167): a behavioural test cannot see a
|
"""Asserted on structure: a behavioural test cannot see a
|
||||||
parameter that was never threaded through."""
|
parameter that was never threaded through."""
|
||||||
missing = [
|
missing = [
|
||||||
f"{module}.{name}"
|
f"{module}.{name}"
|
||||||
@@ -277,7 +270,7 @@ def test_every_getter_that_pulls_passes_the_project_through():
|
|||||||
|
|
||||||
# The arms are the other half: unlike a getter, a surfacing arm ALWAYS knows
|
# The arms are the other half: unlike a getter, a surfacing arm ALWAYS knows
|
||||||
# the project — it is the scope it just searched — so there is no excuse for a
|
# the project — it is the scope it just searched — so there is no excuse for a
|
||||||
# null there, and `lesson_slot` is the one milestone 385 is measured through.
|
# null there, and `lesson_slot` is the one a lesson's transfer is measured through.
|
||||||
#
|
#
|
||||||
# `process_skill_sync` is exempt and named here rather than pattern-matched: it
|
# `process_skill_sync` is exempt and named here rather than pattern-matched: it
|
||||||
# installs every Process the operator can reach, which is not a project-scoped
|
# installs every Process the operator can reach, which is not a project-scoped
|
||||||
@@ -310,7 +303,7 @@ def test_every_project_scoped_surfacing_arm_records_its_project():
|
|||||||
|
|
||||||
|
|
||||||
def test_the_reading_project_guards_can_fail():
|
def test_the_reading_project_guards_can_fail():
|
||||||
"""Rule 167: shown turning red once, so a guard that has quietly stopped
|
"""Shown turning red once, so a guard that has quietly stopped
|
||||||
matching anything is distinguishable from one that passes."""
|
matching anything is distinguishable from one that passes."""
|
||||||
assert list(_pulling_getters()), "the pulling-getter detection matches nothing"
|
assert list(_pulling_getters()), "the pulling-getter detection matches nothing"
|
||||||
fake = "async def get_thing(thing_id: int) -> dict:\n record_pulled(x)"
|
fake = "async def get_thing(thing_id: int) -> dict:\n record_pulled(x)"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from tests.helpers import fake_rule, fake_rulebook, fake_topic, make_mock_sessio
|
|||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_exclusions():
|
def _no_exclusions():
|
||||||
"""get_applicable_rules asks for the project's always-on exclusions
|
"""get_applicable_rules asks for the project's always-on exclusions
|
||||||
(milestone 297) through its own session; these mocked-session tests
|
through its own session; these mocked-session tests
|
||||||
script the rule queries only, so the exclusions lookup is stubbed empty."""
|
script the rule queries only, so the exclusions lookup is stubbed empty."""
|
||||||
if True:
|
if True:
|
||||||
yield
|
yield
|
||||||
@@ -212,12 +212,12 @@ def _areas(*canonical_ids):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_applicable_rules_returns_shape():
|
async def test_get_applicable_rules_returns_shape():
|
||||||
"""The listing is a project's own rules plus the global rules tagged to its
|
"""The listing is a project's own rules plus the global rules tagged to its
|
||||||
areas (milestone 414) — no subscriptions or suppressions in the shape."""
|
areas — no subscriptions or suppressions in the shape."""
|
||||||
mock_session = make_mock_session()
|
mock_session = make_mock_session()
|
||||||
rules_result = MagicMock()
|
rules_result = MagicMock()
|
||||||
rules_result.all.return_value = [
|
rules_result.all.return_value = [
|
||||||
# The query selects the ENTITY plus three labels, so rule_brief stays
|
# The query selects the ENTITY plus three labels, so rule_brief stays
|
||||||
# the one place deciding what a surfaced rule carries (note 3026).
|
# the one place deciding what a surfaced rule carries.
|
||||||
(fake_rule(id=i, title=f"Rule {i}", statement=f"Statement {i}", topic_id=2),
|
(fake_rule(id=i, title=f"Rule {i}", statement=f"Statement {i}", topic_id=2),
|
||||||
"git-workflow", 1, "FabledSword family")
|
"git-workflow", 1, "FabledSword family")
|
||||||
for i in range(50)
|
for i in range(50)
|
||||||
@@ -330,7 +330,7 @@ async def test_a_co_surfaces_partner_on_another_project_is_not_dragged_in():
|
|||||||
assert [r["id"] for r in result["rules"]] == [200, 201]
|
assert [r["id"] for r in result["rules"]] == [200, 201]
|
||||||
|
|
||||||
|
|
||||||
# ── rule_brief (milestone 307) ──────────────────────────────────────────
|
# ── rule_brief ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_rule_brief_carries_age_but_not_the_deep_fields():
|
def test_rule_brief_carries_age_but_not_the_deep_fields():
|
||||||
"""The shape a SURFACED rule takes, and the reason it exists.
|
"""The shape a SURFACED rule takes, and the reason it exists.
|
||||||
@@ -338,7 +338,7 @@ def test_rule_brief_carries_age_but_not_the_deep_fields():
|
|||||||
There were three hand-written copies of this dict and they had already
|
There were three hand-written copies of this dict and they had already
|
||||||
diverged — none carried the timestamps the model has always held, which is
|
diverged — none carried the timestamps the model has always held, which is
|
||||||
why a rule written before the capability it duplicates was
|
why a rule written before the capability it duplicates was
|
||||||
indistinguishable at read time from one still doing work (note 3026).
|
indistinguishable at read time from one still doing work.
|
||||||
"""
|
"""
|
||||||
from scribe.services.rulebooks import rule_brief
|
from scribe.services.rulebooks import rule_brief
|
||||||
|
|
||||||
@@ -356,7 +356,7 @@ def test_rule_brief_carries_age_but_not_the_deep_fields():
|
|||||||
|
|
||||||
|
|
||||||
def test_rule_brief_omits_keys_a_rule_has_no_value_for():
|
def test_rule_brief_omits_keys_a_rule_has_no_value_for():
|
||||||
"""#2483: a null key reads as a capability the record has and isn't using,
|
"""A null key reads as a capability the record has and isn't using,
|
||||||
which is a different claim from not having one."""
|
which is a different claim from not having one."""
|
||||||
from scribe.services.rulebooks import rule_brief
|
from scribe.services.rulebooks import rule_brief
|
||||||
|
|
||||||
@@ -367,7 +367,7 @@ def test_rule_brief_omits_keys_a_rule_has_no_value_for():
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ── verify_with / expires_when (milestone 312) ──────────────────────────
|
# ── verify_with / expires_when ──────────────────────────────────────────
|
||||||
|
|
||||||
def test_a_rule_with_no_check_says_nothing_about_verification():
|
def test_a_rule_with_no_check_says_nothing_about_verification():
|
||||||
"""The empty case is the COMMON case, and it must stay silent.
|
"""The empty case is the COMMON case, and it must stay silent.
|
||||||
@@ -385,7 +385,7 @@ def test_a_rule_with_no_check_says_nothing_about_verification():
|
|||||||
|
|
||||||
|
|
||||||
def test_an_unverified_constraint_reads_never_rather_than_null():
|
def test_an_unverified_constraint_reads_never_rather_than_null():
|
||||||
"""#2483 again: a null key reads as a capability going unused. "never" is
|
"""A null key reads as a capability going unused. "never" is
|
||||||
a different and much stronger claim — this rule asserts a fact about
|
a different and much stronger claim — this rule asserts a fact about
|
||||||
someone else's software and nobody has ever confirmed it."""
|
someone else's software and nobody has ever confirmed it."""
|
||||||
from scribe.services.rulebooks import last_verified_label, rule_brief
|
from scribe.services.rulebooks import last_verified_label, rule_brief
|
||||||
@@ -421,7 +421,7 @@ def test_the_check_text_itself_never_enters_a_listing():
|
|||||||
assert "expires_when" not in out
|
assert "expires_when" not in out
|
||||||
|
|
||||||
|
|
||||||
# ── the sweep's row shape (milestone 312 step 3) ────────────────────────
|
# ── the sweep's row shape ───────────────────────────────────────────────
|
||||||
|
|
||||||
def test_a_sweep_row_carries_the_check_in_full():
|
def test_a_sweep_row_carries_the_check_in_full():
|
||||||
"""The OPPOSITE call from rule_brief, and deliberately so.
|
"""The OPPOSITE call from rule_brief, and deliberately so.
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
"""Every rule-write surface SHOWS what a trigger looks like (#3855).
|
"""Every rule-write surface SHOWS what a trigger looks like.
|
||||||
|
|
||||||
WHY THIS EXISTS
|
WHY THIS EXISTS
|
||||||
|
|
||||||
`when_to_apply` is not documentation. `rule_document()` uses it twice — as
|
`when_to_apply` is not documentation. `rule_document()` uses it twice — as
|
||||||
the embedded title's second half and again above the body — so it dominates
|
the embedded title's second half and again above the body — so it dominates
|
||||||
the vector, and a rule whose trigger names a CATEGORY rather than a moment
|
the vector, and a rule whose trigger names a CATEGORY rather than a moment
|
||||||
collapses toward its title and never arrives. Measured in #3835 across 113
|
collapses toward its title and never arrives.
|
||||||
rules, then again in #3855 on eight preferences.
|
|
||||||
|
|
||||||
The corpus damage traced to an uneven contract rather than to careless
|
Bad triggers come from an uneven contract rather than from careless authors.
|
||||||
authors. `create_rule` had carried the full argument since 2026-08-27, and
|
Triggers written where the guidance exists name a moment; triggers written
|
||||||
the two preferences written that day got good triggers; the six written
|
without it — "during hard debugging", "when reading any request from the
|
||||||
before it — "during hard debugging", "when reading any request from the
|
operator" — name categories. The guidance has to exist on the `update_*`
|
||||||
operator" — got categories. The guidance worked wherever it existed. It
|
surfaces too, which is where every
|
||||||
simply did not exist on either `update_*` surface, which is where every
|
|
||||||
RETROFITTED trigger is written, and retrofitting is most of the work: a
|
RETROFITTED trigger is written, and retrofitting is most of the work: a
|
||||||
trigger that already reads fine as English is the one nobody rewrites.
|
trigger that already reads fine as English is the one nobody rewrites.
|
||||||
|
|
||||||
@@ -21,9 +19,8 @@ WHAT THIS PINS
|
|||||||
|
|
||||||
Two properties, neither of them wording:
|
Two properties, neither of them wording:
|
||||||
|
|
||||||
1. A tool accepting `when_to_apply` mentions it. Exactly what
|
1. A tool accepting `when_to_apply` mentions it — a parameter the
|
||||||
`update_preference` failed before #3855 — the parameter existed and
|
docstring never names is one nobody fills well.
|
||||||
the docstring never said so.
|
|
||||||
2. It carries a worked CONTRAST: one `RETRIEVES:` example and one
|
2. It carries a worked CONTRAST: one `RETRIEVES:` example and one
|
||||||
`COLLAPSES:` example, quoted, and different from each other.
|
`COLLAPSES:` example, quoted, and different from each other.
|
||||||
|
|
||||||
@@ -31,23 +28,21 @@ WHY A CONVENTION RATHER THAN A PROSE HEURISTIC
|
|||||||
|
|
||||||
The second property guards the softer regression — guidance kept but
|
The second property guards the softer regression — guidance kept but
|
||||||
abstracted back to "name the moment in session vocabulary", which is advice
|
abstracted back to "name the moment in session vocabulary", which is advice
|
||||||
about being concrete that is not itself concrete, and is the shape that was
|
about being concrete that is not itself concrete.
|
||||||
already on file while the corpus filled with categories.
|
|
||||||
|
|
||||||
Two attempts to detect that in free prose were written and discarded, and
|
Two ways to detect that in free prose fail, and the reason generalises:
|
||||||
the reason is worth keeping because it generalises:
|
|
||||||
|
|
||||||
- Counting quoted multi-word phrases anywhere in the docstring measured
|
- Counting quoted multi-word phrases anywhere in the docstring measured
|
||||||
ambient quotation, not demonstrated triggers. It PASSED the abstracted
|
ambient quotation, not demonstrated triggers. It PASSES the abstracted
|
||||||
version by scoring unrelated prose, and text with an odd number of
|
version by scoring unrelated prose, and text with an odd number of
|
||||||
quote characters produced matches spanning the gap BETWEEN two
|
quote characters produced matches spanning the gap BETWEEN two
|
||||||
unrelated phrases.
|
unrelated phrases.
|
||||||
- Scoping that count to a window after each "trigger"/"when_to_apply"
|
- Scoping that count to a window after each "trigger"/"when_to_apply"
|
||||||
mention then FAILED `create_preference` in its correct state, because
|
mention FAILS `create_preference` in its correct state, because
|
||||||
its examples sit further from the first mention than any defensible
|
its examples sit further from the first mention than any defensible
|
||||||
window reaches.
|
window reaches.
|
||||||
|
|
||||||
Both were proxies trying to infer demonstration from prose. The fix was to
|
Both are proxies trying to infer demonstration from prose. The answer is to
|
||||||
stop inferring: a two-line labelled contrast is unambiguous to parse, free
|
stop inferring: a two-line labelled contrast is unambiguous to parse, free
|
||||||
in its wording, and a better teaching form than the sentences it replaced —
|
in its wording, and a better teaching form than the sentences it replaced —
|
||||||
`RETRIEVES:`/`COLLAPSES:` names the mechanism, so the label does work for
|
`RETRIEVES:`/`COLLAPSES:` names the mechanism, so the label does work for
|
||||||
@@ -56,7 +51,7 @@ measured, changing the shape of the thing is cheaper than a cleverer
|
|||||||
measurement.
|
measurement.
|
||||||
|
|
||||||
What it still cannot do is judge whether a GOOD example is good. It catches
|
What it still cannot do is judge whether a GOOD example is good. It catches
|
||||||
what actually happened: a write surface shipping with the contrast absent,
|
the likely failure: a write surface shipping with the contrast absent,
|
||||||
half-present, or tidied back into abstract advice.
|
half-present, or tidied back into abstract advice.
|
||||||
|
|
||||||
THE SURFACE LIST IS DERIVED, NEVER HAND-KEPT. It comes from what
|
THE SURFACE LIST IS DERIVED, NEVER HAND-KEPT. It comes from what
|
||||||
@@ -64,7 +59,7 @@ THE SURFACE LIST IS DERIVED, NEVER HAND-KEPT. It comes from what
|
|||||||
taking `when_to_apply`, so a rule-write tool added later is in scope on the
|
taking `when_to_apply`, so a rule-write tool added later is in scope on the
|
||||||
day it is added. A hand-kept list has to be remembered by the person least
|
day it is added. A hand-kept list has to be remembered by the person least
|
||||||
likely to know it exists — the argument `RANKED_SOURCES` makes in
|
likely to know it exists — the argument `RANKED_SOURCES` makes in
|
||||||
services/rule_usage.py, and the reason #3855 happened at all.
|
services/rule_usage.py.
|
||||||
"""
|
"""
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
@@ -111,8 +106,8 @@ def _trigger_writers() -> list[str]:
|
|||||||
if "when_to_apply" in inspect.signature(fn).parameters
|
if "when_to_apply" in inspect.signature(fn).parameters
|
||||||
]
|
]
|
||||||
# An empty list would make every case below vacuous and the guard would
|
# An empty list would make every case below vacuous and the guard would
|
||||||
# pass by having nothing to check — absence read as non-existence, the
|
# pass by having nothing to check — absence read as non-existence. Fail
|
||||||
# #3720 shape. Fail loudly instead.
|
# loudly instead.
|
||||||
assert names, "no rule-write surface takes when_to_apply; the guard is blind"
|
assert names, "no rule-write surface takes when_to_apply; the guard is blind"
|
||||||
return sorted(names)
|
return sorted(names)
|
||||||
|
|
||||||
@@ -123,10 +118,9 @@ def test_a_trigger_writing_surface_documents_the_field(name):
|
|||||||
doc = _doc(_MODULE, name)
|
doc = _doc(_MODULE, name)
|
||||||
assert "when_to_apply" in doc, (
|
assert "when_to_apply" in doc, (
|
||||||
f"{name} accepts when_to_apply but never mentions it in its "
|
f"{name} accepts when_to_apply but never mentions it in its "
|
||||||
"docstring. The tool docstring is the agent-facing contract "
|
"docstring. The tool docstring is the agent-facing contract, and a "
|
||||||
"(rule 119), and a trigger written against no contract is the #3855 "
|
"trigger written against no contract names a category instead of a "
|
||||||
"defect: six preferences named a category instead of a moment, and "
|
"moment — and a category is not a thing any session ever types."
|
||||||
"a category is not a thing any session ever types."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user