feat(rules): a preference does not speak in a rule's voice (#3849 step 3)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m27s
CI & Build / integration (push) Successful in 1m31s
CI & Build / Build & push image (push) Successful in 41s

Two independent axes on one hint line. `kind` decides the head, `seen`
decides the tail, and neither reads the other — which is what let a second
kind arrive without reopening #3750's repeat question. Whether a record is
already on the exclusion ledger has nothing to do with how much force it
carries, so the seen branch is shared verbatim.

The noun carries the whole visual difference, deliberately. A reader skimming
an injected block gets one word to place the register, so the word that moves
is the one naming force: "Standing rule" / "Preference". Everything
structural after it is identical, so the kinds read as one set rather than
two formats.

Force is asserted in exactly one other place, and that moves too. A rule's
line says to read it BEFORE DECIDING IT DOES NOT APPLY, because dismissing a
rule unread is how the thing it prevents happens. A preference makes no such
claim: it says where to find HOW THIS HAS BEEN DONE BEFORE, and following it
buys consistency rather than correctness.

Guarded on both places at once. Pinning the noun alone would pass a line
reading "Preference … before deciding it does not apply" — label swapped,
instruction kept — which is worse than not distinguishing them, because it
looks handled.

And a guard on the independence claim itself, exercising all four
combinations: the way this breaks silently is a seen branch that grows a kind
test, leaving one combination rendered by nobody's intention.

Noted, not fixed: plugin/skills/using-scribe/SKILL.md still says "Standing
rules are binding" with no room for a kind that does not. That surface is
step 6's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
This commit is contained in:
2026-09-10 22:04:14 -04:00
co-authored by Claude Opus 5
parent 89d16d89a9
commit 26e0dff706
2 changed files with 120 additions and 4 deletions
+87
View File
@@ -1006,6 +1006,93 @@ async def test_the_two_tails_are_distinguishable_and_say_the_true_one(source, ru
assert fresh_ctx != seen_ctx, "the two tails collapsed into one"
# ── the third axis: KIND (milestone 399) ────────────────────────────────
#
# A preference is a rule row that does not bind, so it rides the same arms and
# the same line. What must differ is the REGISTER: a rule's line tells the
# reader not to dismiss it unread, because dismissing a rule unread is how the
# thing it prevents happens. A preference makes no such claim — it says where
# to find how this has been done, and following it buys consistency rather
# than correctness.
#
# Rendered in the rule's voice, a preference becomes the thing milestone 399
# exists to avoid: a rule with a different column.
_PREF = fake_rule(
id=177,
kind="preference",
title="Pace hard debugging one step at a time",
statement="Advance one investigative step per turn.",
when_to_apply="during hard debugging",
)
_RULE_FORCE = "before deciding it does not apply"
_PREF_FORCE = "for how this has been done before"
@pytest.mark.parametrize(("source", "run"), _ARMS, ids=["write_path", "pre_tool"])
@pytest.mark.asyncio
async def test_a_preference_does_not_speak_in_the_rules_voice(source, run):
"""The register, pinned on the two places force is actually asserted.
On BOTH the noun and the clause, because either alone is weak. A line
reading "Preference … before deciding it does not apply" has swapped the
label and kept the instruction, which is worse than not distinguishing
them at all: it looks handled.
"""
ctx = (await run([(0.81, _PREF)], MagicMock()))["context"]
assert "Preference that may apply" in ctx, (
f"{source} announced a preference as something else. The noun is the "
f"one word a skimming reader gets to place the register, so it is the "
f"word that has to move. Context was: {ctx!r}"
)
assert _PREF_FORCE in ctx and _RULE_FORCE not in ctx, (
f"{source} told the session to read a PREFERENCE before deciding it "
f"does not apply. That is a rule's claim: it is the sentence that "
f"makes a line bind, and a preference does not."
)
@pytest.mark.parametrize(("source", "run"), _ARMS, ids=["write_path", "pre_tool"])
@pytest.mark.asyncio
async def test_kind_and_seen_do_not_read_each_other(source, run):
"""The structural claim the design rests on: two INDEPENDENT axes.
Whether a record is already on the exclusion ledger has nothing to do with
how much force it carries. Keeping the two unrelated in the code is what
let a second kind arrive without reopening #3750's repeat question — and
the way that silently breaks is a `seen` branch that grows a kind test,
or a kind branch that grows a `seen` test, leaving one of the four
combinations rendered by nobody's intention.
So: all four are exercised, and the seen tail must come out identical for
both kinds.
"""
pref_fresh = (await run([(0.81, _PREF)], MagicMock()))["context"]
pref_seen = (await run([(0.81, _PREF)], MagicMock(),
exclude_rule_ids=[177]))["context"]
rule_seen = (await run([(0.81, _HELD)], MagicMock(),
exclude_rule_ids=[156]))["context"]
assert _SEEN_TAIL in pref_seen and _FRESH_TAIL in pref_fresh, (
f"{source}: the seen/fresh split stopped working once kind was added — "
f"the tail axis is now reading the head axis"
)
# The tail is about the LEDGER, so it is shared verbatim. Compared as the
# tail alone rather than the whole line, since the heads differ by design.
assert _SEEN_TAIL in rule_seen, "the rule's seen tail changed"
assert pref_seen.count(_SEEN_TAIL) == rule_seen.count(_SEEN_TAIL) == 1, (
f"{source} rendered the repeat clause a different number of times for "
f"the two kinds; the tail is about the ledger and does not vary by force"
)
# And the head still differs in the seen case — a repeat of a preference
# is still a preference.
assert "Preference that may apply" in pref_seen, (
f"{source} lost the preference register on a repeat, so a preference "
f"seen twice reads as a rule the second time"
)
@pytest.mark.parametrize(("source", "run"), _ARMS, ids=["write_path", "pre_tool"])
@pytest.mark.asyncio
async def test_neither_tail_injects_the_rule_statement(source, run):