Preferences — a rule kind that Scribe keeps up to date (#3849 steps 1–3) #150
@@ -859,7 +859,13 @@ async def get_writepath_config(user_id: int) -> dict:
|
||||
}
|
||||
|
||||
def _rule_hint_line(rule, *, where: str, seen: bool) -> str:
|
||||
"""One rule hint line — both arms, both tails (#3750).
|
||||
"""One rule hint line — both arms, both tails, both kinds (#3750, #3849).
|
||||
|
||||
TWO INDEPENDENT AXES. `kind` decides the head, `seen` decides the tail,
|
||||
and neither reads the other. A preference and a rule differ in force; a
|
||||
repeat and a first surfacing differ in whether the session already holds
|
||||
the line. Those are unrelated facts, and keeping them unrelated in the
|
||||
code is what stopped the second kind from reopening the repeat question.
|
||||
|
||||
ONE FUNCTION BECAUSE THE TAILS MUST NOT DRIFT. The two arms phrase their
|
||||
heads differently ("may apply here" vs "may apply to this Bash call") and
|
||||
@@ -891,15 +897,38 @@ def _rule_hint_line(rule, *, where: str, seen: bool) -> str:
|
||||
managing it.
|
||||
"""
|
||||
trigger = (rule.when_to_apply or "").strip()
|
||||
preference = rule.kind == "preference"
|
||||
# KIND CHANGES THE HEAD; `seen` CHANGES THE TAIL. The two axes are
|
||||
# independent and stay that way, which is what lets the repeat logic above
|
||||
# survive a second kind without being reasoned about again: whether a
|
||||
# record is already on the ledger has nothing to do with how much force it
|
||||
# carries, so the seen-branch is shared verbatim.
|
||||
#
|
||||
# The noun is the whole of the visual difference, and that is deliberate.
|
||||
# A reader skimming an injected block gets one word to place the register \u2014
|
||||
# so it is the SECOND word that moves, and it is the word naming force.
|
||||
# Everything structural after it is identical, so the three kinds read as
|
||||
# one set rather than three formats (milestone 385's step 5 writes the
|
||||
# lesson voice against these two; they are one paragraph, not three).
|
||||
noun = "Preference" if preference else "Standing rule"
|
||||
# The only other place force is asserted. 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 is what keeps
|
||||
# things consistent rather than what keeps them correct.
|
||||
reason = (
|
||||
"for how this has been done before" if preference
|
||||
else "before deciding it does not apply"
|
||||
)
|
||||
tail = (
|
||||
f"You saw it earlier this session; pull it with get_rule({rule.id}) "
|
||||
"if you no longer hold it."
|
||||
if seen else
|
||||
f"Read it with get_rule({rule.id}) before deciding it does not "
|
||||
"apply; it is not in this session's loaded set."
|
||||
f"Read it with get_rule({rule.id}) {reason}; it is not in this "
|
||||
"session's loaded set."
|
||||
)
|
||||
return (
|
||||
f"Standing rule that may apply {where} \u2014 \u201c{rule.title}\u201d"
|
||||
f"{noun} that may apply {where} \u2014 \u201c{rule.title}\u201d"
|
||||
+ (f" ({trigger})" if trigger else "")
|
||||
+ f". {tail}"
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user