feat(rules)!: retire the always-on tier — every rule arrives by retrieval (#394)
CI & Build / Python lint (push) Failing after 3s
CI & Build / Plugin hooks (push) Failing after 12s
CI & Build / integration (push) Failing after 27s
CI & Build / TypeScript typecheck (push) Failing after 35s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Failing after 3s
CI & Build / Plugin hooks (push) Failing after 12s
CI & Build / integration (push) Failing after 27s
CI & Build / TypeScript typecheck (push) Failing after 35s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped
Milestone 394, steps 5-8. Operator: "remove the always on rule functionality as the goal was to not have it at all since it didn't seem to work as expected." Unconditional preload had three failures the retrieval arms do not. It could not be MEASURED — a resident rule is in the context whether or not it mattered, so nothing distinguished "this governed the act" from "this was scenery", and it was the one surface structurally exempt from the scoreboard judging every other. It was SUMMARISED AWAY by compaction while the session went on believing it held the rules. And it CROWDED OUT the few rules that applied with the thirty that did not. WHAT GOES Schema (0100): rules.tier + ck_rules_tier, rule_versions.tier, rulebooks.always_on, and project_rulebook_exclusions — a table recording a project's opt-out of something that no longer binds it unasked. Tools: list_always_on_rules, exclude_always_on_rulebook, include_always_on_rulebook. Service: the same three plus rules_etag_for, _valid_tier and the whole etag family. The SessionStart preload and the write-path staleness arm go with them: nothing is resident, so nothing can have drifted since a session loaded it. THREE CALLS WORTH REVIEWING enter_project got NARROWER, not wider. Its filter was `always_on OR area-tagged`; dropping the tier arm leaves the deterministic half, so a project with no canonical-tagged Systems gets no bulk rules and reaches them by retrieval instead. Dropping the whole clause would have made that payload bigger than the preload this milestone deletes. Backups import tolerantly. A pre-394 archive carries tier, always_on and the retired inception choice; none is read, and the exclusion key is DROPPED rather than remapped, because restoring it would write data that validate_inception now rejects as unknown. The migration is irreversible in the way that matters and says so: downgrade recreates the columns at their defaults and cannot restore which rules were always-on. A value invented to fill a hole is not a measurement. THE INSTRUCTION SURFACES SAY THE HARDER THING Deleting "call list_always_on_rules()" is easy; replacing it is not, because the new model asks a session to trust something it cannot see. All three surfaces now say a session holds nothing, that rules arrive when work matches them, and — the half that got dangerous — that "no rule arrived" means "nothing matched", never "there is no rule". Under residency an empty session was rare and suspicious; it is now the ordinary state of most turns, so reading it as permission is wrong on nearly every turn rather than occasionally. That is #3720's defect at session scale. test_instruction_surfaces_agree is repointed rather than retired: its two halves collapsed into one instruction, and it gains a guard that every surface states what absence means. _INSTRUCTIONS is back at 1999/2000 — the inception clause paid for the longer HOW line. UI (rule 27, and the opportunity step 8 named) The tier selector is gone, and what replaces it is the point: `when_to_apply` is now the field that decides whether a rule is ever seen, so the editor marks it required, warns while it is empty, and both rule lists badge a trigger-less rule "never surfaces". A rule without one is not quiet, it is unreachable. TESTS Two files deleted outright — test_rules_etag.py and test_inception_rules.py tested subsystems that no longer exist. Elsewhere obsolete cases were removed and the rest repointed. One deserves naming: the wiring test asserted the act arms pass no `tier`, which had become an assertion that could not fail. It is repointed onto `kind`, which does still exist and where the same claim is live — a preference must reach a write exactly as a rule does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
This commit is contained in:
@@ -1,22 +1,27 @@
|
||||
"""Real-Postgres tests for WHICH rules reach a session (milestone 307 step 5).
|
||||
"""Real-Postgres tests for WHICH rules reach a session (milestone 307 step 5,
|
||||
narrowed by 394).
|
||||
|
||||
What mocks can't prove, and what this milestone must not get wrong:
|
||||
What mocks can't prove, and what this design must not get wrong:
|
||||
|
||||
1. **Nothing stops binding.** A rule with no tier, no areas and no edges
|
||||
behaves exactly as it did before tiers existed. That is the one failure this
|
||||
whole design must not produce, and it is asserted first.
|
||||
2. A conditional rule is invisible to a project that doesn't work in its area,
|
||||
and arrives — binding, not suggested — to one that does.
|
||||
3. A `co_surfaces` partner arrives with its other half, which is the failure
|
||||
1. A rule is invisible to a project that doesn't work in its area, and
|
||||
arrives — binding, not suggested — to one that does. Area matching is
|
||||
DETERMINISTIC: a tag comparison, never a similarity score.
|
||||
2. A `co_surfaces` partner arrives with its other half, which is the failure
|
||||
that made merging rule 144 into rule 46 look like the only fix.
|
||||
4. An explicit suppression outranks an edge.
|
||||
3. An explicit suppression outranks an edge.
|
||||
|
||||
TWO CLAIMS WERE DROPPED HERE BY MILESTONE 394, and it is worth saying which
|
||||
rather than leaving a shorter list. "A rule with no tier binds exactly as
|
||||
before" and "a conditional rule is reachable, not resident" were both about
|
||||
the always-on tier. There is no tier and no resident payload, so neither
|
||||
states anything that can now be true or false — they were not failing, they
|
||||
had stopped being claims.
|
||||
"""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.rulebook import Rulebook
|
||||
from scribe.services import canonical_systems as canonical_svc
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from scribe.services import systems as systems_svc
|
||||
@@ -27,12 +32,13 @@ pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine"
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def world():
|
||||
"""A project with TWO rulebooks, because the two payloads are different sets.
|
||||
"""A project with two rulebooks — one subscribed, one not.
|
||||
|
||||
`list_always_on_rules` covers always-on rulebooks; `get_applicable_rules`
|
||||
covers SUBSCRIBED ones. Conflating them is easy and would make these tests
|
||||
assert nothing, so the fixture carries one of each and every test says
|
||||
which payload it is about.
|
||||
Both are ordinary rulebooks since milestone 394; the fixture used to flag
|
||||
one always-on because that was a second, separate way to reach a project.
|
||||
Keeping two is still worth it: a rulebook nobody subscribed to must
|
||||
contribute nothing, and a fixture with only the subscribed one could not
|
||||
tell "correctly scoped" from "returns everything".
|
||||
"""
|
||||
async with async_session() as s:
|
||||
owner = await ensure_user(s, "surfacing_owner")
|
||||
@@ -43,10 +49,6 @@ async def world():
|
||||
await s.commit()
|
||||
|
||||
always = await rulebooks_svc.create_rulebook(ids["owner"], "Family standards")
|
||||
async with async_session() as s:
|
||||
rb = await s.get(Rulebook, always.id)
|
||||
rb.always_on = True
|
||||
await s.commit()
|
||||
always_topic = await rulebooks_svc.create_topic(always.id, ids["owner"], "git")
|
||||
await rulebooks_svc.create_rule(
|
||||
always_topic.id, ids["owner"], "dev is home", "Work on dev.",
|
||||
@@ -72,39 +74,8 @@ async def _titles(ids) -> set[str]:
|
||||
return {r["title"] for r in applicable["rules"]}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_a_rule_with_no_tier_no_areas_and_no_edges_binds_exactly_as_before(world):
|
||||
"""THE compatibility guarantee. An install upgrades and every rule it
|
||||
already had keeps arriving — no tier set, no areas, no edges, still bound.
|
||||
Getting this wrong would silently stop enforcing rules people rely on,
|
||||
which is worse than any amount of payload bloat."""
|
||||
always_on = await rulebooks_svc.list_always_on_rules(world["owner"])
|
||||
assert "dev is home" in {r.title for r in always_on}
|
||||
assert "Between batches, keep stacking" in await _titles(world)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_a_conditional_rule_is_reachable_not_resident(world):
|
||||
"""It leaves the session-start payload entirely — that is the point of the
|
||||
tier — and it does NOT reach a project with no matching area."""
|
||||
# In the ALWAYS-ON book: the tier alone keeps it out of the session-start
|
||||
# payload, which is the whole point of the tier.
|
||||
resident = await rulebooks_svc.create_rule(
|
||||
world["always_topic"], world["owner"], "Release tagging", "Derive the tag.",
|
||||
when_to_apply="when cutting a release", tier="conditional",
|
||||
)
|
||||
assert resident.tier == "conditional"
|
||||
always_on = await rulebooks_svc.list_always_on_rules(world["owner"])
|
||||
assert "Release tagging" not in {r.title for r in always_on}
|
||||
|
||||
# In the SUBSCRIBED book, untagged: the project has no area to reach it by,
|
||||
# so it stays out of the project payload too. Absent for a DIFFERENT reason
|
||||
# than above, which is why both are asserted.
|
||||
await rulebooks_svc.create_rule(
|
||||
world["topic"], world["owner"], "Untagged conditional", "No area yet.",
|
||||
when_to_apply="sometime", tier="conditional",
|
||||
)
|
||||
assert "Untagged conditional" not in await _titles(world)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@@ -117,7 +88,7 @@ async def test_a_conditional_rule_binds_a_project_that_works_in_its_area(world):
|
||||
|
||||
rule = await rulebooks_svc.create_rule(
|
||||
world["topic"], world["owner"], "Release tagging", "Derive the tag.",
|
||||
when_to_apply="when cutting a release", tier="conditional",
|
||||
when_to_apply="when cutting a release",
|
||||
)
|
||||
await rulebooks_svc.set_rule_systems(rule.id, world["owner"], [area.id])
|
||||
|
||||
@@ -145,7 +116,7 @@ async def test_co_surfaces_drags_in_the_half_that_would_have_been_missed(world):
|
||||
partner = await rulebooks_svc.create_rule(
|
||||
world["topic"], world["owner"], "Version names are labels",
|
||||
"A name decides nothing.",
|
||||
when_to_apply="when naming a build", tier="conditional",
|
||||
when_to_apply="when naming a build",
|
||||
)
|
||||
await rulebooks_svc.add_rule_relation(
|
||||
world["owner"], world["plain"], partner.id, "co_surfaces",
|
||||
@@ -163,7 +134,6 @@ async def test_a_suppression_outranks_an_edge(world):
|
||||
does not want that one. An explicit decision beats an inferred one."""
|
||||
partner = await rulebooks_svc.create_rule(
|
||||
world["topic"], world["owner"], "Muted partner", "Should not arrive.",
|
||||
tier="conditional",
|
||||
)
|
||||
await rulebooks_svc.add_rule_relation(
|
||||
world["owner"], world["plain"], partner.id, "co_surfaces",
|
||||
|
||||
Reference in New Issue
Block a user