feat(rules): a preference updates without asking, and says what taught it (#3849 step 2)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 1m7s
CI & Build / integration (push) Successful in 1m8s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 33s
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 1m7s
CI & Build / integration (push) Successful in 1m8s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 33s
The write path, and the step where a preference stops being a relabelled rule. `create_preference` / `update_preference` on the MCP surface, plus `kind` on update_rule and both HTTP doors. SEPARATE TOOLS, NOT A `kind=` ARGUMENT. create_rule's docstring IS the approval gate (#3557): propose, offer three answers, wait. That is right for a rule — the person it binds should have agreed. A preference inverts it, and one reached through create_rule would be read through that prose, so the caller would hesitate over exactly the act this kind exists to make routine. Two doors, two contracts, one table. Reads stay shared: a preference IS a rule row, and "what governs this" wants both. Two required fields, each buying something: - `when_to_apply`, because the trigger is two-thirds of the embedded document. Without one the record is written, stored, and silently never delivered — indistinguishable from one nobody wrote. - `arose_from_id`, the price of the ungated write. A corpus that drifts with no record of what taught each change cannot be audited, and the operator's veto over drift is worth exactly as much as their ability to read why it happened. The near-duplicate gate is what lets this corpus be written freely and stay small: the second preference about a thing updates the first. It is title-scoped and kind-blind, so it also catches a preference restating a rule that already binds. The asymmetry is guarded as two PRESENCE facts — the rule door still asks, the preference door still says write it — never as an absence. An absence check passes against a docstring that was deleted or rewritten into something else, which is snippet #3352's warning and would read as coverage here while proving nothing. `_plain_detail` moved to tests/helpers on its second copy, per that module's own reason for existing (#2825). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
This commit is contained in:
@@ -226,6 +226,12 @@ def fake_rule(**attrs) -> MagicMock:
|
||||
# `when_to_apply` and `arose_from_id` would be truthy MagicMocks and
|
||||
# rule_brief would attach both keys on every stand-in.
|
||||
"when_to_apply": None, "tier": "always_on", "arose_from_id": None,
|
||||
# Named for the same reason one line up, and it bites harder here.
|
||||
# `rule_brief` and `to_dict` both emit `kind or "rule"`, and a
|
||||
# MagicMock is truthy — so an unnamed `kind` would put a MagicMock
|
||||
# where every payload promises a force, and every stand-in rule would
|
||||
# read as neither a rule nor a preference.
|
||||
"kind": "rule",
|
||||
# Same reason, and the same trap one field further on: an unnamed
|
||||
# `verify_with` is a truthy MagicMock, so every stand-in rule would
|
||||
# claim to carry a check and rule_brief would stamp a MagicMock date
|
||||
@@ -235,6 +241,25 @@ def fake_rule(**attrs) -> MagicMock:
|
||||
}, attrs)
|
||||
|
||||
|
||||
def plain_rule_detail():
|
||||
"""Stub `rulebooks_svc.rule_detail` down to the record's own dict.
|
||||
|
||||
Every rule-tool unit test needs it and none of them wants it: the real
|
||||
`rule_detail` reads the rule's Systems and its typed edges from the
|
||||
database, which a unit test has none of. What these tests assert is that
|
||||
the TOOL forwarded the right arguments, so the seam is stubbed the same
|
||||
way the create/update calls themselves already are.
|
||||
|
||||
Consolidated here on its second copy, per this module's own reason for
|
||||
existing (#2825) — two stubs for one seam drift apart quietly, and a test
|
||||
stubbing the seam slightly differently is a test asserting something
|
||||
slightly different than it appears to.
|
||||
"""
|
||||
async def _detail(_uid, rule, _system_ids=None):
|
||||
return rule.to_dict()
|
||||
return patch("scribe.mcp.tools.rulebooks.rulebooks_svc.rule_detail", _detail)
|
||||
|
||||
|
||||
class FakeMCP:
|
||||
"""Stand-in for the FastMCP server a tool module's ``register(mcp)`` is
|
||||
handed: records the ``name=`` of every ``@mcp.tool(...)`` registration in
|
||||
|
||||
Reference in New Issue
Block a user