docs(6209): coverage, guidance-ownership, shape, warnings tests and shared fixtures
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Successful in 1m56s
CI & Build / Build & push image (push) Successful in 13s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Successful in 1m56s
CI & Build / Build & push image (push) Successful in 13s
test_pattern_coverage, test_guidance_ownership, test_integration_shape_classify, test_instruction_surfaces_agree, test_retrieval_warnings, helpers.py and conftest.py state the property each test or fixture holds. The issue and milestone numbers, consolidation dates and one-install measurements that recorded how each was found are dropped; fixture dates and ids stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+19
-20
@@ -7,8 +7,8 @@ in the environment before importing the app.
|
|||||||
|
|
||||||
For unit tests of pure functions no database is needed at all.
|
For unit tests of pure functions no database is needed at all.
|
||||||
|
|
||||||
The fixtures below are the ONE definition of three things that used to be
|
The fixtures below are the ONE definition of things a dozen test modules
|
||||||
copied into a dozen test modules each (#2825). They are deliberately not
|
each need. They are deliberately not
|
||||||
autouse: a module opts in with
|
autouse: a module opts in with
|
||||||
``pytestmark = pytest.mark.usefixtures("<name>")`` (or a test names the
|
``pytestmark = pytest.mark.usefixtures("<name>")`` (or a test names the
|
||||||
fixture as a parameter), so a unit test that never touches the engine or the
|
fixture as a parameter), so a unit test that never touches the engine or the
|
||||||
@@ -80,7 +80,7 @@ def _no_embedding():
|
|||||||
"""
|
"""
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
# Milestones embed too since milestone 415; a plan created in a test would
|
# Milestones embed too; a plan created in a test would
|
||||||
# otherwise detach the same model-loading task.
|
# otherwise detach the same model-loading task.
|
||||||
with patch("scribe.services.notes.embed_note", MagicMock()), \
|
with patch("scribe.services.notes.embed_note", MagicMock()), \
|
||||||
patch("scribe.services.milestones.embed_milestone", MagicMock()):
|
patch("scribe.services.milestones.embed_milestone", MagicMock()):
|
||||||
@@ -89,7 +89,7 @@ def _no_embedding():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def _no_supersession():
|
def _no_supersession():
|
||||||
"""Stub the auto-inject menu's "which lines are superseded?" lookup (#278).
|
"""Stub the auto-inject menu's "which lines are superseded?" lookup.
|
||||||
|
|
||||||
That is a real database call on a path the plugin-context tests exercise
|
That is a real database call on a path the plugin-context tests exercise
|
||||||
without one. Stubbed to "nothing superseded" — the ordinary state — rather
|
without one. Stubbed to "nothing superseded" — the ordinary state — rather
|
||||||
@@ -104,7 +104,7 @@ def _no_supersession():
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_reply_shape_log():
|
def _no_reply_shape_log():
|
||||||
"""Stub the reply-shape delivery row (milestone 500 step 3).
|
"""Stub the reply-shape delivery row.
|
||||||
|
|
||||||
Autouse for _no_system_labels' reason: every turn's retrieval, every
|
Autouse for _no_system_labels' reason: every turn's retrieval, every
|
||||||
moment request and every Scribe tool that reaches a shaped moment writes
|
moment request and every Scribe tool that reaches a shaped moment writes
|
||||||
@@ -118,7 +118,7 @@ def _no_reply_shape_log():
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_system_labels():
|
def _no_system_labels():
|
||||||
"""Stub the menu's "which System is each line about?" lookup (#4364).
|
"""Stub the menu's "which System is each line about?" lookup.
|
||||||
|
|
||||||
Autouse because every test that renders an injected menu reaches it, and
|
Autouse because every test that renders an injected menu reaches it, and
|
||||||
it is a real database call on a path those tests run without one. Stubbed
|
it is a real database call on a path those tests run without one. Stubbed
|
||||||
@@ -132,7 +132,7 @@ def _no_system_labels():
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_rulings_arm():
|
def _no_rulings_arm():
|
||||||
"""Stub the rulings arm both PreToolUse builders run (milestone 444).
|
"""Stub the rulings arm both PreToolUse builders run.
|
||||||
|
|
||||||
Autouse for _no_system_labels' reason: every write-path and tool-rule
|
Autouse for _no_system_labels' reason: every write-path and tool-rule
|
||||||
test with a project in scope reaches it, and its first act is reading the
|
test with a project in scope reaches it, and its first act is reading the
|
||||||
@@ -147,8 +147,7 @@ def _no_rulings_arm():
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_system_usage_readout():
|
def _no_system_usage_readout():
|
||||||
"""Stub the per-System usage lookup the Systems list and get_system carry
|
"""Stub the per-System usage lookup the Systems list and get_system carry.
|
||||||
(#4769).
|
|
||||||
|
|
||||||
Autouse for _no_rulings_arm's reason: every route and tool test that
|
Autouse for _no_rulings_arm's reason: every route and tool test that
|
||||||
reads a System now reaches a GROUP BY on system_usage_events. Stubbed to
|
reads a System now reaches a GROUP BY on system_usage_events. Stubbed to
|
||||||
@@ -168,7 +167,7 @@ def _no_system_usage_readout():
|
|||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_task_log_arm():
|
def _no_task_log_arm():
|
||||||
"""Stub the task-log read arm that get_task / list_tasks / get_milestone
|
"""Stub the task-log read arm that get_task / list_tasks / get_milestone
|
||||||
grew in #4241.
|
carry.
|
||||||
|
|
||||||
Autouse for the reason _no_rule_arm is: those three tools now read work
|
Autouse for the reason _no_rule_arm is: those three tools now read work
|
||||||
logs, and the reads go through the access layer to Postgres. Every unit
|
logs, and the reads go through the access layer to Postgres. Every unit
|
||||||
@@ -193,7 +192,7 @@ def _no_task_log_arm():
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_lesson_rule_links(request):
|
def _no_lesson_rule_links(request):
|
||||||
"""Stub the lesson↔rule link decorations (milestone 440).
|
"""Stub the lesson↔rule link decorations.
|
||||||
|
|
||||||
Every door that returns a lesson or a rule now attaches its links, and the
|
Every door that returns a lesson or a rule now attaches its links, and the
|
||||||
read is a real database call — so every unit test that opens either would
|
read is a real database call — so every unit test that opens either would
|
||||||
@@ -201,16 +200,16 @@ def _no_lesson_rule_links(request):
|
|||||||
Both decorations are fail-open, so the cost would be a slow failed connect
|
Both decorations are fail-open, so the cost would be a slow failed connect
|
||||||
per test rather than a failure, which is worse: it would never be noticed.
|
per test rather than a failure, which is worse: it would never be noticed.
|
||||||
|
|
||||||
The candidate search a new lesson runs (#4631) is stubbed to "nothing
|
The candidate search a new lesson runs is stubbed to "nothing
|
||||||
resembles it" for the same reason: it embeds, and the model load would
|
resembles it" for the same reason: it embeds, and the model load would
|
||||||
otherwise ride along with every unit test that creates a lesson. The
|
otherwise ride along with every unit test that creates a lesson. The
|
||||||
co-surfacing recorder (#4637) is stubbed to "no proposal": every hook
|
co-surfacing recorder is stubbed to "no proposal": every hook
|
||||||
builder test that renders a lesson beside a rule would otherwise reach
|
builder test that renders a lesson beside a rule would otherwise reach
|
||||||
for the database to count the pair. And the via-lesson rule step (#4633)
|
for the database to count the pair. And the via-lesson rule step
|
||||||
is stubbed to "no lesson brought a rule": all three rule arms run it, and
|
is stubbed to "no lesson brought a rule": all three rule arms run it, and
|
||||||
its first act is a database read. tests/test_rule_via_lesson.py binds the
|
its first act is a database read. tests/test_rule_via_lesson.py binds the
|
||||||
real function at import time, before this patch runs. The convergence
|
real function at import time, before this patch runs. The convergence
|
||||||
check a "no rule fits" answer runs (#4634) is stubbed to "no group" for
|
check a "no rule fits" answer runs is stubbed to "no group" for
|
||||||
the same reason; tests/test_lesson_convergence.py binds the real one.
|
the same reason; tests/test_lesson_convergence.py binds the real one.
|
||||||
|
|
||||||
Skipped for integration tests, which exercise the real links against
|
Skipped for integration tests, which exercise the real links against
|
||||||
@@ -231,7 +230,7 @@ def _no_lesson_rule_links(request):
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_family_triggers(request):
|
def _no_family_triggers(request):
|
||||||
"""Stub family canon's write-time triggers (milestone 463 step 3).
|
"""Stub family canon's write-time triggers.
|
||||||
|
|
||||||
create_note, create_task, create_snippet, the updates that change a body,
|
create_note, create_task, create_snippet, the updates that change a body,
|
||||||
and update_milestone(status="done") now look for a pattern carried between
|
and update_milestone(status="done") now look for a pattern carried between
|
||||||
@@ -255,7 +254,7 @@ def _no_family_triggers(request):
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_moment_delivery(request):
|
def _no_moment_delivery(request):
|
||||||
"""Stub the moment lookup the mapped MCP tools attach (milestone 458).
|
"""Stub the moment lookup the mapped MCP tools attach.
|
||||||
|
|
||||||
update_task, the create_* record tools and the milestone tools now hand
|
update_task, the create_* record tools and the milestone tools now hand
|
||||||
back the rules mounted on the moment they mark, and finding those is two
|
back the rules mounted on the moment they mark, and finding those is two
|
||||||
@@ -276,7 +275,7 @@ def _no_moment_delivery(request):
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_rule_arm():
|
def _no_rule_arm():
|
||||||
"""Stub the write-path hint's standing-RULES arm (milestone 307).
|
"""Stub the write-path hint's standing-RULES arm.
|
||||||
|
|
||||||
Autouse, and deliberately so. The arm calls semantic_search_rules, which
|
Autouse, and deliberately so. The arm calls semantic_search_rules, which
|
||||||
loads the embedding model — so every unrelated plugin-context test that
|
loads the embedding model — so every unrelated plugin-context test that
|
||||||
@@ -297,7 +296,7 @@ def _no_rule_arm():
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _no_rule_overlap():
|
def _no_rule_overlap():
|
||||||
"""Stub the rule/preference create path's overlap check (#4134).
|
"""Stub the rule/preference create path's overlap check.
|
||||||
|
|
||||||
The same reason as _no_rule_arm, one door over: every create_rule /
|
The same reason as _no_rule_arm, one door over: every create_rule /
|
||||||
create_project_rule / create_preference now asks semantic_search_rules
|
create_project_rule / create_preference now asks semantic_search_rules
|
||||||
@@ -314,7 +313,7 @@ def _no_rule_overlap():
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _default_gate_bars():
|
def _default_gate_bars():
|
||||||
"""Read the create gate's bars as their defaults, not from settings (#4385).
|
"""Read the create gate's bars as their defaults, not from settings.
|
||||||
|
|
||||||
Every create through the note gate now asks the user's settings for its
|
Every create through the note gate now asks the user's settings for its
|
||||||
similarity bars, which is a database read on a path the gate's unit tests
|
similarity bars, which is a database read on a path the gate's unit tests
|
||||||
|
|||||||
+24
-40
@@ -1,8 +1,7 @@
|
|||||||
"""Shared test helpers — the plain functions tests call, as opposed to the
|
"""Shared test helpers — the plain functions tests call, as opposed to the
|
||||||
fixtures in conftest.py.
|
fixtures in conftest.py.
|
||||||
|
|
||||||
Each of these was copied into several test modules before #2825 consolidated
|
Each of these serves several test modules; a module imports what it needs with ``from tests.helpers import ...``.
|
||||||
them; a module imports what it needs with ``from tests.helpers import ...``.
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -17,9 +16,7 @@ async def drive_update_note(note, **kwargs):
|
|||||||
|
|
||||||
The patch stack is the point: update_note reaches for a version snapshot,
|
The patch stack is the point: update_note reaches for a version snapshot,
|
||||||
an embedding refresh and a project reactivation on its way out, none of
|
an embedding refresh and a project reactivation on its way out, none of
|
||||||
which a unit test has. Written twice — once for the snippet mirror
|
which a unit test has.
|
||||||
(#3128) and once for the verification fields (#3182/317) — before being
|
|
||||||
consolidated here.
|
|
||||||
|
|
||||||
Returns whatever update_note returned; assert on the `note` you passed in.
|
Returns whatever update_note returned; assert on the `note` you passed in.
|
||||||
"""
|
"""
|
||||||
@@ -43,12 +40,11 @@ def tool_doc(module: str, name: str) -> str:
|
|||||||
|
|
||||||
Flattened because these are hard-wrapped at ~76 characters, so any phrase
|
Flattened because these are hard-wrapped at ~76 characters, so any phrase
|
||||||
worth asserting on is liable to straddle a line break — a property of the
|
worth asserting on is liable to straddle a line break — a property of the
|
||||||
formatter, not of the guidance. The disambiguator guard (#3123) learned
|
formatter, not of the guidance. Matching raw text reports a phrase absent
|
||||||
that on its own first run, matching raw text and reporting a phrase absent
|
that is plainly there.
|
||||||
that was plainly there.
|
|
||||||
|
|
||||||
Used by every test that pins the docstring CONTRACT rather than its
|
Used by every test that pins the docstring CONTRACT rather than its
|
||||||
wording. The tool docstring is the agent-facing contract (rule 119), so
|
wording. The tool docstring is the agent-facing contract, so
|
||||||
these guards exist to catch it being tidied down to a parameter list.
|
these guards exist to catch it being tidied down to a parameter list.
|
||||||
"""
|
"""
|
||||||
import importlib
|
import importlib
|
||||||
@@ -63,8 +59,7 @@ def compiled_sql(element, dialect=None) -> str:
|
|||||||
"""A SQLAlchemy clause or statement rendered as literal SQL text.
|
"""A SQLAlchemy clause or statement rendered as literal SQL text.
|
||||||
|
|
||||||
For asserting on the shape of a predicate without a database — which is how
|
For asserting on the shape of a predicate without a database — which is how
|
||||||
the visibility clauses and the knowledge facets are both tested. Was a
|
the visibility clauses and the knowledge facets are both tested.
|
||||||
private copy in each of those modules before #3128 needed a third.
|
|
||||||
|
|
||||||
Pass `dialect` when the assertion is about something only one backend
|
Pass `dialect` when the assertion is about something only one backend
|
||||||
renders — a Postgres row-lock mode, say. The generic dialect is enough for
|
renders — a Postgres row-lock mode, say. The generic dialect is enough for
|
||||||
@@ -93,9 +88,8 @@ def session_returning(note):
|
|||||||
"""``make_mock_session`` whose every ``execute()`` yields `note` (or None).
|
"""``make_mock_session`` whose every ``execute()`` yields `note` (or None).
|
||||||
|
|
||||||
The commonest shape above the bare session: a service that loads one
|
The commonest shape above the bare session: a service that loads one
|
||||||
record by id and acts on it. Two files had spelled this out identically
|
record by id and acts on it, and it belongs beside `make_mock_session`
|
||||||
before a third was about to (#3207 — derive before the third copy), and
|
for the same reason that one does.
|
||||||
it belongs beside `make_mock_session` for the same reason that one does.
|
|
||||||
|
|
||||||
For a service making SEVERAL different reads, set
|
For a service making SEVERAL different reads, set
|
||||||
``session.execute.side_effect`` to a list of results instead.
|
``session.execute.side_effect`` to a list of results instead.
|
||||||
@@ -134,7 +128,7 @@ def fake_record(**attrs) -> MagicMock:
|
|||||||
"""A MagicMock record with REAL values on the attributes named, and a
|
"""A MagicMock record with REAL values on the attributes named, and a
|
||||||
``to_dict()`` that mirrors them.
|
``to_dict()`` that mirrors them.
|
||||||
|
|
||||||
The hazard this exists for (note 2109): an auto-created MagicMock attribute
|
The hazard this exists for: an auto-created MagicMock attribute
|
||||||
is truthy and has a repr — so a bare MagicMock handed to the product reads
|
is truthy and has a repr — so a bare MagicMock handed to the product reads
|
||||||
as trashed, shared, a task, and owned by a MagicMock. Name every attribute
|
as trashed, shared, a task, and owned by a MagicMock. Name every attribute
|
||||||
the code under test will read; the per-model ``fake_*`` builders below
|
the code under test will read; the per-model ``fake_*`` builders below
|
||||||
@@ -180,10 +174,10 @@ def fake_note(**attrs) -> MagicMock:
|
|||||||
"note_type": "note", "is_task": False, "task_kind": "work",
|
"note_type": "note", "is_task": False, "task_kind": "work",
|
||||||
"status": "todo" if is_task else None,
|
"status": "todo" if is_task else None,
|
||||||
"data": None, "deleted_at": None,
|
"data": None, "deleted_at": None,
|
||||||
# Milestone 317: a truthy mock here reads as "this note carries a
|
# A truthy mock here reads as "this note carries a
|
||||||
# check", which trips the guard on records that may not have one.
|
# check", which trips the guard on records that may not have one.
|
||||||
"verify_with": None, "expires_when": None, "verified_at": None,
|
"verify_with": None, "expires_when": None, "verified_at": None,
|
||||||
# Milestone 381: a truthy mock would read as a live claim.
|
# A truthy mock would read as a live claim.
|
||||||
"claimed_by": None, "claimed_at": None, "claim_touched_at": None,
|
"claimed_by": None, "claimed_at": None, "claim_touched_at": None,
|
||||||
"claim_session": None,
|
"claim_session": None,
|
||||||
}, attrs)
|
}, attrs)
|
||||||
@@ -196,7 +190,7 @@ def fake_task(**attrs) -> MagicMock:
|
|||||||
"tags": [], "parent_id": None, "project_id": None, "is_task": True,
|
"tags": [], "parent_id": None, "project_id": None, "is_task": True,
|
||||||
"task_kind": "work", "user_id": 7, "deleted_at": None,
|
"task_kind": "work", "user_id": 7, "deleted_at": None,
|
||||||
"verify_with": None, "expires_when": None, "verified_at": None,
|
"verify_with": None, "expires_when": None, "verified_at": None,
|
||||||
# Milestone 381: a truthy mock would read as a live claim.
|
# A truthy mock would read as a live claim.
|
||||||
"claimed_by": None, "claimed_at": None, "claim_touched_at": None,
|
"claimed_by": None, "claimed_at": None, "claim_touched_at": None,
|
||||||
"claim_session": None,
|
"claim_session": None,
|
||||||
}, attrs)
|
}, attrs)
|
||||||
@@ -219,9 +213,8 @@ def fake_lesson(**attrs) -> MagicMock:
|
|||||||
"""A stand-in lesson: a note whose `note_type` is what makes it one.
|
"""A stand-in lesson: a note whose `note_type` is what makes it one.
|
||||||
|
|
||||||
The title is the subject alone and the trigger lives in `data`, because
|
The title is the subject alone and the trigger lives in `data`, because
|
||||||
that is the record the product creates (milestone 427) — the trigger joins
|
that is the record the product creates — the trigger joins the title only
|
||||||
the title only in the embedded document. A default whose title carried the
|
in the embedded document.
|
||||||
trigger would be testing a row only an un-migrated database holds.
|
|
||||||
|
|
||||||
The check fields and `arose_from_id` are explicitly None for the reason
|
The check fields and `arose_from_id` are explicitly None for the reason
|
||||||
`fake_snippet`'s `data` is: `update_note` reads `verify_with` and
|
`fake_snippet`'s `data` is: `update_note` reads `verify_with` and
|
||||||
@@ -310,8 +303,7 @@ def plain_rule_detail():
|
|||||||
the TOOL forwarded the right arguments, so the seam is stubbed the same
|
the TOOL forwarded the right arguments, so the seam is stubbed the same
|
||||||
way the create/update calls themselves already are.
|
way the create/update calls themselves already are.
|
||||||
|
|
||||||
Consolidated here on its second copy, per this module's own reason for
|
Shared here because two stubs for one seam drift apart quietly, and a test
|
||||||
existing (#2825) — two stubs for one seam drift apart quietly, and a test
|
|
||||||
stubbing the seam slightly differently is a test asserting something
|
stubbing the seam slightly differently is a test asserting something
|
||||||
slightly different than it appears to.
|
slightly different than it appears to.
|
||||||
"""
|
"""
|
||||||
@@ -356,8 +348,7 @@ def http_sink(reply: bytes = b'{"context":"","note_ids":[]}',
|
|||||||
(one dict per request, in order). Lets the shell be tested end to end —
|
(one dict per request, in order). Lets the shell be tested end to end —
|
||||||
the extraction, the encoding, the URL — without a Scribe instance.
|
the extraction, the encoding, the URL — without a Scribe instance.
|
||||||
|
|
||||||
Three test modules each carried their own ``_Sink`` handler before #2904
|
Pass ``reply`` for the body the hook should see.
|
||||||
consolidated them here; pass ``reply`` for the body the hook should see.
|
|
||||||
|
|
||||||
``by_path`` is for a hook that calls more than one route: each request is
|
``by_path`` is for a hook that calls more than one route: each request is
|
||||||
answered with the reply for its path (``reply`` when none is listed), and
|
answered with the reply for its path (``reply`` when none is listed), and
|
||||||
@@ -406,10 +397,10 @@ def http_sink(reply: bytes = b'{"context":"","note_ids":[]}',
|
|||||||
|
|
||||||
|
|
||||||
def writepath_cfg(**over):
|
def writepath_cfg(**over):
|
||||||
"""A complete `get_writepath_config` stand-in, built from the registry (#4102).
|
"""A complete `get_writepath_config` stand-in, built from the registry.
|
||||||
|
|
||||||
DERIVED, NOT LITERAL, and the reason is a failure mode this file already
|
DERIVED, NOT LITERAL, because of a failure mode prose cannot prevent: the
|
||||||
warned about in prose without being able to prevent: the write-path hint
|
write-path hint
|
||||||
drives three arms, each of which reads its numbers out of the config dict
|
drives three arms, each of which reads its numbers out of the config dict
|
||||||
inside a fail-open `except`. A dict missing one key does not raise where a
|
inside a fail-open `except`. A dict missing one key does not raise where a
|
||||||
reader would see it — the arm silently becomes a no-op, which is
|
reader would see it — the arm silently becomes a no-op, which is
|
||||||
@@ -417,9 +408,9 @@ def writepath_cfg(**over):
|
|||||||
|
|
||||||
So the keys come from `retrieval_surfaces.SURFACES`. A seventh surface, or a
|
So the keys come from `retrieval_surfaces.SURFACES`. A seventh surface, or a
|
||||||
rename, changes this helper for free and cannot quietly disable an arm in
|
rename, changes this helper for free and cannot quietly disable an arm in
|
||||||
ten hand-written dicts that each looked complete on the day they were typed.
|
hand-written dicts that each looked complete on the day they were typed.
|
||||||
|
|
||||||
NOT EVERY KEY IS A SURFACE, and that gap already bit once (#4214). The
|
NOT EVERY KEY IS A SURFACE. The
|
||||||
checkpoint bar has no entry in SURFACES on purpose — everything in that
|
checkpoint bar has no entry in SURFACES on purpose — everything in that
|
||||||
table is a floor/budget pair belonging to one QUERY, and the checkpoint
|
table is a floor/budget pair belonging to one QUERY, and the checkpoint
|
||||||
runs none, it re-reads hits the rule arms already produced. Adding it there
|
runs none, it re-reads hits the rule arms already produced. Adding it there
|
||||||
@@ -458,11 +449,7 @@ def need_tools(*tools):
|
|||||||
|
|
||||||
A skip rather than a stub on purpose. Stubbing `jq` would test the stub —
|
A skip rather than a stub on purpose. Stubbing `jq` would test the stub —
|
||||||
these tests exist precisely because the shell's behaviour is the thing in
|
these tests exist precisely because the shell's behaviour is the thing in
|
||||||
question (#2932), so anything short of the real tool proves nothing.
|
question, so anything short of the real tool proves nothing.
|
||||||
|
|
||||||
Consolidated 2026-09-21 from three byte-identical copies in
|
|
||||||
test_contract_around_the_change, test_hook_json_reader and
|
|
||||||
test_session_slippage_readout.
|
|
||||||
"""
|
"""
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
@@ -482,14 +469,11 @@ async def rule_row(rule_id: int):
|
|||||||
map, which can hold a value the database never accepted — a column the
|
map, which can hold a value the database never accepted — a column the
|
||||||
write never reached, a default the ORM supplied rather than the schema.
|
write never reached, a default the ORM supplied rather than the schema.
|
||||||
Opening a new session forces a real read and is the only way `verify_with`
|
Opening a new session forces a real read and is the only way `verify_with`
|
||||||
being NULL rather than "" is distinguishable at all (milestone 312).
|
being NULL rather than "" is distinguishable at all.
|
||||||
|
|
||||||
Import is lazy because this module is imported by unit tests that have no
|
Import is lazy because this module is imported by unit tests that have no
|
||||||
database and must not pay for one — the same reason `plugin_config` defers
|
database and must not pay for one — the same reason `plugin_config` defers
|
||||||
its service imports.
|
its service imports.
|
||||||
|
|
||||||
Consolidated 2026-09-21 from two byte-identical copies in
|
|
||||||
test_integration_rule_move and test_integration_rule_verification.
|
|
||||||
"""
|
"""
|
||||||
from scribe.models import async_session
|
from scribe.models import async_session
|
||||||
from scribe.models.rulebook import Rule
|
from scribe.models.rulebook import Rule
|
||||||
@@ -519,7 +503,7 @@ def skill_text(name: str) -> str:
|
|||||||
|
|
||||||
A skill keeps what matters on most turns in SKILL.md and moves what matters
|
A skill keeps what matters on most turns in SKILL.md and moves what matters
|
||||||
at one moment into sibling files it links (Agent Skills progressive
|
at one moment into sibling files it links (Agent Skills progressive
|
||||||
disclosure, #4398). Both are the skill's own statement, so a test asking
|
disclosure). Both are the skill's own statement, so a test asking
|
||||||
"does this skill still say X" reads them together rather than pinning X to
|
"does this skill still say X" reads them together rather than pinning X to
|
||||||
whichever file it happens to sit in today.
|
whichever file it happens to sit in today.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
"""Every piece of agent guidance has exactly one owner that states it.
|
"""Every piece of agent guidance has exactly one owner that states it.
|
||||||
|
|
||||||
WHY THIS EXISTS (milestone 410, decision #4027)
|
WHY THIS EXISTS
|
||||||
|
|
||||||
Scribe's guidance to agents was written up to five times over: the MCP
|
Scribe's guidance to agents reaches a session through five surfaces: the MCP
|
||||||
`_INSTRUCTIONS`, the plugin's static session context, the live session context
|
`_INSTRUCTIONS`, the plugin's static session context, the live session context
|
||||||
the server builds, the `using-scribe` skill, and tool docstrings. An earlier
|
the server builds, the skills, and tool docstrings. A practice stated in full
|
||||||
design (#2494) made that deliberate — insurance against any one surface failing
|
on several of them drifts apart, copy by copy.
|
||||||
silently — and the copies drifted apart instead (#2497, #4022).
|
|
||||||
|
|
||||||
Decision #4027 replaced the redundancy with ownership: the server orients with
|
So each practice has one owner: the server orients with a short index, the
|
||||||
a short index, the skills hold the depth, tool docstrings hold each tool's
|
skills hold the depth, tool docstrings hold each tool's contract, and each
|
||||||
contract, and each client adapter holds only its own timing and conventions.
|
client adapter holds only its own timing and conventions. This module is the
|
||||||
This module is the registry that decision is enforced from.
|
registry that ownership is enforced from.
|
||||||
|
|
||||||
WHAT IT PINS
|
WHAT IT PINS
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ WHAT IT PINS
|
|||||||
surface every MCP client receives, so each reflex it indexes keeps its
|
surface every MCP client receives, so each reflex it indexes keeps its
|
||||||
`index` markers there — a one-line pointer, not a copy. Only session-start
|
`index` markers there — a one-line pointer, not a copy. Only session-start
|
||||||
practices carry `index`: judging, reporting and the missed-rule route fire
|
practices carry `index`: judging, reporting and the missed-rule route fire
|
||||||
mid-work, and #4389 took them out of the index (the skills and the in-band
|
mid-work, so they stay out of the index (the skills and the in-band
|
||||||
responses carry them at the moment they apply).
|
responses carry them at the moment they apply).
|
||||||
|
|
||||||
WHAT IT CANNOT SEE
|
WHAT IT CANNOT SEE
|
||||||
@@ -90,7 +89,7 @@ def delivered_surfaces() -> dict[str, str]:
|
|||||||
- `commands` — the Claude Code adapter's slash commands
|
- `commands` — the Claude Code adapter's slash commands
|
||||||
- `live` — the live session context the server builds
|
- `live` — the live session context the server builds
|
||||||
- `shapes` — the reply shapes the server delivers at their moments,
|
- `shapes` — the reply shapes the server delivers at their moments,
|
||||||
in the full form a session first sees (milestone 500)
|
in the full form a session first sees
|
||||||
"""
|
"""
|
||||||
server = (ROOT / "src/scribe/mcp/server.py").read_text()
|
server = (ROOT / "src/scribe/mcp/server.py").read_text()
|
||||||
match = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S)
|
match = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S)
|
||||||
@@ -103,7 +102,7 @@ def delivered_surfaces() -> dict[str, str]:
|
|||||||
"live": _live_session_context_source(),
|
"live": _live_session_context_source(),
|
||||||
"shapes": _delivered_shapes(),
|
"shapes": _delivered_shapes(),
|
||||||
}
|
}
|
||||||
# A skill is SKILL.md plus the reference files it links (#4398): one
|
# A skill is SKILL.md plus the reference files it links: one
|
||||||
# owner, however many files it is split across.
|
# owner, however many files it is split across.
|
||||||
for skill in sorted((ROOT / "plugin/skills").glob("*/SKILL.md")):
|
for skill in sorted((ROOT / "plugin/skills").glob("*/SKILL.md")):
|
||||||
surfaces[f"skill:{skill.parent.name}"] = skill_text(skill.parent.name)
|
surfaces[f"skill:{skill.parent.name}"] = skill_text(skill.parent.name)
|
||||||
@@ -144,9 +143,8 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
index=("lessons inform",),
|
index=("lessons inform",),
|
||||||
# Stated twice on purpose, at the two moments it is needed: the
|
# Stated twice on purpose, at the two moments it is needed: the
|
||||||
# skill while deciding what a record IS, and create_rule while a
|
# skill while deciding what a record IS, and create_rule while a
|
||||||
# proposal is actually being written — which is where the
|
# proposal is actually being written — which is where mis-routing
|
||||||
# mis-routing #3733 recorded happens, and the caller there has not
|
# happens, and the caller there has not necessarily read the skill.
|
||||||
# necessarily read the skill.
|
|
||||||
shared_with=("docstrings",)),
|
shared_with=("docstrings",)),
|
||||||
Topic("a lesson grows each time it proves itself", U,
|
Topic("a lesson grows each time it proves itself", U,
|
||||||
("update_lesson", "re-keyed", "grows each time"),
|
("update_lesson", "re-keyed", "grows each time"),
|
||||||
@@ -182,7 +180,7 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
Topic("a retrieved rule outranks a default habit", U, ("outranks a default habit",),
|
Topic("a retrieved rule outranks a default habit", U, ("outranks a default habit",),
|
||||||
"a retrieved rule outranks a default habit"),
|
"a retrieved rule outranks a default habit"),
|
||||||
Topic("log on completion and on a problem", U, ("hit or discover a problem",), "hit or discover a problem"),
|
Topic("log on completion and on a problem", U, ("hit or discover a problem",), "hit or discover a problem"),
|
||||||
# Milestone 381 step 4: the half of a hand-off only the model can do. The
|
# The half of a hand-off only the model can do. The
|
||||||
# release is mechanical (SessionEnd hook); saying what happened is not.
|
# release is mechanical (SessionEnd hook); saying what happened is not.
|
||||||
Topic("hand off before the context stops existing", U,
|
Topic("hand off before the context stops existing", U,
|
||||||
("hand off", "claims it for this session"),
|
("hand off", "claims it for this session"),
|
||||||
@@ -196,7 +194,7 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
Topic("where a new rule goes, and its trigger", U, ("create_project_rule", "when_to_apply"),
|
Topic("where a new rule goes, and its trigger", U, ("create_project_rule", "when_to_apply"),
|
||||||
"whichever home it gets"),
|
"whichever home it gets"),
|
||||||
Topic("a rule vs the other entities", U, ("standing instruction",), "first ask whether it's a rule at all"),
|
Topic("a rule vs the other entities", U, ("standing instruction",), "first ask whether it's a rule at all"),
|
||||||
# Moments (milestone 458 step 8). Delivery at a moment and its in-session
|
# Moments. Delivery at a moment and its in-session
|
||||||
# corrections are owned by using-scribe's moments.md; the index carries one
|
# corrections are owned by using-scribe's moments.md; the index carries one
|
||||||
# clause, and a new rule's moments are asked where its home and trigger are.
|
# clause, and a new rule's moments are asked where its home and trigger are.
|
||||||
Topic("rules mounted on a moment arrive by lookup; correct a misfire in-session", U,
|
Topic("rules mounted on a moment arrive by lookup; correct a misfire in-session", U,
|
||||||
@@ -212,9 +210,8 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
Topic("a missed when is mounted or mapped, not reworded", U,
|
Topic("a missed when is mounted or mapped, not reworded", U,
|
||||||
("update_rule(moments=",),
|
("update_rule(moments=",),
|
||||||
"first ask whether it missed a when or a what"),
|
"first ask whether it missed a when or a what"),
|
||||||
# Milestone 416 step 9: the tuning tools shipped in #4102/#4104 and were
|
# The tuning tools need a route on an instruction surface; machinery
|
||||||
# named on NO instruction surface — measured, `retrieval_tuning_history`
|
# nothing names goes unused.
|
||||||
# returned zero events. Machinery with no route to it.
|
|
||||||
Topic("a missed rule is a trigger to fix before a floor to move", U,
|
Topic("a missed rule is a trigger to fix before a floor to move", U,
|
||||||
("retrieval_telemetry", "tune_retrieval", "retrieval_surfaces"),
|
("retrieval_telemetry", "tune_retrieval", "retrieval_surfaces"),
|
||||||
"take it to the record first and the dial second"),
|
"take it to the record first and the dial second"),
|
||||||
@@ -222,9 +219,8 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
("what_might_apply",), "ask what already covers that moment"),
|
("what_might_apply",), "ask what already covers that moment"),
|
||||||
Topic("reference notes update in place; dev-logs don't", U, ("reference note",),
|
Topic("reference notes update in place; dev-logs don't", U, ("reference note",),
|
||||||
"state updates in place; chronicles don't"),
|
"state updates in place; chronicles don't"),
|
||||||
# Milestone 444: an operator's ruling was kept only as a quote in a work
|
# A ruling kept only as a quote in a work log sits beside a session's
|
||||||
# log, beside a session's reading of it that contradicted it, and three
|
# reading of it, and later sessions build on the reading. No index marker: both fire
|
||||||
# later sessions built on the reading. No index marker: both fire
|
|
||||||
# mid-work — when the operator decides, and when code is read as intent.
|
# mid-work — when the operator decides, and when code is read as intent.
|
||||||
Topic("an operator's ruling lives on the System it governs", U,
|
Topic("an operator's ruling lives on the System it governs", U,
|
||||||
("rulings", "update_system", "the operator decided it"),
|
("rulings", "update_system", "the operator decided it"),
|
||||||
@@ -238,8 +234,8 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
# ── process arcs — owned by their skills ──
|
# ── process arcs — owned by their skills ──
|
||||||
Topic("plan in a milestone, steps created together", "skill:writing-plans", ("start_planning", "{{ref:"),
|
Topic("plan in a milestone, steps created together", "skill:writing-plans", ("start_planning", "{{ref:"),
|
||||||
"a milestone earns its place when the work has an arc", index=("start_planning",)),
|
"a milestone earns its place when the work has an arc", index=("start_planning",)),
|
||||||
# Milestone 415: sessions opened a second plan beside the roadmap milestone
|
# Without this, a session opens a second plan beside the roadmap milestone
|
||||||
# that already covered the work, because nothing told them to look.
|
# that already covers the work.
|
||||||
Topic("find the existing plan before making one", "skill:writing-plans",
|
Topic("find the existing plan before making one", "skill:writing-plans",
|
||||||
('content_type="milestone"', "unplanned_milestones", "existing_milestone"),
|
('content_type="milestone"', "unplanned_milestones", "existing_milestone"),
|
||||||
"when an active milestone already covers the work, the plan is that milestone",
|
"when an active milestone already covers the work, the plan is that milestone",
|
||||||
@@ -247,14 +243,14 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
Topic("reuse recorded shapes; record at first build", "skill:reusing-code",
|
Topic("reuse recorded shapes; record at first build", "skill:reusing-code",
|
||||||
("create_snippet", "when_to_use", "first build", "second copy"),
|
("create_snippet", "when_to_use", "first build", "second copy"),
|
||||||
"prior art offered beside a write is not noise", index=("create_snippet",)),
|
"prior art offered beside a write is not noise", index=("create_snippet",)),
|
||||||
# Milestone 439: the writer judges what it wrote, at the end of the turn,
|
# The writer judges what it wrote, at the end of the turn,
|
||||||
# asked by the plugin's Stop hook — the moment the knowledge exists.
|
# asked by the plugin's Stop hook — the moment the knowledge exists.
|
||||||
Topic("judge what you wrote before the turn ends", "skill:reusing-code",
|
Topic("judge what you wrote before the turn ends", "skill:reusing-code",
|
||||||
("before the turn ends", "classify_shapes", 'kind: "file"'),
|
("before the turn ends", "classify_shapes", 'kind: "file"'),
|
||||||
"you are the one who knows what the code you just wrote is"),
|
"you are the one who knows what the code you just wrote is"),
|
||||||
Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement"),
|
Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement"),
|
||||||
"take the placement from the record"),
|
"take the placement from the record"),
|
||||||
# Milestone 500: the default shapes are server product, delivered at their
|
# The default shapes are server product, delivered at their
|
||||||
# moments with the operator's preferences mounted beside them — so the
|
# moments with the operator's preferences mounted beside them — so the
|
||||||
# rule that a preference wins is stated where the shape arrives.
|
# rule that a preference wins is stated where the shape arrives.
|
||||||
Topic("a preference beside a delivered shape is what the operator asked for", "shapes",
|
Topic("a preference beside a delivered shape is what the operator asked for", "shapes",
|
||||||
@@ -263,8 +259,8 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
Topic("every reply leads with its conclusion and stays short", "shapes",
|
Topic("every reply leads with its conclusion and stays short", "shapes",
|
||||||
("conclusion first", "the shortest reply that carries the answer", "what can go"),
|
("conclusion first", "the shortest reply that carries the answer", "what can go"),
|
||||||
"length is work handed back to the reader"),
|
"length is work handed back to the reader"),
|
||||||
# Milestone 409 step 7: the sections were being FILLED rather than chosen,
|
# Sections FILLED rather than chosen let a reply satisfy every heading and
|
||||||
# so a reply could satisfy every heading and still be unreadable. These
|
# still be unreadable. These
|
||||||
# three are the discipline around the scaffold, not the scaffold itself.
|
# three are the discipline around the scaffold, not the scaffold itself.
|
||||||
Topic("sections are chosen, not filled, and the reply is cut twice",
|
Topic("sections are chosen, not filled, and the reply is cut twice",
|
||||||
"skill:reporting-back", ("not a form to complete", "what can go"),
|
"skill:reporting-back", ("not a form to complete", "what can go"),
|
||||||
@@ -275,19 +271,18 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
Topic("a settled decision is acted on, not re-opened",
|
Topic("a settled decision is acted on, not re-opened",
|
||||||
"skill:reporting-back", ("already made",),
|
"skill:reporting-back", ("already made",),
|
||||||
"reads as contradicting yourself rather than as being careful"),
|
"reads as contradicting yourself rather than as being careful"),
|
||||||
# Milestone 444: a cap a past session chose rode inside an option as a
|
# A cap a past session chose, riding inside an option as a "known cost",
|
||||||
# "known cost", the operator approved the option, and the cap read as
|
# reads as the operator's once they approve the option.
|
||||||
# theirs from then on.
|
|
||||||
Topic("an option says whose call each carried assumption was",
|
Topic("an option says whose call each carried assumption was",
|
||||||
"skill:reporting-back", ("past session chose this", "conflict"),
|
"skill:reporting-back", ("past session chose this", "conflict"),
|
||||||
"an unlabelled one gets approved as if it had been asked"),
|
"an unlabelled one gets approved as if it had been asked"),
|
||||||
# Milestone 409 step 8: `placement` rides a WRITE, so a task the reply only
|
# `placement` rides a WRITE, so a task the reply only cites arrives with
|
||||||
# cites arrived with nothing vouching for it — which is how a step finished
|
# nothing vouching for it — a finished step can be reported as the open
|
||||||
# four days earlier was reported as the open one (#4154).
|
# one.
|
||||||
Topic("a record you only cite still gets read",
|
Topic("a record you only cite still gets read",
|
||||||
"skill:reporting-back", ("next_step", "only mention"),
|
"skill:reporting-back", ("next_step", "only mention"),
|
||||||
"a record you only mention is a record to read"),
|
"a record you only mention is a record to read"),
|
||||||
# Milestone 463 step 6: family canon — when an idea is evaluated, how a
|
# Family canon — when an idea is evaluated, how a
|
||||||
# project answers one, what a reason is, and the conflict order's reflex.
|
# project answers one, what a reason is, and the conflict order's reflex.
|
||||||
# The criteria, outcomes and grounds themselves are product text on the
|
# The criteria, outcomes and grounds themselves are product text on the
|
||||||
# tools that enforce them; the skill owns when and how.
|
# tools that enforce them; the skill owns when and how.
|
||||||
@@ -298,7 +293,7 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
index=("family-canon",)),
|
index=("family-canon",)),
|
||||||
# ── per-tool contracts and in-band behaviour — owned by the server ──
|
# ── per-tool contracts and in-band behaviour — owned by the server ──
|
||||||
Topic("closing a task cues the report", "docstrings", ("report_back",), "reporting this to the operator?"),
|
Topic("closing a task cues the report", "docstrings", ("report_back",), "reporting this to the operator?"),
|
||||||
# The agent is the judge (#4208). Two topics, not one, because they fire
|
# The agent is the judge. Two topics, not one, because they fire
|
||||||
# at different moments and say different things: using-scribe owns the
|
# at different moments and say different things: using-scribe owns the
|
||||||
# authority — what you may decide and that deciding means READING, not
|
# authority — what you may decide and that deciding means READING, not
|
||||||
# thresholding — while reporting-back owns the shape a report takes when
|
# thresholding — while reporting-back owns the shape a report takes when
|
||||||
@@ -368,8 +363,8 @@ def test_every_topic_is_stated_by_its_owner():
|
|||||||
gaps = owner_gaps(TOPICS, delivered_surfaces())
|
gaps = owner_gaps(TOPICS, delivered_surfaces())
|
||||||
assert not gaps, (
|
assert not gaps, (
|
||||||
"these topics are no longer stated in full by their owner:\n "
|
"these topics are no longer stated in full by their owner:\n "
|
||||||
+ "\n ".join(gaps) + "\nEvery guidance topic has one owner (decision "
|
+ "\n ".join(gaps) + "\nEvery guidance topic has one owner. "
|
||||||
"#4027). If the text moved on purpose, move the topic's owner here in the "
|
"If the text moved on purpose, move the topic's owner here in the "
|
||||||
"same commit; if it was reworded, update its markers and statement."
|
"same commit; if it was reworded, update its markers and statement."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -378,7 +373,7 @@ def test_no_topic_is_stated_in_full_off_its_owner():
|
|||||||
found = copies(TOPICS, delivered_surfaces())
|
found = copies(TOPICS, delivered_surfaces())
|
||||||
assert not found, (
|
assert not found, (
|
||||||
"guidance stated in full on a surface that doesn't own it:\n "
|
"guidance stated in full on a surface that doesn't own it:\n "
|
||||||
+ "\n ".join(found) + "\nOne owner per topic (decision #4027): replace "
|
+ "\n ".join(found) + "\nOne owner per topic: replace "
|
||||||
"the copy with a one-line pointer to the owner. If both places genuinely "
|
"the copy with a one-line pointer to the owner. If both places genuinely "
|
||||||
"need it at different moments, declare it in `shared_with` with the reason."
|
"need it at different moments, declare it in `shared_with` with the reason."
|
||||||
)
|
)
|
||||||
@@ -405,7 +400,7 @@ def test_topic_keys_are_unique():
|
|||||||
|
|
||||||
|
|
||||||
def test_the_ownership_guards_can_fail():
|
def test_the_ownership_guards_can_fail():
|
||||||
"""Rule 167: each guard is shown turning red once."""
|
"""Each guard is shown turning red once."""
|
||||||
surfaces = {
|
surfaces = {
|
||||||
"instructions": "use the widget",
|
"instructions": "use the widget",
|
||||||
"docstrings": "the widget owner statement lives here",
|
"docstrings": "the widget owner statement lives here",
|
||||||
@@ -424,9 +419,9 @@ def test_the_ownership_guards_can_fail():
|
|||||||
assert index_gaps((topic,), surfaces) == ["'widget': ['not in the index']"]
|
assert index_gaps((topic,), surfaces) == ["'widget': ['not in the index']"]
|
||||||
|
|
||||||
|
|
||||||
# ── The skills are client-neutral (milestone 410 step 2) ────────────────
|
# ── The skills are client-neutral ───────────────────────────────────────
|
||||||
#
|
#
|
||||||
# Agent Skills is an open format read by dozens of clients (#4023), so the
|
# Agent Skills is an open format read by dozens of clients, so the
|
||||||
# skills folder is the part of every client package that is shared verbatim.
|
# skills folder is the part of every client package that is shared verbatim.
|
||||||
# Anything only one client understands belongs in that client's adapter —
|
# Anything only one client understands belongs in that client's adapter —
|
||||||
# for Claude Code, the plugin's static context, hooks and commands — or the
|
# for Claude Code, the plugin's static context, hooks and commands — or the
|
||||||
@@ -465,7 +460,7 @@ def test_the_skills_name_no_particular_client():
|
|||||||
offenders = {path: hits for path, hits in offenders.items() if hits}
|
offenders = {path: hits for path, hits in offenders.items() if hits}
|
||||||
assert not offenders, (
|
assert not offenders, (
|
||||||
f"skills that name one client: {offenders}. Skills are shared by every "
|
f"skills that name one client: {offenders}. Skills are shared by every "
|
||||||
f"client package (decision #4027); say the universal thing in the skill "
|
f"client package; say the universal thing in the skill "
|
||||||
f"and put the client's own name for it in that client's adapter."
|
f"and put the client's own name for it in that client's adapter."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,11 @@
|
|||||||
"""What the instruction surfaces must say that one owner per topic cannot enforce.
|
"""What the instruction surfaces must say that one owner per topic cannot enforce.
|
||||||
|
|
||||||
WHY THIS FILE IS SMALLER THAN IT WAS
|
tests/test_guidance_ownership.py enforces who states each topic: every topic
|
||||||
|
stated in full by exactly one owner, with a one-line pointer in the server's
|
||||||
Until milestone 410 this file pinned REDUNDANCY: every session-start surface —
|
index. How to ask for a rule and what an empty session means are the rules
|
||||||
the MCP `_INSTRUCTIONS`, the plugin's static context, the `using-scribe` skill —
|
topic, owned by using-scribe and indexed in `_INSTRUCTIONS`; the Systems
|
||||||
had to restate how a rule reaches a session, what an empty session means, the
|
reflex and the snippet-recording triggers are their topics' markers, required
|
||||||
Systems reflex and the snippet-recording triggers. That was the design #2494
|
on their owners.
|
||||||
chose after #2497 (two surfaces disagreeing about who loads the rules) and
|
|
||||||
#2198 (every hook silently inert): say it everywhere, so no single failure
|
|
||||||
loses it.
|
|
||||||
|
|
||||||
Decision #4027 replaced that with ownership, and tests/test_guidance_ownership.py
|
|
||||||
now enforces it: every topic stated in full by exactly one owner, with a
|
|
||||||
one-line pointer in the server's index. The tests that required every surface
|
|
||||||
to repeat itself were retired there, because they enforced the very duplication
|
|
||||||
that drifted (#4022). What they protected is still protected:
|
|
||||||
- "state how to ask for a rule" / "say an empty session is not an empty
|
|
||||||
rulebook" → the rules topic: owned by using-scribe, indexed in
|
|
||||||
`_INSTRUCTIONS` with both phrases;
|
|
||||||
- the Systems reflex and the snippet-recording triggers → their topics'
|
|
||||||
markers, required on their owners;
|
|
||||||
- "never name the SessionStart push without stating the ask" (#2497's exact
|
|
||||||
shape) → retired outright. Since milestone 394 the push carries no rules,
|
|
||||||
so naming it can no longer imply rules were handled, and requiring every
|
|
||||||
surface that mentions it to restate the ask would force a copy.
|
|
||||||
|
|
||||||
WHAT STAYS HERE
|
WHAT STAYS HERE
|
||||||
|
|
||||||
@@ -46,7 +28,7 @@ def _all_surfaces() -> list[tuple[str, str]]:
|
|||||||
what the hook does is not the same act as telling an agent it need not pull.
|
what the hook does is not the same act as telling an agent it need not pull.
|
||||||
The boundary is "does a session read this", which is skills (loaded by
|
The boundary is "does a session read this", which is skills (loaded by
|
||||||
description match, and each reference file a skill links is read on its
|
description match, and each reference file a skill links is read on its
|
||||||
own, so it is its own surface — #4398), the hook-injected static context, and the MCP server's
|
own, so it is its own surface), the hook-injected static context, and the MCP server's
|
||||||
own instructions.
|
own instructions.
|
||||||
"""
|
"""
|
||||||
found = [(str(p.relative_to(ROOT)), p.read_text())
|
found = [(str(p.relative_to(ROOT)), p.read_text())
|
||||||
@@ -68,10 +50,9 @@ def _instructions_text() -> str:
|
|||||||
|
|
||||||
|
|
||||||
# Claude Code injects only the first ~2,048 characters of an MCP server's
|
# Claude Code injects only the first ~2,048 characters of an MCP server's
|
||||||
# instructions and silently cuts the rest mid-word (#2562: observed live —
|
# instructions and silently cuts the rest mid-word, so guidance past the cap
|
||||||
# the previous 20k-char version delivered ~10% of itself, and none of the
|
# never reaches a session. The budget is set below the cap, not at it: the
|
||||||
# Systems tagging guidance ever reached a session). The budget is set below
|
# field runs ~600–1,600 characters, and
|
||||||
# the cap, not at it: the field runs ~600–1,600 characters (spike #4389), and
|
|
||||||
# a block pressed against 2,048 is being used for depth that belongs in a
|
# a block pressed against 2,048 is being used for depth that belongs in a
|
||||||
# skill.
|
# skill.
|
||||||
INSTRUCTIONS_BUDGET = 1600
|
INSTRUCTIONS_BUDGET = 1600
|
||||||
@@ -81,18 +62,17 @@ def test_instructions_fit_the_fold():
|
|||||||
text = _instructions_text()
|
text = _instructions_text()
|
||||||
assert len(text) <= INSTRUCTIONS_BUDGET, (
|
assert len(text) <= INSTRUCTIONS_BUDGET, (
|
||||||
f"_INSTRUCTIONS is {len(text)} chars; Claude Code injects only ~2,048 "
|
f"_INSTRUCTIONS is {len(text)} chars; Claude Code injects only ~2,048 "
|
||||||
f"and silently cuts the rest (#2562). This block is an INDEX — state "
|
f"and silently cuts the rest. This block is an INDEX — state "
|
||||||
f"the topic in full on its owner (a skill or the tool's docstring, "
|
f"the topic in full on its owner (a skill or the tool's docstring) "
|
||||||
f"decision #4027) and give it at most a line here; see the comment "
|
f"and give it at most a line here; see the comment "
|
||||||
f"above _INSTRUCTIONS."
|
f"above _INSTRUCTIONS."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── force: a surface that says rules bind must say what does not ────────
|
# ── force: a surface that says rules bind must say what does not ────────
|
||||||
#
|
#
|
||||||
# Added with the preference kind (milestone 399). Before it, "rules bind" was
|
# With a preference kind beside rules, "rules bind" is half of the truth, and
|
||||||
# the whole truth and every surface said so flatly. It is now half of one, and
|
# the missing half is the dangerous half to omit: a session reading
|
||||||
# the half that is missing is the dangerous half to omit: a session reading
|
|
||||||
# only "rules bind" and then receiving a preference has been told, by the most
|
# only "rules bind" and then receiving a preference has been told, by the most
|
||||||
# authoritative surface it has, to treat it as binding.
|
# authoritative surface it has, to treat it as binding.
|
||||||
#
|
#
|
||||||
@@ -107,7 +87,7 @@ def test_instructions_fit_the_fold():
|
|||||||
# PINNED ON THE CLAIM, NOT THE WORD "bind". A bare substring also matches
|
# PINNED ON THE CLAIM, NOT THE WORD "bind". A bare substring also matches
|
||||||
# `bind_repo`, `list_repo_bindings` and the DNS-rebinding comment in
|
# `bind_repo`, `list_repo_bindings` and the DNS-rebinding comment in
|
||||||
# server.py — so it would one day fail a skill that mentions repo binding and
|
# server.py — so it would one day fail a skill that mentions repo binding and
|
||||||
# has nothing to do with force, which is rule 167's named failure: a guard
|
# has nothing to do with force — a guard
|
||||||
# raising a false alarm about the very thing it protects. These phrases are
|
# raising a false alarm about the very thing it protects. These phrases are
|
||||||
# the ones that actually assert bindingness to a reader.
|
# the ones that actually assert bindingness to a reader.
|
||||||
BINDING_CLAIMS = (
|
BINDING_CLAIMS = (
|
||||||
@@ -144,7 +124,7 @@ def test_a_surface_claiming_rules_bind_also_names_what_does_not():
|
|||||||
# whole corpus; a session that meets a third line shape with no name for it
|
# whole corpus; a session that meets a third line shape with no name for it
|
||||||
# falls back on the nearest one it has, which is the binding one.
|
# falls back on the nearest one it has, which is the binding one.
|
||||||
#
|
#
|
||||||
# The currency half is the claim #3896 was written around: a preference is the
|
# The currency half is the central claim: a preference is the
|
||||||
# one record a session updates ITSELF, without asking. Stated only in the
|
# one record a session updates ITSELF, without asking. Stated only in the
|
||||||
# skill, it reaches a session that loaded the skill — which is exactly the
|
# skill, it reaches a session that loaded the skill — which is exactly the
|
||||||
# session least likely to need telling. In the index it reaches everyone.
|
# session least likely to need telling. In the index it reaches everyone.
|
||||||
@@ -168,12 +148,12 @@ def test_the_index_names_all_three_strengths():
|
|||||||
f"client receives, and a record kind it does not name is one the session "
|
f"client receives, and a record kind it does not name is one the session "
|
||||||
f"has to guess the force of — which it does by reaching for the kind it "
|
f"has to guess the force of — which it does by reaching for the kind it "
|
||||||
f"does know. Keep it to a clause; the full statement lives on "
|
f"does know. Keep it to a clause; the full statement lives on "
|
||||||
f"using-scribe (decision #4027)."
|
f"using-scribe."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_the_strength_guard_can_fail():
|
def test_the_strength_guard_can_fail():
|
||||||
"""Rule 167. The guard has to be able to bite, and it bites on omission —
|
"""The guard has to be able to bite, and it bites on omission —
|
||||||
the failure that actually happens, when a kind is added to the product and
|
the failure that actually happens, when a kind is added to the product and
|
||||||
the index is left describing the corpus that came before it."""
|
the index is left describing the corpus that came before it."""
|
||||||
text = "rules bind; preferences guide and you keep them current."
|
text = "rules bind; preferences guide and you keep them current."
|
||||||
@@ -182,14 +162,13 @@ def test_the_strength_guard_can_fail():
|
|||||||
assert missing == ["that lessons inform"]
|
assert missing == ["that lessons inform"]
|
||||||
|
|
||||||
|
|
||||||
# ── Reporting back (milestone 409 step 3) ──────────────────────────────
|
# ── Reporting back ─────────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# The reporting-back skill carries the shapes, but a skill only helps if it
|
# The reporting-back skill carries the shapes, but a skill only helps if it
|
||||||
# fires. The reflex that sends a session to it is stated in using-scribe, and
|
# fires. The reflex that sends a session to it is stated in using-scribe, and
|
||||||
# the in-band cue on update_task (`placement`, `report_back`) is the half that
|
# the in-band cue on update_task (`placement`, `report_back`) is the half that
|
||||||
# fires on its own in every client. The server's index carried a REPORT line
|
# fires on its own in every client. The server's index carries no REPORT line:
|
||||||
# until #4389: reporting is a mid-work stance, not a session-start practice. The
|
# reporting is a mid-work stance, not a session-start practice.
|
||||||
# static context was a second plugin-side copy until milestone 410.
|
|
||||||
|
|
||||||
REPORT_REFLEX = "report back in a shape the operator can read"
|
REPORT_REFLEX = "report back in a shape the operator can read"
|
||||||
REPORT_SURFACES = (
|
REPORT_SURFACES = (
|
||||||
@@ -211,8 +190,8 @@ def test_the_reporting_reflex_is_stated_in_using_scribe():
|
|||||||
|
|
||||||
|
|
||||||
def test_the_reporting_reflex_takes_placement_from_the_record():
|
def test_the_reporting_reflex_takes_placement_from_the_record():
|
||||||
"""The failure milestone 409 began from: a placement reconstructed from
|
"""A placement reconstructed from memory reads exactly like a real one
|
||||||
memory reads exactly like a real one when it is wrong."""
|
when it is wrong."""
|
||||||
for path in REPORT_SURFACES:
|
for path in REPORT_SURFACES:
|
||||||
text = " ".join(path.read_text().split())
|
text = " ".join(path.read_text().split())
|
||||||
assert "`placement`" in text, f"{path.relative_to(ROOT)} no longer points at the placement block"
|
assert "`placement`" in text, f"{path.relative_to(ROOT)} no longer points at the placement block"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Real-Postgres integration tests for shape classification (#2789).
|
"""Real-Postgres integration tests for shape classification.
|
||||||
|
|
||||||
What mocks can't prove: the all-or-nothing batch against real rows, the
|
What mocks can't prove: the all-or-nothing batch against real rows, the
|
||||||
write-ACL gate, the todo query's filters, and the consumer map riding
|
write-ACL gate, the todo query's filters, and the consumer map riding
|
||||||
@@ -96,7 +96,7 @@ async def test_classify_applies_judgments_and_reports_unmatched(seeded):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_a_bad_batch_applies_nothing(seeded):
|
async def test_a_bad_batch_applies_nothing(seeded):
|
||||||
"""All-or-nothing (#2709's lesson): a caller must never learn later that
|
"""All-or-nothing: a caller must never learn later that
|
||||||
half a batch silently happened."""
|
half a batch silently happened."""
|
||||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||||
with pytest.raises(ValueError) as err:
|
with pytest.raises(ValueError) as err:
|
||||||
@@ -131,7 +131,7 @@ async def test_classification_is_write_gated_and_listing_read_gated(seeded):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_rule_form_sweeps_unclassified_rows_only_and_applies_whole(seeded):
|
async def test_rule_form_sweeps_unclassified_rows_only_and_applies_whole(seeded):
|
||||||
"""#2868: one judgment over a directory + glob; judged rows are left
|
"""One judgment over a directory + glob; judged rows are left
|
||||||
alone unless include_judged; the same gates as the row form."""
|
alone unless include_judged; the same gates as the row form."""
|
||||||
owner, other, pid, sid = seeded["owner"], seeded["other"], seeded["pid"], seeded["snippet"]
|
owner, other, pid, sid = seeded["owner"], seeded["other"], seeded["pid"], seeded["snippet"]
|
||||||
await classify_shapes(owner, pid, [
|
await classify_shapes(owner, pid, [
|
||||||
@@ -171,7 +171,7 @@ async def test_rule_form_sweeps_unclassified_rows_only_and_applies_whole(seeded)
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_sync_stamps_scoped_rows_and_unstamps_when_they_become_reachable(seeded):
|
async def test_sync_stamps_scoped_rows_and_unstamps_when_they_become_reachable(seeded):
|
||||||
"""#2869: by-construction one-offs arrive `scoped` (mechanical), count as
|
"""By-construction one-offs arrive `scoped` (mechanical), count as
|
||||||
accounted, are reached by the sweep's default, and go back to the todo
|
accounted, are reached by the sweep's default, and go back to the todo
|
||||||
if a later tree makes them ordinary. A judgment overrides the stamp."""
|
if a later tree makes them ordinary. A judgment overrides the stamp."""
|
||||||
from scribe.services.coverage import ArchiveShape
|
from scribe.services.coverage import ArchiveShape
|
||||||
@@ -203,7 +203,7 @@ async def test_sync_stamps_scoped_rows_and_unstamps_when_they_become_reachable(s
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_uses_edges_are_the_consumer_map(seeded):
|
async def test_uses_edges_are_the_consumer_map(seeded):
|
||||||
"""#2870: a shape keeps ONE snippet_id (what it is) and any number of
|
"""A shape keeps ONE snippet_id (what it is) and any number of
|
||||||
uses edges (what it calls); the snippet's consumer map lists them,
|
uses edges (what it calls); the snippet's consumer map lists them,
|
||||||
list_shapes(uses=N) finds them, and a sweep can write them."""
|
list_shapes(uses=N) finds them, and a sweep can write them."""
|
||||||
from scribe.services import snippets as snippets_svc
|
from scribe.services import snippets as snippets_svc
|
||||||
@@ -296,7 +296,7 @@ async def test_get_snippet_carries_the_structured_consumer_map(seeded):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_sync_refiles_rows_whose_snippet_was_purged(seeded):
|
async def test_sync_refiles_rows_whose_snippet_was_purged(seeded):
|
||||||
"""The SET NULL companion (#2787): a judgment whose target is hard-deleted
|
"""The SET NULL companion: a judgment whose target is hard-deleted
|
||||||
rejoins the todo on the next sync instead of dangling target-less."""
|
rejoins the todo on the next sync instead of dangling target-less."""
|
||||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||||
await classify_shapes(owner, pid, [
|
await classify_shapes(owner, pid, [
|
||||||
@@ -318,12 +318,12 @@ async def test_sync_refiles_rows_whose_snippet_was_purged(seeded):
|
|||||||
assert row.snippet_id is None
|
assert row.snippet_id is None
|
||||||
|
|
||||||
|
|
||||||
# --- #2791: the write-path feed lands hook evidence as rows -------------------
|
# --- the write-path feed lands hook evidence as rows --------------------------
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_write_path_evidence_is_a_proposal_never_a_verdict(seeded):
|
async def test_write_path_evidence_is_a_proposal_never_a_verdict(seeded):
|
||||||
"""Milestone 439. Pulled + referenced → every named shape of the
|
"""Pulled + referenced → every named shape of the
|
||||||
snippet's kind CARRIES the snippet as a proposal (basis "reference"),
|
snippet's kind CARRIES the snippet as a proposal (basis "reference"),
|
||||||
and its status is untouched: the agent that wrote it says what it is.
|
and its status is untouched: the agent that wrote it says what it is.
|
||||||
A judgment is never touched, not even by a proposal. The outsider
|
A judgment is never touched, not even by a proposal. The outsider
|
||||||
@@ -465,7 +465,7 @@ async def test_recent_pulls_reads_the_usage_stream(seeded):
|
|||||||
assert sid + 2000 not in pulls
|
assert sid + 2000 not in pulls
|
||||||
|
|
||||||
|
|
||||||
# --- #2792: the mechanical proposer against real rows -------------------------
|
# --- the mechanical proposer against real rows -------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _quiet_semantic():
|
def _quiet_semantic():
|
||||||
@@ -594,7 +594,7 @@ async def test_derive_groups_land_on_rows_and_in_the_summary(seeded):
|
|||||||
|
|
||||||
summary = proposal_summary(await live_rows(pid))
|
summary = proposal_summary(await live_rows(pid))
|
||||||
assert summary["proposed"] == 1
|
assert summary["proposed"] == 1
|
||||||
# #2872: the body-identical group (a real copy) outranks the bigger name
|
# The body-identical group (a real copy) outranks the bigger name
|
||||||
# group (usually convention), even at size 2 vs 3.
|
# group (usually convention), even at size 2 vs 3.
|
||||||
order = [g["group"] for g in summary["derive_groups"]]
|
order = [g["group"] for g in summary["derive_groups"]]
|
||||||
assert order[0].startswith("dup:") and order[1] == "name:css:card"
|
assert order[0].startswith("dup:") and order[1] == "name:css:card"
|
||||||
@@ -605,7 +605,7 @@ async def test_derive_groups_land_on_rows_and_in_the_summary(seeded):
|
|||||||
|
|
||||||
# One of the css copies gets judged → the group shrinks on the next pass
|
# One of the css copies gets judged → the group shrinks on the next pass
|
||||||
# but stays a family: a class in two files is already a recipe living in
|
# but stays a family: a class in two files is already a recipe living in
|
||||||
# two places (css name floor 2, note 2917). Judge the second and it's gone.
|
# two places (css name floor 2). Judge the second and it's gone.
|
||||||
await classify_shapes(owner, pid, [
|
await classify_shapes(owner, pid, [
|
||||||
{"path": "b/z.css", "symbol": "card", "status": "exempt", "reason": "print sheet"},
|
{"path": "b/z.css", "symbol": "card", "status": "exempt", "reason": "print sheet"},
|
||||||
])
|
])
|
||||||
@@ -623,8 +623,8 @@ async def test_derive_groups_land_on_rows_and_in_the_summary(seeded):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_write_time_derive_names_the_family_or_the_canon_for_a_name(seeded):
|
async def test_write_time_derive_names_the_family_or_the_canon_for_a_name(seeded):
|
||||||
"""#2900: against real rows — a name in a family → the family (other
|
"""Against real rows — a name in a family → the family (other files,
|
||||||
files, count; for CSS a NAME family, never a body one — note 2917); a
|
count; for CSS a NAME family, never a body one); a
|
||||||
name whose canonical row lives elsewhere → that canon; a judged row at
|
name whose canonical row lives elsewhere → that canon; a judged row at
|
||||||
the path, the canon's own file, or an unknown name → silence."""
|
the path, the canon's own file, or an unknown name → silence."""
|
||||||
from scribe.services.shape_ledger import apply_derive_groups, write_time_derive
|
from scribe.services.shape_ledger import apply_derive_groups, write_time_derive
|
||||||
@@ -667,7 +667,7 @@ async def test_write_time_derive_names_the_family_or_the_canon_for_a_name(seeded
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_derive_new_names_the_copy_that_joined_a_family_since_the_stamp(seeded):
|
async def test_derive_new_names_the_copy_that_joined_a_family_since_the_stamp(seeded):
|
||||||
"""#2899: the first sync seeds one `slug`; a later sync adds an identical
|
"""The first sync seeds one `slug`; a later sync adds an identical
|
||||||
copy. Against the stamp between them, derive_new counts ONLY the
|
copy. Against the stamp between them, derive_new counts ONLY the
|
||||||
newcomer — the drift since the last refresh, not the whole family."""
|
newcomer — the drift since the last refresh, not the whole family."""
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -700,7 +700,7 @@ async def test_derive_new_names_the_copy_that_joined_a_family_since_the_stamp(se
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_transition_and_prefix_references_clear_the_unused_css_flag(seeded):
|
async def test_transition_and_prefix_references_clear_the_unused_css_flag(seeded):
|
||||||
"""#2970 end to end: the two class forms a template never spells out —
|
"""End to end: the two class forms a template never spells out —
|
||||||
a transition `name=` and a concatenated name — travel from the real
|
a transition `name=` and a concatenated name — travel from the real
|
||||||
extractor through resolution into the flag, so rules they reach stop
|
extractor through resolution into the flag, so rules they reach stop
|
||||||
being reported as unused. Only a rule nothing can reach stays listed."""
|
being reported as unused. Only a rule nothing can reach stays listed."""
|
||||||
@@ -752,7 +752,7 @@ async def test_transition_and_prefix_references_clear_the_unused_css_flag(seeded
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_consumer_map_syncs_edges_from_template_references(seeded):
|
async def test_consumer_map_syncs_edges_from_template_references(seeded):
|
||||||
"""Milestone 302: the consumer edges follow the archive — own-file
|
"""The consumer edges follow the archive — own-file
|
||||||
resolution for a scoped class, fan-out to the shared sheet for a class a
|
resolution for a scoped class, fan-out to the shared sheet for a class a
|
||||||
template does not define, counts refreshed and stale edges removed on
|
template does not define, counts refreshed and stale edges removed on
|
||||||
the next sync, and a vanished row's edges gone with it."""
|
the next sync, and a vanished row's edges gone with it."""
|
||||||
@@ -812,7 +812,7 @@ async def test_consumer_map_syncs_edges_from_template_references(seeded):
|
|||||||
assert rows[("v/B.vue", "error-msg")] not in edges
|
assert rows[("v/B.vue", "error-msg")] not in edges
|
||||||
|
|
||||||
|
|
||||||
# --- #2793: the divergence readout against real rows -------------------------
|
# --- the divergence readout against real rows -------------------------------
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
@@ -833,7 +833,7 @@ async def test_a_second_confirm_dialog_is_detected_and_named(seeded):
|
|||||||
from scribe.services import snippets as snippets_svc
|
from scribe.services import snippets as snippets_svc
|
||||||
|
|
||||||
owner, pid = seeded["owner"], seeded["pid"]
|
owner, pid = seeded["owner"], seeded["pid"]
|
||||||
# The confirm helper is TS canon: since #2871 a sym basis only proposes
|
# The confirm helper is TS canon: a sym basis only proposes
|
||||||
# within the shape's language family, so the fixture's Python snippet
|
# within the shape's language family, so the fixture's Python snippet
|
||||||
# says nothing about these Vue bodies — the canon must be one of theirs.
|
# says nothing about these Vue bodies — the canon must be one of theirs.
|
||||||
canon = await snippets_svc.create_snippet(
|
canon = await snippets_svc.create_snippet(
|
||||||
@@ -906,10 +906,9 @@ async def test_a_second_confirm_dialog_is_detected_and_named(seeded):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_weak_stamps_elect_no_canon_and_their_flags_are_withdrawn(seeded):
|
async def test_weak_stamps_elect_no_canon_and_their_flags_are_withdrawn(seeded):
|
||||||
"""#4608, the poisoned-ledger case as it stood on two projects: a
|
"""The poisoned-ledger case: a directory's "dominant canon" elected
|
||||||
directory's "dominant canon" was elected entirely by hook stamps written
|
entirely by hook stamps written below the 0.80 floor would tell every new
|
||||||
at 0.68-0.72, before the 0.80 floor, so every new shape there was told it
|
shape there it diverged from an unrelated snippet. The same fixture as the acceptance case
|
||||||
diverged from a YAML CI snippet. The same fixture as the acceptance case
|
|
||||||
above, with one difference — the four instances are weak hook stamps
|
above, with one difference — the four instances are weak hook stamps
|
||||||
rather than an audit — and the prompt must fall silent: at refresh (the
|
rather than an audit — and the prompt must fall silent: at refresh (the
|
||||||
standing flag is withdrawn) and at write time. The stamps themselves are
|
standing flag is withdrawn) and at write time. The stamps themselves are
|
||||||
@@ -984,14 +983,14 @@ async def test_weak_stamps_elect_no_canon_and_their_flags_are_withdrawn(seeded):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_a_semantic_miss_does_not_silence_the_prompt(seeded):
|
async def test_a_semantic_miss_does_not_silence_the_prompt(seeded):
|
||||||
"""#4208, reversed on measurement: a semantic miss is not evidence.
|
"""A semantic miss is not evidence.
|
||||||
|
|
||||||
THE FIXTURE IS THE ACCEPTANCE CASE ABOVE, DELIBERATELY. A hand-rolled
|
THE FIXTURE IS THE ACCEPTANCE CASE ABOVE, DELIBERATELY. A hand-rolled
|
||||||
`confirmDanger` beside an async confirm canon is structurally identical to
|
`confirmDanger` beside an async confirm canon is structurally identical to
|
||||||
a registry helper beside an async service canon, and the arm's miss was
|
a registry helper beside an async service canon, and the arm's miss was
|
||||||
meant to tell them apart. Measured live, true instances of a canon rarely
|
meant to tell them apart. True instances of a canon rarely clear the arm's
|
||||||
clear the arm's floor either, so a miss fires on both — silencing on
|
floor either, so a miss fires on both — silencing on it would silence the
|
||||||
it silenced #2793's own case. Here the arm has read the body, found
|
case the readout exists for. Here the arm has read the body, found
|
||||||
nothing, and the prompt is still RAISED.
|
nothing, and the prompt is still RAISED.
|
||||||
|
|
||||||
Asserted on the row, not a flag count: `since` sits a second early to
|
Asserted on the row, not a flag count: `since` sits a second early to
|
||||||
@@ -1087,9 +1086,9 @@ async def _two_generations(seeded, tag: str):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_the_capped_semantic_pass_reaches_the_new_shape_first(seeded):
|
async def test_the_capped_semantic_pass_reaches_the_new_shape_first(seeded):
|
||||||
"""Measured live: after the #4208 version bump, 1,533 rows competed for 150
|
"""After a version bump, every row competes for a capped number of
|
||||||
semantic checks in row order and the six new shapes — the only ones the
|
semantic checks; in row order the new shapes — the only ones the
|
||||||
divergence check acts on — were all left unread and flagged. With room for
|
divergence check acts on — are left unread and flagged. With room for
|
||||||
ONE check, the new shape must be the one that gets it."""
|
ONE check, the new shape must be the one that gets it."""
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
@@ -1155,7 +1154,7 @@ async def test_history_records_what_was_used_when_and_drift_asks_for_a_recheck(s
|
|||||||
assert await shape_history(other, pid, "src/app.py") == {}
|
assert await shape_history(other, pid, "src/app.py") == {}
|
||||||
|
|
||||||
|
|
||||||
# --- milestone 439: the writer judges what it wrote, before the sync has seen it
|
# --- the writer judges what it wrote, before the sync has seen it
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_a_shape_written_this_turn_is_judged_before_the_sync_sees_it(seeded):
|
async def test_a_shape_written_this_turn_is_judged_before_the_sync_sees_it(seeded):
|
||||||
@@ -1235,7 +1234,7 @@ async def test_unjudged_is_what_nobody_read(seeded):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_a_whole_file_snippet_makes_its_file_row_canonical(seeded):
|
async def test_a_whole_file_snippet_makes_its_file_row_canonical(seeded):
|
||||||
"""Milestone 439: a snippet recorded at a component's path, with no
|
"""A snippet recorded at a component's path, with no
|
||||||
symbol, is the canon for that file row — the claim a whole-file record
|
symbol, is the canon for that file row — the claim a whole-file record
|
||||||
makes and a definition row never could. It still covers no definition
|
makes and a definition row never could. It still covers no definition
|
||||||
inside the file."""
|
inside the file."""
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Pattern-library coverage (#2692) — the extractor that mirrors the hook,
|
"""Pattern-library coverage — the extractor that mirrors the hook,
|
||||||
the shape/record matcher, and the end-to-end measurement on real Postgres.
|
the shape/record matcher, and the end-to-end measurement on real Postgres.
|
||||||
|
|
||||||
The extractor here and the hook's awk program (scribe_prior_art.sh ARM 1)
|
The extractor here and the hook's awk program (scribe_prior_art.sh ARM 1)
|
||||||
@@ -66,11 +66,9 @@ EXTRACTION_VECTORS = [
|
|||||||
[]),
|
[]),
|
||||||
("dedup-within-file", "def f():\n pass\ndef f():\n pass\n",
|
("dedup-within-file", "def f():\n pass\ndef f():\n pass\n",
|
||||||
[("sym", "f")]),
|
[("sym", "f")]),
|
||||||
# --- comment and string spans (#4222) ------------------------------------
|
# --- comment and string spans --------------------------------------------
|
||||||
# Prose is not code. A wrapped docstring line beginning "class AND the"
|
# Prose is not code. A wrapped docstring line beginning "class AND the"
|
||||||
# announced a shape called `AND` to a live session; `with` and `nobody`
|
# would otherwise announce a shape called `AND`.
|
||||||
# out of one module docstring in scripts/check_dangling_styles.py reached
|
|
||||||
# persisted, judged `code_shapes` rows.
|
|
||||||
("docstring-prose",
|
("docstring-prose",
|
||||||
'def real_one():\n """Its own text is about the\n'
|
'def real_one():\n """Its own text is about the\n'
|
||||||
' class AND the to_dict, and is a def bar():\n'
|
' class AND the to_dict, and is a def bar():\n'
|
||||||
@@ -81,18 +79,15 @@ EXTRACTION_VECTORS = [
|
|||||||
("unterminated-docstring",
|
("unterminated-docstring",
|
||||||
'def before():\n """oops, never closed\n\ndef after():\n pass\n',
|
'def before():\n """oops, never closed\n\ndef after():\n pass\n',
|
||||||
[("sym", "before"), ("sym", "after")]),
|
[("sym", "before"), ("sym", "after")]),
|
||||||
# The CSS half of the same defect (#2990): a wrapped comment line that
|
# The CSS half of the same rule: a wrapped comment line that
|
||||||
# happens to begin with a dotted token reads as a selector.
|
# happens to begin with a dotted token reads as a selector.
|
||||||
("css-comment-selector",
|
("css-comment-selector",
|
||||||
"/* A real base rule, not just descendants: the check reads a\n"
|
"/* A real base rule, not just descendants: the check reads a\n"
|
||||||
" .ghost, class that only ever appears as an ancestor */\n.check { }\n",
|
" .ghost, class that only ever appears as an ancestor */\n.check { }\n",
|
||||||
[("css", "check")]),
|
[("css", "check")]),
|
||||||
# The rest of #2990's verify list, which this scan satisfies as written:
|
# A comment holding a WHOLE rule defines nothing, and an unterminated
|
||||||
# a comment holding a WHOLE rule defines nothing, and an unterminated
|
# comment does not swallow the file. A phantom class out of a comment
|
||||||
# comment does not swallow the file. The phantom it was filed for —
|
# would persist with a `used_by` count of zero and inflate unused_css.
|
||||||
# `.editor-body` out of a wrapped comment in TaskEditorView.vue, a
|
|
||||||
# persisted row with a `used_by` count of zero inflating unused_css — is
|
|
||||||
# among the twenty-two this removes.
|
|
||||||
("comment-containing-a-whole-rule",
|
("comment-containing-a-whole-rule",
|
||||||
"/* .ghost {\n color: red;\n } */\n.real { color: blue; }\n",
|
"/* .ghost {\n color: red;\n } */\n.real { color: blue; }\n",
|
||||||
[("css", "real")]),
|
[("css", "real")]),
|
||||||
@@ -195,7 +190,7 @@ def test_shapes_from_archive_strips_the_wrapper_and_gates_files():
|
|||||||
assert shapes_from_archive(_tarball(TREE)) == TREE_SHAPES
|
assert shapes_from_archive(_tarball(TREE)) == TREE_SHAPES
|
||||||
|
|
||||||
|
|
||||||
# --- unit: template class references — the CSS consumer map (milestone 302) --
|
# --- unit: template class references — the CSS consumer map ----------------
|
||||||
|
|
||||||
|
|
||||||
def test_class_references_reads_vue_static_and_dynamic_forms_only():
|
def test_class_references_reads_vue_static_and_dynamic_forms_only():
|
||||||
@@ -229,7 +224,7 @@ def test_class_references_reads_vue_static_and_dynamic_forms_only():
|
|||||||
def test_class_references_reads_framework_transition_names():
|
def test_class_references_reads_framework_transition_names():
|
||||||
"""A transition `name=` is a class reference: the framework applies
|
"""A transition `name=` is a class reference: the framework applies
|
||||||
`.toast-enter-active` and friends at runtime, so a stylesheet that
|
`.toast-enter-active` and friends at runtime, so a stylesheet that
|
||||||
defines them is consumed even though no template spells one out (#2970).
|
defines them is consumed even though no template spells one out.
|
||||||
Every spelling of the tag counts; a bound `:name` stays unknowable."""
|
Every spelling of the tag counts; a bound `:name` stays unknowable."""
|
||||||
refs = class_references(
|
refs = class_references(
|
||||||
"a/T.vue",
|
"a/T.vue",
|
||||||
@@ -251,7 +246,7 @@ def test_class_references_reads_framework_transition_names():
|
|||||||
def test_class_references_reads_a_concatenated_name_as_a_prefix():
|
def test_class_references_reads_a_concatenated_name_as_a_prefix():
|
||||||
"""`status-${s}` cannot be resolved to one class, but its static head is
|
"""`status-${s}` cannot be resolved to one class, but its static head is
|
||||||
real information: it is emitted as the prefix reference `status-*` so
|
real information: it is emitted as the prefix reference `status-*` so
|
||||||
the rows it could have built are not reported unused (#2970). A head too
|
the rows it could have built are not reported unused. A head too
|
||||||
short to mean anything, or a bare separator, says nothing."""
|
short to mean anything, or a bare separator, says nothing."""
|
||||||
vue = (
|
vue = (
|
||||||
'<div :class="`status-${task.status}`" />'
|
'<div :class="`status-${task.status}`" />'
|
||||||
@@ -301,7 +296,7 @@ def test_scan_archive_returns_definitions_and_references_from_one_walk():
|
|||||||
scan = scan_archive(_tarball(tree))
|
scan = scan_archive(_tarball(tree))
|
||||||
assert isinstance(scan, ArchiveScan)
|
assert isinstance(scan, ArchiveScan)
|
||||||
assert [(d.path, d.kind, d.name) for d in scan.definitions] == TREE_SHAPES + [
|
assert [(d.path, d.kind, d.name) for d in scan.definitions] == TREE_SHAPES + [
|
||||||
("web/Card.vue", "file", "Card"), # a component is a unit (milestone 439)
|
("web/Card.vue", "file", "Card"), # a component is a unit
|
||||||
("web/Card.vue", "css", "card"),
|
("web/Card.vue", "css", "card"),
|
||||||
]
|
]
|
||||||
# Only files whose markup names a class appear; the .py/.css files don't.
|
# Only files whose markup names a class appear; the .py/.css files don't.
|
||||||
@@ -309,7 +304,7 @@ def test_scan_archive_returns_definitions_and_references_from_one_walk():
|
|||||||
assert shapes_from_archive(_tarball(tree)) == [(d.path, d.kind, d.name) for d in scan.definitions]
|
assert shapes_from_archive(_tarball(tree)) == [(d.path, d.kind, d.name) for d in scan.definitions]
|
||||||
|
|
||||||
|
|
||||||
# --- unit: the covering predicate (lives with the ledger since #2788) --------
|
# --- unit: the covering predicate (lives with the ledger) --------------------
|
||||||
|
|
||||||
|
|
||||||
def test_location_covers_by_exact_path_dir_prefix_and_css_dot():
|
def test_location_covers_by_exact_path_dir_prefix_and_css_dot():
|
||||||
@@ -341,7 +336,7 @@ def test_largest_gaps_ranks_by_unclassified_and_drops_clean_dirs():
|
|||||||
|
|
||||||
|
|
||||||
def test_type_import_specifiers_are_not_definitions():
|
def test_type_import_specifiers_are_not_definitions():
|
||||||
"""#2904: `import { type Foo, bar }` is the same two words as `type Foo =`
|
"""`import { type Foo, bar }` is the same two words as `type Foo =`
|
||||||
and defines nothing; only a `type` line with a declaration after the
|
and defines nothing; only a `type` line with a declaration after the
|
||||||
name counts (TS alias, Go/Rust type)."""
|
name counts (TS alias, Go/Rust type)."""
|
||||||
from scribe.services.coverage import extract_shapes
|
from scribe.services.coverage import extract_shapes
|
||||||
@@ -375,7 +370,7 @@ def test_coverage_line_is_evidence_carrying_and_labeled_estimate():
|
|||||||
|
|
||||||
|
|
||||||
def test_bind_repo_tool_takes_a_ref():
|
def test_bind_repo_tool_takes_a_ref():
|
||||||
"""#2873: the binding names the branch the ledger follows."""
|
"""The binding names the branch the ledger follows."""
|
||||||
from scribe.mcp.server import build_mcp_server
|
from scribe.mcp.server import build_mcp_server
|
||||||
tool = build_mcp_server()._tool_manager.get_tool("bind_repo")
|
tool = build_mcp_server()._tool_manager.get_tool("bind_repo")
|
||||||
assert "ref" in tool.parameters.get("properties", {})
|
assert "ref" in tool.parameters.get("properties", {})
|
||||||
@@ -412,7 +407,7 @@ def _forge(tar_bytes: bytes):
|
|||||||
|
|
||||||
|
|
||||||
def _selector(tar_bytes: bytes):
|
def _selector(tar_bytes: bytes):
|
||||||
"""The keyring shape compute_coverage consumes since #2778 — one owner
|
"""The keyring shape compute_coverage consumes — one owner
|
||||||
keyring holding the mocked Gitea adapter."""
|
keyring holding the mocked Gitea adapter."""
|
||||||
from scribe.services.forge import ForgeSelector
|
from scribe.services.forge import ForgeSelector
|
||||||
|
|
||||||
@@ -482,10 +477,10 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
|
|||||||
assert coverage["largest_gaps"] == [
|
assert coverage["largest_gaps"] == [
|
||||||
{"dir": "src", "unclassified": 2, "total": 3}
|
{"dir": "src", "unclassified": 2, "total": 3}
|
||||||
]
|
]
|
||||||
# #2899: a first computation has no previous stamp — nothing is "new".
|
# A first computation has no previous stamp — nothing is "new".
|
||||||
assert coverage["derive_new"] == {"count": 0, "examples": []}
|
assert coverage["derive_new"] == {"count": 0, "examples": []}
|
||||||
|
|
||||||
# The walk fed the LEDGER (#2788): every extracted shape has a row, the
|
# The walk fed the LEDGER: every extracted shape has a row, the
|
||||||
# snippet reference locations are mechanically stamped canonical WITH
|
# snippet reference locations are mechanically stamped canonical WITH
|
||||||
# their snippet id, and the rest sit in the todo state.
|
# their snippet id, and the rest sit in the todo state.
|
||||||
async with async_session() as s:
|
async with async_session() as s:
|
||||||
@@ -516,7 +511,7 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_ledger_keeps_judgments_and_stamps_vanished_shapes(seeded):
|
async def test_ledger_keeps_judgments_and_stamps_vanished_shapes(seeded):
|
||||||
"""The two survival rules (#2788): an agent's classification outlives
|
"""The two survival rules: an agent's classification outlives
|
||||||
recompute, and a shape that leaves the tree is stamped vanished — kept
|
recompute, and a shape that leaves the tree is stamped vanished — kept
|
||||||
for history, dropped from the readout."""
|
for history, dropped from the readout."""
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -593,9 +588,8 @@ async def test_enter_project_surfaces_the_line_only_once_computed(seeded):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_explicit_refresh_names_its_failures(seeded):
|
async def test_explicit_refresh_names_its_failures(seeded):
|
||||||
"""refresh_for_caller (#2802) raises fixable errors instead of silence:
|
"""refresh_for_caller raises fixable errors instead of silence: an agent
|
||||||
an agent mid-task must learn WHY nothing measured — 'None' is exactly the
|
mid-task must learn WHY nothing measured, and 'None' would strand it."""
|
||||||
stranding the button-only path caused."""
|
|
||||||
from scribe.models import async_session
|
from scribe.models import async_session
|
||||||
from scribe.services.coverage import refresh_for_caller
|
from scribe.services.coverage import refresh_for_caller
|
||||||
|
|
||||||
@@ -617,8 +611,8 @@ async def test_explicit_refresh_names_its_failures(seeded):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_background_seed_is_quiet_without_a_forge(seeded):
|
async def test_background_seed_is_quiet_without_a_forge(seeded):
|
||||||
"""refresh_if_stale (#2802) must exit silently for a forge-less owner —
|
"""refresh_if_stale must exit silently for a forge-less owner — the
|
||||||
rule #115's baseline — and treat a fresh cache as nothing-to-do."""
|
baseline install — and treat a fresh cache as nothing-to-do."""
|
||||||
from scribe.services.coverage import refresh_coverage, refresh_if_stale
|
from scribe.services.coverage import refresh_coverage, refresh_if_stale
|
||||||
|
|
||||||
uid, pid = seeded["uid"], seeded["pid"]
|
uid, pid = seeded["uid"], seeded["pid"]
|
||||||
@@ -651,7 +645,7 @@ async def test_unservable_binding_measures_nothing(seeded):
|
|||||||
assert await compute_coverage(uid, other_pid, selector=_selector(_tarball(TREE))) is None
|
assert await compute_coverage(uid, other_pid, selector=_selector(_tarball(TREE))) is None
|
||||||
|
|
||||||
|
|
||||||
# --- #2792: fingerprints and the proposer's readout --------------------------
|
# --- fingerprints and the proposer's readout --------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_extract_definitions_fingerprints_each_block():
|
def test_extract_definitions_fingerprints_each_block():
|
||||||
@@ -684,14 +678,14 @@ def test_extract_definitions_fingerprints_each_block():
|
|||||||
# And the identity view is unchanged for the hook mirror.
|
# And the identity view is unchanged for the hook mirror.
|
||||||
from scribe.services.coverage import extract_shapes
|
from scribe.services.coverage import extract_shapes
|
||||||
assert extract_shapes(text) == [(d.kind, d.name) for d in extract_definitions(text)]
|
assert extract_shapes(text) == [(d.kind, d.name) for d in extract_definitions(text)]
|
||||||
# A CSS rule's fingerprint is its declarations (#2872): the same body
|
# A CSS rule's fingerprint is its declarations: the same body
|
||||||
# under another selector is the same shape to the derive grouping.
|
# under another selector is the same shape to the derive grouping.
|
||||||
css = ".closed-msg {\n text-align: center;\n padding: 0.5rem 0;\n}\n.error-block {\n text-align: center;\n padding: 0.5rem 0;\n}\n.other {\n text-align: left;\n}\n"
|
css = ".closed-msg {\n text-align: center;\n padding: 0.5rem 0;\n}\n.error-block {\n text-align: center;\n padding: 0.5rem 0;\n}\n.other {\n text-align: left;\n}\n"
|
||||||
d = {x.name: x for x in extract_definitions(css)}
|
d = {x.name: x for x in extract_definitions(css)}
|
||||||
assert d["closed-msg"].body_sha == d["error-block"].body_sha != d["other"].body_sha
|
assert d["closed-msg"].body_sha == d["error-block"].body_sha != d["other"].body_sha
|
||||||
# One-line rules hash their own declarations — never the empty string
|
# One-line rules hash their own declarations — never the empty string,
|
||||||
# (first deploy grouped 68 unrelated one-liners as one copy) — and a
|
# which would group every unrelated one-liner as one copy — and a
|
||||||
# SINGLE declaration is not a shape (#2903): it keeps its selector in the
|
# SINGLE declaration is not a shape: it keeps its selector in the
|
||||||
# hash, so `.a { color: red }` groups only with another `.a`, never with
|
# hash, so `.a { color: red }` groups only with another `.a`, never with
|
||||||
# `.b { color: red }`. Two declarations and up stay selector-agnostic.
|
# `.b { color: red }`. Two declarations and up stay selector-agnostic.
|
||||||
one = ".a { color: red; }\n\n.b { color: red; }\n\n.c { color: blue; }\n\n.a {\n color: red;\n}\n"
|
one = ".a { color: red; }\n\n.b { color: red; }\n\n.c { color: blue; }\n\n.a {\n color: red;\n}\n"
|
||||||
@@ -718,7 +712,7 @@ def test_coverage_line_names_the_proposers_standing():
|
|||||||
assert "; 90 unclassified (40 proposed, 2 derive groups), largest: src" in line
|
assert "; 90 unclassified (40 proposed, 2 derive groups), largest: src" in line
|
||||||
line = coverage_line({**base, "proposed": 0, "derive_groups": [{"group": "a"}]})
|
line = coverage_line({**base, "proposed": 0, "derive_groups": [{"group": "a"}]})
|
||||||
assert "(1 derive group)" in line
|
assert "(1 derive group)" in line
|
||||||
# Milestone 302: a css top copy says what renders it; unused classes
|
# A css top copy says what renders it; unused classes
|
||||||
# join the standing block only when measured (None = no evidence).
|
# join the standing block only when measured (None = no evidence).
|
||||||
line = coverage_line({**base, "unclassified": 0, "proposed": 0, "derive_groups": [
|
line = coverage_line({**base, "unclassified": 0, "proposed": 0, "derive_groups": [
|
||||||
{"group": "name:css:error-msg", "label": ".error-msg", "files": 6,
|
{"group": "name:css:error-msg", "label": ".error-msg", "files": 6,
|
||||||
@@ -729,7 +723,7 @@ def test_coverage_line_names_the_proposers_standing():
|
|||||||
{"group": "name:css:x", "label": ".x", "files": 2,
|
{"group": "name:css:x", "label": ".x", "files": 2,
|
||||||
"consumers": {"count": 1, "paths": ["a.vue"]}}], "unused_css": None})
|
"consumers": {"count": 1, "paths": ["a.vue"]}}], "unused_css": None})
|
||||||
assert "top copy .x ×2 files · used by 1 template" in line and "unused" not in line
|
assert "top copy .x ×2 files · used by 1 template" in line and "unused" not in line
|
||||||
# #2874: the next action on the line — biggest canon queue, widest copy.
|
# The next action on the line — biggest canon queue, widest copy.
|
||||||
line = coverage_line({
|
line = coverage_line({
|
||||||
**base, "proposed": 40, "top_canon": {"snippet_id": 2844, "count": 78},
|
**base, "proposed": 40, "top_canon": {"snippet_id": 2844, "count": 78},
|
||||||
"derive_groups": [{"group": "dup:abc", "label": "closed-msg (identical body)", "files": 3}],
|
"derive_groups": [{"group": "dup:abc", "label": "closed-msg (identical body)", "files": 3}],
|
||||||
@@ -738,7 +732,7 @@ def test_coverage_line_names_the_proposers_standing():
|
|||||||
|
|
||||||
|
|
||||||
def test_coverage_line_shows_standing_work_even_with_nothing_unclassified():
|
def test_coverage_line_shows_standing_work_even_with_nothing_unclassified():
|
||||||
"""#2899: since the scoped bucket a ledger can be fully accounted and
|
"""A ledger can be fully accounted and
|
||||||
still carry derive groups / proposals / divergence — the line names
|
still carry derive groups / proposals / divergence — the line names
|
||||||
them as `standing:` instead of hiding them behind the todo count, and
|
them as `standing:` instead of hiding them behind the todo count, and
|
||||||
names the drift since the previous refresh first-copy-first."""
|
names the drift since the previous refresh first-copy-first."""
|
||||||
@@ -789,7 +783,7 @@ def test_coverage_line_names_divergence_and_recheck():
|
|||||||
|
|
||||||
|
|
||||||
def test_scoped_definitions_are_vue_script_setup_and_scoped_style_only():
|
def test_scoped_definitions_are_vue_script_setup_and_scoped_style_only():
|
||||||
"""#2869: one-offs by construction — every sym in a .vue and every css
|
"""One-offs by construction — every sym in a .vue and every css
|
||||||
rule inside <style scoped>; an unscoped <style> block and non-.vue files
|
rule inside <style scoped>; an unscoped <style> block and non-.vue files
|
||||||
stay ordinary."""
|
stay ordinary."""
|
||||||
from scribe.services.coverage import extract_definitions, scoped_definitions
|
from scribe.services.coverage import extract_definitions, scoped_definitions
|
||||||
@@ -816,10 +810,10 @@ def test_scoped_definitions_are_vue_script_setup_and_scoped_style_only():
|
|||||||
|
|
||||||
|
|
||||||
def test_svelte_scopes_by_default_and_its_module_script_stays_importable():
|
def test_svelte_scopes_by_default_and_its_module_script_stays_importable():
|
||||||
"""#4608: Svelte scopes the other way round from Vue — every <style> is
|
"""Svelte scopes the other way round from Vue — every <style> is the
|
||||||
the component's own unless it opts out, and only <script module> /
|
component's own unless it opts out, and only <script module> /
|
||||||
<script context="module"> can be imported elsewhere. Without this a
|
<script context="module"> can be imported elsewhere. Without this a
|
||||||
Svelte project's every `.card` and `.title` sat in the human todo."""
|
Svelte project's every `.card` and `.title` sits in the human todo."""
|
||||||
from scribe.services.coverage import extract_definitions, scoped_definitions
|
from scribe.services.coverage import extract_definitions, scoped_definitions
|
||||||
svelte = (
|
svelte = (
|
||||||
"<script module lang=\"ts\">\n"
|
"<script module lang=\"ts\">\n"
|
||||||
@@ -847,8 +841,8 @@ def test_svelte_scopes_by_default_and_its_module_script_stays_importable():
|
|||||||
|
|
||||||
|
|
||||||
def test_the_line_names_the_review_queue_and_the_tool_that_holds_it():
|
def test_the_line_names_the_review_queue_and_the_tool_that_holds_it():
|
||||||
"""#4608: `stamps_to_review` existed and two projects never called it,
|
"""`stamps_to_review` goes uncalled unless something on arrival says
|
||||||
because nothing on arrival said there was anything to review."""
|
there is anything to review."""
|
||||||
from scribe.services.coverage import coverage_line
|
from scribe.services.coverage import coverage_line
|
||||||
base = {"accounted": 10, "total": 10, "counts": {"exempt": 10},
|
base = {"accounted": 10, "total": 10, "counts": {"exempt": 10},
|
||||||
"unclassified": 0, "computed_at": "2026-09-30T00:00:00"}
|
"unclassified": 0, "computed_at": "2026-09-30T00:00:00"}
|
||||||
@@ -860,7 +854,7 @@ def test_the_line_names_the_review_queue_and_the_tool_that_holds_it():
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_binding_ref_is_the_branch_the_ledger_follows(seeded):
|
async def test_binding_ref_is_the_branch_the_ledger_follows(seeded):
|
||||||
"""#2873: a binding that names a ref is read at that ref (not the forge's
|
"""A binding that names a ref is read at that ref (not the forge's
|
||||||
default branch); "" clears it; None on a re-bind leaves it standing."""
|
default branch); "" clears it; None on a re-bind leaves it standing."""
|
||||||
from scribe.services.coverage import compute_coverage
|
from scribe.services.coverage import compute_coverage
|
||||||
from scribe.services.repo_bindings import bindings_for_project, set_binding
|
from scribe.services.repo_bindings import bindings_for_project, set_binding
|
||||||
@@ -894,14 +888,14 @@ async def test_binding_ref_is_the_branch_the_ledger_follows(seeded):
|
|||||||
("web/button.css", ".btn {\n color: red;\n}\n", False),
|
("web/button.css", ".btn {\n color: red;\n}\n", False),
|
||||||
])
|
])
|
||||||
def test_a_file_is_a_unit_when_it_renders_and_nothing_inside_carries_its_name(path, text, unit):
|
def test_a_file_is_a_unit_when_it_renders_and_nothing_inside_carries_its_name(path, text, unit):
|
||||||
"""Milestone 439: one structural test, not a framework list."""
|
"""One structural test, not a framework list."""
|
||||||
from scribe.services.coverage import class_references, extract_definitions, is_file_unit
|
from scribe.services.coverage import class_references, extract_definitions, is_file_unit
|
||||||
|
|
||||||
assert is_file_unit(path, text, extract_definitions(text), class_references(path, text)) is unit
|
assert is_file_unit(path, text, extract_definitions(text), class_references(path, text)) is unit
|
||||||
|
|
||||||
|
|
||||||
def test_the_line_says_whether_writers_judged_what_they_wrote():
|
def test_the_line_says_whether_writers_judged_what_they_wrote():
|
||||||
"""Milestone 439: the write-time figure — silent until the question has
|
"""The write-time figure — silent until the question has
|
||||||
been put, then turns checked, asked, and asks walked past."""
|
been put, then turns checked, asked, and asks walked past."""
|
||||||
from scribe.services.coverage import coverage_line
|
from scribe.services.coverage import coverage_line
|
||||||
base = {"accounted": 10, "total": 10, "counts": {"exempt": 10},
|
base = {"accounted": 10, "total": 10, "counts": {"exempt": 10},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""`retrieval_telemetry` says what is wrong, rather than implying it (#3431).
|
"""`retrieval_telemetry` says what is wrong, rather than implying it.
|
||||||
|
|
||||||
WHY THIS IS TESTED AS ARITHMETIC AND NOT THROUGH THE DATABASE. The warnings
|
WHY THIS IS TESTED AS ARITHMETIC AND NOT THROUGH THE DATABASE. The warnings
|
||||||
are a pure function of the blocks the readout already built — that is the
|
are a pure function of the blocks the readout already built — that is the
|
||||||
@@ -8,12 +8,12 @@ the wrong shape. Every case below is a shape that once produced, or would
|
|||||||
produce, a wrong reading:
|
produce, a wrong reading:
|
||||||
|
|
||||||
* an arm that never declines, which is either a missing floor or a missing
|
* an arm that never declines, which is either a missing floor or a missing
|
||||||
LOG (#3497 — the rule arms recorded only their hits, so their decline
|
LOG (an arm that records only its hits has a structurally zero decline
|
||||||
count was structurally zero and the obvious warning would have sent a
|
count, and the obvious warning would send a reader to move a threshold
|
||||||
reader to move a threshold that was never involved);
|
that was never involved);
|
||||||
* a search that never declines, which is a search working correctly;
|
* a search that never declines, which is a search working correctly;
|
||||||
* a quiet window, where firing every check would describe an empty database
|
* a quiet window, where firing every check would describe an empty database
|
||||||
rather than a broken one (rule 115);
|
rather than a broken one;
|
||||||
* a floor the band is sitting on, which is the case where tuning changes
|
* a floor the band is sitting on, which is the case where tuning changes
|
||||||
volume while looking like it changes quality.
|
volume while looking like it changes quality.
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ def test_the_warning_carries_the_numbers_that_produced_it() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_a_quiet_window_flags_nothing() -> None:
|
def test_a_quiet_window_flags_nothing() -> None:
|
||||||
"""Three calls returning nothing is an afternoon, not a defect (rule 115)."""
|
"""Three calls returning nothing is an afternoon, not a defect."""
|
||||||
assert codes(warn({"auto_inject": src(calls=3, zero_result_calls=0)})) == set()
|
assert codes(warn({"auto_inject": src(calls=3, zero_result_calls=0)})) == set()
|
||||||
|
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ def test_an_arm_that_does_decline_is_not_flagged() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_an_arm_not_known_to_log_unconditionally_is_exempt(monkeypatch) -> None:
|
def test_an_arm_not_known_to_log_unconditionally_is_exempt(monkeypatch) -> None:
|
||||||
"""#3497: a structurally-zero decline count is a logging bug, not a floor.
|
"""A structurally-zero decline count is a logging bug, not a floor.
|
||||||
|
|
||||||
Flagging it would report a ranking problem and send the reader to a
|
Flagging it would report a ranking problem and send the reader to a
|
||||||
threshold that was never involved.
|
threshold that was never involved.
|
||||||
@@ -183,7 +183,7 @@ def test_records_shown_and_never_opened_are_reported_per_corpus() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_each_corpus_is_sent_only_to_a_tool_that_takes_it() -> None:
|
def test_each_corpus_is_sent_only_to_a_tool_that_takes_it() -> None:
|
||||||
"""#4798: one remedy for both corpora sent the rules reader to
|
"""One remedy for both corpora would send the rules reader to
|
||||||
`menus_to_review`, which refuses every source but the notes menu. Tied to
|
`menus_to_review`, which refuses every source but the notes menu. Tied to
|
||||||
`REVIEWABLE` itself, so it goes red in either direction: the rules text
|
`REVIEWABLE` itself, so it goes red in either direction: the rules text
|
||||||
naming the tool while no rule arm is reviewable, or a rule arm becoming
|
naming the tool while no rule arm is reviewable, or a rule arm becoming
|
||||||
@@ -231,7 +231,7 @@ def test_an_unregistered_source_gets_no_other_verdict() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_every_registered_source_stays_quiet_when_healthy() -> None:
|
def test_every_registered_source_stays_quiet_when_healthy() -> None:
|
||||||
"""The negative control for the whole suite (rule 167).
|
"""The negative control for the whole suite.
|
||||||
|
|
||||||
A guard that cannot pass cleanly is not a guard — if a healthy window
|
A guard that cannot pass cleanly is not a guard — if a healthy window
|
||||||
produced warnings, every assertion above would be meaningless.
|
produced warnings, every assertion above would be meaningless.
|
||||||
@@ -256,7 +256,7 @@ def test_a_point_that_emitted_is_not_reported() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_a_deliberately_quiet_point_is_never_reported() -> None:
|
def test_a_deliberately_quiet_point_is_never_reported() -> None:
|
||||||
"""A justified silence must not read as a gap (#2475)."""
|
"""A justified silence must not read as a gap."""
|
||||||
quiet = {p["source"] for p in _silent_surfaces({}, {}, {}, active=True)}
|
quiet = {p["source"] for p in _silent_surfaces({}, {}, {}, active=True)}
|
||||||
for web_only in ("rest_search", "browse_search", "rest_note", "rest_rule"):
|
for web_only in ("rest_search", "browse_search", "rest_note", "rest_rule"):
|
||||||
assert web_only not in quiet
|
assert web_only not in quiet
|
||||||
@@ -264,7 +264,7 @@ def test_a_deliberately_quiet_point_is_never_reported() -> None:
|
|||||||
|
|
||||||
def test_an_inactive_window_reports_no_silence_at_all() -> None:
|
def test_an_inactive_window_reports_no_silence_at_all() -> None:
|
||||||
"""On a fresh install every point is silent; the list would be the
|
"""On a fresh install every point is silent; the list would be the
|
||||||
registry printed back (rule 115)."""
|
registry printed back."""
|
||||||
assert _silent_surfaces({}, {}, {}, active=False) == []
|
assert _silent_surfaces({}, {}, {}, active=False) == []
|
||||||
|
|
||||||
|
|
||||||
@@ -292,14 +292,13 @@ def test_a_setting_falls_back_rather_than_raising(raw, fallback, want) -> None:
|
|||||||
assert _num(raw, fallback) == want
|
assert _num(raw, fallback) == want
|
||||||
|
|
||||||
|
|
||||||
# ── read_and_unacted / outcomes_never_recorded (#4213, milestone 419) ─────
|
# ── read_and_unacted / outcomes_never_recorded ──────────────────────────
|
||||||
#
|
#
|
||||||
# The pair exists because ZERO OUTCOMES IS AMBIGUOUS, and getting that wrong
|
# The pair exists because ZERO OUTCOMES IS AMBIGUOUS: a window with no outcome
|
||||||
# would have been this milestone's own failure mode in miniature: a window
|
# rows cannot tell "every rule was ignored" from "nothing reports outcomes
|
||||||
# with no outcome rows cannot tell "every rule was ignored" from "nothing
|
# yet". Reporting the first when the truth is the second manufactures a
|
||||||
# reports outcomes yet". Reporting the first when the truth is the second
|
# finding out of an unwired feature — a statistic that cannot vary, read as a
|
||||||
# manufactures a finding out of an unwired feature — #3311, where a statistic
|
# fact about the corpus.
|
||||||
# that could not vary was read as a fact about the corpus.
|
|
||||||
|
|
||||||
def ru(**kw) -> dict:
|
def ru(**kw) -> dict:
|
||||||
base = {
|
base = {
|
||||||
@@ -358,7 +357,7 @@ def test_every_opened_rule_acted_on_reports_nothing() -> None:
|
|||||||
def test_no_rules_opened_at_all_reports_neither() -> None:
|
def test_no_rules_opened_at_all_reports_neither() -> None:
|
||||||
"""Silence is not a finding. A window where nothing was opened has nothing
|
"""Silence is not a finding. A window where nothing was opened has nothing
|
||||||
to say about outcomes, and saying it anyway would put a warning on every
|
to say about outcomes, and saying it anyway would put a warning on every
|
||||||
fresh install (rule 115)."""
|
fresh install."""
|
||||||
ws = warn({}, rule_usage=ru(distinct_rules_surfaced=12))
|
ws = warn({}, rule_usage=ru(distinct_rules_surfaced=12))
|
||||||
assert "read_and_unacted" not in codes(ws)
|
assert "read_and_unacted" not in codes(ws)
|
||||||
assert "outcomes_never_recorded" not in codes(ws)
|
assert "outcomes_never_recorded" not in codes(ws)
|
||||||
@@ -367,7 +366,7 @@ def test_no_rules_opened_at_all_reports_neither() -> None:
|
|||||||
def test_an_absent_rule_usage_block_is_not_a_finding() -> None:
|
def test_an_absent_rule_usage_block_is_not_a_finding() -> None:
|
||||||
"""A failed rule-usage read leaves the keys missing or zero. Neither may
|
"""A failed rule-usage read leaves the keys missing or zero. Neither may
|
||||||
become a warning, because a warning computed over rows that could not be
|
become a warning, because a warning computed over rows that could not be
|
||||||
loaded describes the outage, not the corpus (#2663)."""
|
loaded describes the outage, not the corpus."""
|
||||||
assert "read_and_unacted" not in codes(warn({}, rule_usage={}))
|
assert "read_and_unacted" not in codes(warn({}, rule_usage={}))
|
||||||
assert "outcomes_never_recorded" not in codes(warn({}, rule_usage={}))
|
assert "outcomes_never_recorded" not in codes(warn({}, rule_usage={}))
|
||||||
assert "outcomes_never_recorded" not in codes(
|
assert "outcomes_never_recorded" not in codes(
|
||||||
@@ -375,7 +374,7 @@ def test_an_absent_rule_usage_block_is_not_a_finding() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── floor_moved_mid_window (#4225) ────────────────────────────────────────
|
# ── floor_moved_mid_window ────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# WHY THE BAND CHECK IS SUSPENDED RATHER THAN SOFTENED.
|
# WHY THE BAND CHECK IS SUSPENDED RATHER THAN SOFTENED.
|
||||||
#
|
#
|
||||||
@@ -481,16 +480,10 @@ def test_a_quiet_arm_is_not_suspended_either_way() -> None:
|
|||||||
# floor row, "no move recorded" and "no move" are different statements, and
|
# floor row, "no move recorded" and "no move" are different statements, and
|
||||||
# the first was standing in for the second.
|
# the first was standing in for the second.
|
||||||
#
|
#
|
||||||
# It showed up the day this instance's ledger was seeded. Its earliest event
|
# The case: an arm whose first floor row is dated inside the window. Without
|
||||||
# of any kind is 2026-09-17; `write_path_rule`'s only floor row is a release
|
# this check the readout prints its band as a negative distance above its
|
||||||
# baseline written 2026-09-21; a 30-day window opens 2026-08-22. The readout
|
# floor — the impossible number the section above exists to prevent — with
|
||||||
# printed that arm's band as "-0.0208 above its floor" — the impossible
|
# the suspension silent, because there is nothing on record for it to notice.
|
||||||
# negative the section above exists to prevent — with the suspension silent,
|
|
||||||
# because there was nothing on record for it to notice.
|
|
||||||
#
|
|
||||||
# The distinction is not academic for #4261: every window for the next month
|
|
||||||
# opens before that date, and measuring a change against an instrument that
|
|
||||||
# reports a number it cannot support is the #4225 trap one level up.
|
|
||||||
|
|
||||||
GAP_FROM = "2026-09-21T11:58:00+00:00"
|
GAP_FROM = "2026-09-21T11:58:00+00:00"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user