wip(394): steps 6+7 — backend path and instruction surfaces
This commit is contained in:
+17
-17
@@ -38,8 +38,8 @@ from quart import Quart
|
||||
# them) was DECLINED a line, deliberately, by the operator — not overlooked.
|
||||
# The reasoning, so it is not re-litigated blind: this is a map, and its own
|
||||
# closing line says each tool's description carries the full contract. The
|
||||
# sweep is a curation act, not a session-start reflex like enter_project or
|
||||
# list_always_on_rules. Spending the last of the budget on it would leave the
|
||||
# sweep is a curation act, not a session-start reflex like enter_project.
|
||||
# Spending the last of the budget on it would leave the
|
||||
# map unable to grow for something more central later.
|
||||
#
|
||||
# The accepted cost: an agent that never opens create_note's docstring never
|
||||
@@ -59,15 +59,15 @@ from quart import Quart
|
||||
# - What it bought is not per-tool guidance and has nowhere else to live at
|
||||
# session-start altitude. Rules were retrievable only by RESIDENCY: the
|
||||
# always-on preload put them in front of the agent, and nothing told a
|
||||
# session to go looking for one it had not been handed. The tier split is
|
||||
# therefore load-bearing on ANY install (rule 115): a delivered rule costs
|
||||
# tokens in every session forever, so a rulebook that only delivers cannot
|
||||
# grow past what one session can hold, and every rule worth keeping has to
|
||||
# become resident to bind at all. Retrieval is what lets it keep growing —
|
||||
# and retrieval fires only if something asks, which nothing told a session
|
||||
# to do. A tool-choice reflex asks least of all (#3476, #161).
|
||||
# - This states the PULL for conditional rules, exactly as the surrounding
|
||||
# line states it for always-on ones. Rule 119 makes these surfaces the
|
||||
# session to go looking for one it had not been handed. That preload is
|
||||
# gone (milestone 394), which makes this line LOAD-BEARING rather than
|
||||
# supplementary: retrieval is now the only delivery, and retrieval fires
|
||||
# only if something asks. A session that waits to be handed a rule is
|
||||
# handed nothing. A tool-choice reflex asks least of all (#3476, #161).
|
||||
# - It also has to carry what absence MEANS. "No rule arrived" is now the
|
||||
# ordinary state rather than the exceptional one, and reading it as
|
||||
# "there is no rule" is the #3720 defect at session scale. Rule 119 makes
|
||||
# these surfaces the
|
||||
# specification, so the same sentence lands on all three session-start
|
||||
# surfaces, and test_instruction_surfaces_agree pins it.
|
||||
_INSTRUCTIONS = """
|
||||
@@ -77,8 +77,8 @@ in local files (CLAUDE.md, auto-memory); Scribe holds the single copy.
|
||||
|
||||
Hierarchy: Project -> Milestone -> Task/Note. The map, by purpose:
|
||||
- ORIENT: enter_project(id) at session start — rules, open tasks, recent
|
||||
notes, Systems, design system. `inception` key: ask what the project
|
||||
inherits, decide_project_inception (create_project takes the same).
|
||||
notes, Systems, design system. `inception`: ask what the project
|
||||
inherits, then decide_project_inception.
|
||||
- DO: create_task. Fixed a problem? kind="issue" (symptom -> root cause ->
|
||||
fix), never a work-log line on an unrelated task. Log with add_task_log;
|
||||
keep status honest — in_progress on start, done on finish.
|
||||
@@ -88,9 +88,9 @@ Hierarchy: Project -> Milestone -> Task/Note. The map, by purpose:
|
||||
active project_id to stay in scope.
|
||||
- WHERE work happens: Systems. Tag records with system_ids as you write;
|
||||
create_system when the area is unmodelled.
|
||||
- HOW: rules bind; preferences guide. list_always_on_rules() at start;
|
||||
before a consequential act, search(content_type="rule") — the resident
|
||||
set is not all of them.
|
||||
- HOW: rules bind; preferences guide. Nothing preloads — a rule arrives
|
||||
when your work matches it. Before a consequential act,
|
||||
search(content_type="rule"); silence means nothing matched, not none.
|
||||
- UI: the project's design system is binding — resolve_design_system /
|
||||
get_design_system_stylesheet before hand-writing a value.
|
||||
- REUSE: search snippets before writing a helper; record what you build with
|
||||
@@ -130,7 +130,7 @@ _READ_ONLY_TOOLS = frozenset({
|
||||
"get_task", "get_milestone", "get_recent", "enter_project",
|
||||
"list_milestones", "list_notes", "list_projects", "list_rulebooks",
|
||||
"list_rules", "list_tags", "list_tasks", "list_topics", "list_trash",
|
||||
"list_always_on_rules", "search",
|
||||
"search",
|
||||
"get_system", "list_systems", "list_system_records",
|
||||
# The global area catalog and its mapping REPORT — propose writes nothing;
|
||||
# map_system_to_canonical is the separate, explicitly-called write.
|
||||
|
||||
@@ -256,17 +256,16 @@ async def get_project(project_id: int) -> dict:
|
||||
|
||||
|
||||
def _inception_choices(
|
||||
exclude_always_on_rulebooks, subscribe_rulebooks, design_system_id, seed_systems,
|
||||
subscribe_rulebooks, design_system_id, seed_systems,
|
||||
) -> dict | None:
|
||||
"""The tool args → an inception choices object, or None when no inception
|
||||
arg was given at all (a bare create stays undecided and enter_project
|
||||
asks). design_system_id: 0 = not stated, -1 = explicitly none, n = that
|
||||
system."""
|
||||
if (exclude_always_on_rulebooks is None and subscribe_rulebooks is None
|
||||
if (subscribe_rulebooks is None
|
||||
and not design_system_id and seed_systems is None):
|
||||
return None
|
||||
return {
|
||||
"exclude_always_on_rulebooks": list(exclude_always_on_rulebooks or []),
|
||||
"subscribe_rulebooks": list(subscribe_rulebooks or []),
|
||||
"design_system_id": None if design_system_id in (0, -1) else design_system_id,
|
||||
"seed_systems": bool(seed_systems),
|
||||
@@ -279,7 +278,6 @@ async def create_project(
|
||||
goal: str = "",
|
||||
status: str = "active",
|
||||
color: str = "",
|
||||
exclude_always_on_rulebooks: list[int] | None = None,
|
||||
subscribe_rulebooks: list[int] | None = None,
|
||||
design_system_id: int = 0,
|
||||
seed_systems: bool | None = None,
|
||||
@@ -299,9 +297,10 @@ async def create_project(
|
||||
goal: The desired outcome or definition of done for the project.
|
||||
status: one of active (default), paused, completed, archived.
|
||||
color: Optional hex colour for the project card (e.g. "#6366f1").
|
||||
exclude_always_on_rulebooks: always-on rulebook ids this project does
|
||||
subscribe_rulebooks: rulebook ids this project opts into. Since
|
||||
milestone 394 subscription is the only way a rulebook binds a
|
||||
project, so there is no automatic tier left to decline. Was
|
||||
NOT inherit ([] = inherit them all). list_rulebooks shows which are
|
||||
always_on.
|
||||
subscribe_rulebooks: rulebook ids to subscribe (the non-always-on ones).
|
||||
design_system_id: the design system this project's UI is built from
|
||||
(list_design_systems); -1 = explicitly none; 0 = not stated.
|
||||
@@ -319,7 +318,7 @@ async def create_project(
|
||||
)
|
||||
data = project.to_dict()
|
||||
choices = _inception_choices(
|
||||
exclude_always_on_rulebooks, subscribe_rulebooks, design_system_id, seed_systems,
|
||||
subscribe_rulebooks, design_system_id, seed_systems,
|
||||
)
|
||||
if choices is not None:
|
||||
decided = await inception_svc.decide(uid, project.id, choices=choices, via="mcp")
|
||||
@@ -336,7 +335,6 @@ async def create_project(
|
||||
|
||||
async def decide_project_inception(
|
||||
project_id: int,
|
||||
exclude_always_on_rulebooks: list[int] | None = None,
|
||||
subscribe_rulebooks: list[int] | None = None,
|
||||
design_system_id: int = 0,
|
||||
seed_systems: bool | None = None,
|
||||
@@ -345,11 +343,11 @@ async def decide_project_inception(
|
||||
or re-decide later (milestone 297).
|
||||
|
||||
Owner-only. Applies the effects through the ordinary tools' paths —
|
||||
exclude_always_on_rulebook, subscribe_project_to_rulebook,
|
||||
subscribe_project_to_rulebook,
|
||||
set_project_design_system, the standard Systems seed — and writes the
|
||||
decision on the project last, so get_project/enter_project can say why
|
||||
the project has the rules, design and Systems it has. Re-deciding is
|
||||
additive for exclusions/subscriptions (use include_always_on_rulebook /
|
||||
additive for subscriptions (use
|
||||
unsubscribe_project_from_rulebook to undo one), replaces the design
|
||||
system, and never re-seeds Systems a project already has.
|
||||
|
||||
@@ -359,7 +357,7 @@ async def decide_project_inception(
|
||||
"""
|
||||
uid = current_user_id()
|
||||
choices = _inception_choices(
|
||||
exclude_always_on_rulebooks, subscribe_rulebooks, design_system_id, seed_systems,
|
||||
subscribe_rulebooks, design_system_id, seed_systems,
|
||||
) or {}
|
||||
decided = await inception_svc.decide(uid, project_id, choices=choices, via="mcp")
|
||||
return {"project_id": project_id, **decided}
|
||||
|
||||
@@ -48,16 +48,13 @@ async def get_rulebook(rulebook_id: int) -> dict:
|
||||
async def create_rulebook(title: str, description: str = "") -> dict:
|
||||
"""Create a new rulebook (a shared, reusable module of general rules).
|
||||
|
||||
Two ways a rulebook reaches projects, set by its always_on flag (toggle via
|
||||
update_rulebook):
|
||||
- always_on = true -> binds EVERY one of your projects automatically.
|
||||
Use for universal cross-project norms that apply across every
|
||||
project, not just one.
|
||||
- always_on = false -> binds only projects that subscribe
|
||||
(subscribe_project_to_rulebook). Use for a THEMED body of rules a
|
||||
category of projects shares (e.g. a design system that visual apps
|
||||
opt into).
|
||||
Either way a rulebook is SHARED, so its rules must stay general — agnostic
|
||||
A rulebook reaches a project ONE way: the project subscribes to it
|
||||
(subscribe_project_to_rulebook). There was a second until milestone 394 —
|
||||
an `always_on` flag that bound every project automatically — and it is
|
||||
gone with the tier it belonged to. Opt-in is now the whole model, so a
|
||||
rulebook binds what asked for it and nothing else.
|
||||
|
||||
A rulebook is SHARED, so its rules must stay general — agnostic
|
||||
to any single project. Project-specific rules go in create_project_rule.
|
||||
|
||||
Args:
|
||||
@@ -73,7 +70,6 @@ async def create_rulebook(title: str, description: str = "") -> dict:
|
||||
|
||||
async def update_rulebook(
|
||||
rulebook_id: int, title: str = "", description: str = "",
|
||||
always_on: bool | None = None,
|
||||
) -> dict:
|
||||
"""Update an existing rulebook. Only non-empty fields are changed.
|
||||
|
||||
@@ -81,9 +77,6 @@ async def update_rulebook(
|
||||
rulebook_id: Rulebook to update.
|
||||
title: New title. Empty string leaves unchanged.
|
||||
description: New description. Empty string leaves unchanged.
|
||||
always_on: When True, rules in this rulebook are loaded at session
|
||||
start by list_always_on_rules regardless of project context.
|
||||
Pass None to leave unchanged.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
fields: dict = {}
|
||||
@@ -91,8 +84,6 @@ async def update_rulebook(
|
||||
fields["title"] = title
|
||||
if description:
|
||||
fields["description"] = description
|
||||
if always_on is not None:
|
||||
fields["always_on"] = always_on
|
||||
rb = await rulebooks_svc.update_rulebook(rulebook_id, uid, **fields)
|
||||
if rb is None:
|
||||
raise ValueError(f"rulebook {rulebook_id} not found")
|
||||
@@ -234,58 +225,6 @@ async def list_rules(
|
||||
return {"rules": [_rule_summary(r) for r in rows], "total": len(rows)}
|
||||
|
||||
|
||||
async def list_always_on_rules(project_id: int = 0) -> dict:
|
||||
"""Return all rules from rulebooks flagged always_on for the current user.
|
||||
|
||||
Call this at session start. Treat the returned rules as binding for the
|
||||
session — they apply regardless of which project (if any) is in scope.
|
||||
|
||||
Returns the ALWAYS-ON tier only (milestone 307). A `conditional` rule is
|
||||
still binding when it applies; it just is not resident — it reaches a
|
||||
session through enter_project (when the project works in an area the rule
|
||||
is tagged to) or through search(content_type="rule"). Nothing here is a
|
||||
behaviour change until rules are actually re-tiered: `tier` defaults to
|
||||
always_on, so an existing rulebook returns exactly what it always did.
|
||||
Pair with get_project(id).applicable_rules when working on a specific
|
||||
project to also load that project's subscription-derived rules.
|
||||
|
||||
A rule carrying `last_verified` asserts a FACT about something outside the
|
||||
operator's control — a runner's shell, a tool's existence, a setting
|
||||
somewhere. It is still binding; the field says how long ago anyone
|
||||
confirmed it, and "never" means nobody has. Follow the rule, and if you
|
||||
are already standing where the check could be made, make it: get_rule
|
||||
gives you its `verify_with`. Most rules have no such field, which means
|
||||
they are decisions and there is nothing to check.
|
||||
|
||||
Args:
|
||||
project_id: 0 (default) = the user-wide set. Inside a project, pass
|
||||
its id: an always-on rulebook the project EXCLUDED at inception
|
||||
(see enter_project's `excluded_always_on`) is left out — the
|
||||
project decided not to inherit it.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
rules = await rulebooks_svc.list_always_on_rules(uid, project_id=project_id)
|
||||
# AMBIENT source: the resident set, handed over whole. No ranker chose
|
||||
# these, so they must not land in the pull-through numerator's denominator
|
||||
# — but they must land SOMEWHERE, or the largest rule surface in the
|
||||
# product stays the one surface its own scoreboard cannot see (#3473).
|
||||
record_rule_surfaced(
|
||||
user_id=uid,
|
||||
rule_ids=[r.id for r in rules],
|
||||
source="list_always_on_rules",
|
||||
)
|
||||
return {
|
||||
"rules": [_rule_summary(r) for r in rules],
|
||||
"total": len(rules),
|
||||
# A marker for the set you are now holding. It is not for you to read:
|
||||
# the write-path hook carries it back and is told if these rules have
|
||||
# moved since. Deliberately NOT on rules_payload's applicable_rules —
|
||||
# that is a DIFFERENT set (subscription-derived), and one key name
|
||||
# over two sets is how a comparison starts reporting phantom changes.
|
||||
"rules_etag": rulebooks_svc.rules_etag(rules),
|
||||
}
|
||||
|
||||
|
||||
async def get_rule(rule_id: int) -> dict:
|
||||
"""Fetch a rule by id — full statement + why + how_to_apply.
|
||||
|
||||
@@ -309,7 +248,6 @@ async def get_rule(rule_id: int) -> dict:
|
||||
async def create_rule(
|
||||
topic_id: int, title: str, statement: str, when_to_apply: str = "",
|
||||
why: str = "", how_to_apply: str = "", order_index: int = 0,
|
||||
tier: str = "always_on", system_ids: list[int] | None = None,
|
||||
arose_from_id: int = 0, verify_with: str = "", expires_when: str = "",
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
@@ -357,7 +295,7 @@ async def create_rule(
|
||||
* "Approve it AS WRITTEN" — you create it with the statement exactly as
|
||||
shown. This is what makes element 1 load-bearing: they approved TEXT,
|
||||
so that text is what gets stored, verbatim.
|
||||
* "LET'S TALK ABOUT IT" — the wording, the scope, the tier, whether it
|
||||
* "LET'S TALK ABOUT IT" — the wording, the scope, whether it
|
||||
wants to be a rule at all. Most good rules arrive this way, so treat
|
||||
this answer as the expected one rather than a setback.
|
||||
* "NO" — let it go. If the observation is still worth keeping, it is a
|
||||
@@ -371,7 +309,7 @@ async def create_rule(
|
||||
into existence, which is the thing this whole loop exists to prevent.
|
||||
|
||||
A rulebook rule is shared by every project that gets the rulebook: an
|
||||
always_on rulebook binds ALL your projects; a subscribed rulebook binds the
|
||||
A subscribed rulebook binds the
|
||||
projects that opt in. So a rulebook rule must read as a general standard —
|
||||
never pin it to one project's files, paths, or quirks. For a rule that
|
||||
applies to a single project only, use create_project_rule instead (no
|
||||
@@ -408,7 +346,7 @@ async def create_rule(
|
||||
instruction. State the moment or the material: "before any git
|
||||
push", "when adding a value to a CHECK-gated column", "when a
|
||||
release is being cut". Write it even though the parameter is
|
||||
optional: it decides the tier below, it is how the rule is found
|
||||
optional: it is how the rule is found
|
||||
when it matters, and a rule nobody can place is a rule nobody
|
||||
applies.
|
||||
This field is also the rule's RETRIEVAL SURFACE — it and the
|
||||
@@ -428,12 +366,6 @@ async def create_rule(
|
||||
category — it produces the command, the error, the half-formed
|
||||
ask — so a trigger written that way leaves the embedded
|
||||
document to be carried by the title alone.
|
||||
tier: "always_on" (default) or "conditional".
|
||||
The test: can you name the trigger WITHOUT naming a system, an
|
||||
artifact type or a moment? If the honest answer is "whenever you
|
||||
are working", it is always_on. If you had to name something, it is
|
||||
conditional — and conditional costs nothing when it is irrelevant,
|
||||
which is what lets it be as long as it needs to be.
|
||||
system_ids: Ids from list_canonical_systems — the global AREAS this
|
||||
rule is about. This is what lets a rule reach a project that is
|
||||
working in that area, so a CI rule surfaces on a CI change.
|
||||
@@ -472,7 +404,7 @@ async def create_rule(
|
||||
rule = await rulebooks_svc.create_rule(
|
||||
topic_id=topic_id, user_id=uid,
|
||||
title=title, statement=statement, when_to_apply=when_to_apply,
|
||||
tier=tier, arose_from_id=arose_from_id,
|
||||
arose_from_id=arose_from_id,
|
||||
why=why, how_to_apply=how_to_apply, order_index=order_index,
|
||||
verify_with=verify_with, expires_when=expires_when,
|
||||
)
|
||||
@@ -482,7 +414,6 @@ async def create_rule(
|
||||
async def create_project_rule(
|
||||
project_id: int, statement: str, title: str = "", when_to_apply: str = "",
|
||||
why: str = "", how_to_apply: str = "", order_index: int = 0,
|
||||
tier: str = "always_on", system_ids: list[int] | None = None,
|
||||
arose_from_id: int = 0, verify_with: str = "", expires_when: str = "",
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
@@ -531,25 +462,7 @@ async def create_project_rule(
|
||||
RETRIEVES: "the CI job passed locally and fails on the runner
|
||||
with a permission error"
|
||||
COLLAPSES: "when touching CI config"
|
||||
See create_rule for the full argument. It informs the
|
||||
tier below rather than deciding it,
|
||||
since a project rule's tier turns on area-scope, not on whether
|
||||
the trigger can be named.
|
||||
tier: "always_on" (default) or "conditional". The SAME two values as
|
||||
create_rule, judged against a different cost — do not import that
|
||||
tool's test wholesale. There, always_on means every session in
|
||||
every project, so the bar is high: the trigger must be nameless
|
||||
("whenever you are working"). Here the rule is already scoped to
|
||||
one project by construction, so always_on costs only that
|
||||
project's sessions and the bar is correspondingly lower. A
|
||||
project rule that names something specific is still ordinarily
|
||||
always_on — being specific is what project rules are FOR.
|
||||
Reach for conditional when the rule is about one AREA of a large
|
||||
project — a CI quirk, a migration gotcha, one subsystem's
|
||||
convention — so it arrives with that area instead of resident in
|
||||
every session. The failure to avoid is local: forty always-on
|
||||
rules on one project reproduces, inside that project, exactly the
|
||||
preload bloat that made every rule compete for the same budget.
|
||||
See create_rule for the full argument.
|
||||
system_ids: Ids from list_canonical_systems — the global AREAS this
|
||||
rule is about. Worth setting even on a project rule: it is what
|
||||
lets a conditional one surface when the project is working in
|
||||
@@ -583,7 +496,7 @@ async def create_project_rule(
|
||||
rule = await rulebooks_svc.create_project_rule(
|
||||
project_id=project_id, user_id=uid,
|
||||
title=derived_title, statement=statement, when_to_apply=when_to_apply,
|
||||
tier=tier, arose_from_id=arose_from_id,
|
||||
arose_from_id=arose_from_id,
|
||||
why=why, how_to_apply=how_to_apply, order_index=order_index,
|
||||
verify_with=verify_with, expires_when=expires_when,
|
||||
)
|
||||
@@ -593,7 +506,7 @@ async def create_project_rule(
|
||||
async def update_rule(
|
||||
rule_id: int, title: str = "", statement: str = "", when_to_apply: str = "",
|
||||
why: str = "", how_to_apply: str = "", order_index: int = -1,
|
||||
tier: str = "", system_ids: list[int] | None = None, arose_from_id: int = 0,
|
||||
system_ids: list[int] | None = None, arose_from_id: int = 0,
|
||||
verify_with: str = "", expires_when: str = "", kind: str = "",
|
||||
clear_fields: list[str] | None = None,
|
||||
) -> dict:
|
||||
@@ -606,9 +519,10 @@ async def update_rule(
|
||||
correct. Ordinary edits to an existing preference belong in
|
||||
update_preference, which asks for what taught the change.
|
||||
|
||||
Adding `when_to_apply` and a `tier` to an existing rule is the ordinary way
|
||||
a rule stops being preloaded into every session and starts arriving when it
|
||||
is relevant. `system_ids` REPLACES the rule's areas (pass [] to clear).
|
||||
`when_to_apply` IS HOW A RULE ARRIVES AT ALL. Nothing is preloaded since
|
||||
milestone 394, so a rule with no trigger is not a quiet rule — it is one
|
||||
no session will ever be shown. `system_ids` REPLACES the rule's areas
|
||||
(pass [] to clear), and they decide which PROJECTS a rule binds by area.
|
||||
|
||||
RETROFITTING A TRIGGER HAS ITS OWN TRAP, and it is not the one create_rule
|
||||
warns about. There the field is empty and the instruction is "write one".
|
||||
@@ -662,8 +576,6 @@ async def update_rule(
|
||||
fields["statement"] = statement
|
||||
if when_to_apply:
|
||||
fields["when_to_apply"] = when_to_apply
|
||||
if tier:
|
||||
fields["tier"] = tier
|
||||
if kind:
|
||||
fields["kind"] = kind
|
||||
if arose_from_id:
|
||||
@@ -978,7 +890,7 @@ async def subscribe_project_to_rulebook(
|
||||
) -> dict:
|
||||
"""Subscribe a project to a rulebook — its rules then bind that project.
|
||||
|
||||
Subscription is the opt-in path for a non-always_on rulebook: a reusable,
|
||||
Subscription is the ONLY path for a rulebook (milestone 394): a reusable,
|
||||
themed module of GENERAL rules shared across the projects that subscribe.
|
||||
Subscribe a project because it fits the rulebook's theme (e.g. a visual app
|
||||
-> the design-system rulebook), not to host rules about this one project —
|
||||
@@ -1004,34 +916,6 @@ async def unsubscribe_project_from_rulebook(
|
||||
|
||||
# ── Suppressions — project-level mute of rulebook rules / topics ────────
|
||||
|
||||
async def exclude_always_on_rulebook(project_id: int, rulebook_id: int) -> dict:
|
||||
"""Opt a project OUT of a whole always-on rulebook (milestone 297).
|
||||
|
||||
Always-on rulebooks bind every project implicitly; an inception decision
|
||||
can say "not this one, not here". The exclusion is total for that project
|
||||
— list_always_on_rules(project_id), enter_project/get_project rules and
|
||||
the session-start context all leave it out and name it under
|
||||
`excluded_always_on`. Owner-only; the rulebook must be always_on (a
|
||||
subscribed rulebook is left with unsubscribe_project_from_rulebook).
|
||||
Idempotent; include_always_on_rulebook reverses it. Normally reached via
|
||||
decide_project_inception, not by hand.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
await rulebooks_svc.exclude_always_on_rulebook_for_project(
|
||||
project_id=project_id, rulebook_id=rulebook_id, user_id=uid,
|
||||
)
|
||||
return {"project_id": project_id, "rulebook_id": rulebook_id, "excluded": True}
|
||||
|
||||
|
||||
async def include_always_on_rulebook(project_id: int, rulebook_id: int) -> dict:
|
||||
"""Reverse exclude_always_on_rulebook: the always-on rulebook binds this
|
||||
project again. Idempotent."""
|
||||
uid = current_user_id()
|
||||
await rulebooks_svc.include_always_on_rulebook_for_project(
|
||||
project_id=project_id, rulebook_id=rulebook_id, user_id=uid,
|
||||
)
|
||||
return {"project_id": project_id, "rulebook_id": rulebook_id, "excluded": False}
|
||||
|
||||
|
||||
async def suppress_rule_for_project(
|
||||
project_id: int, rule_id: int,
|
||||
@@ -1087,8 +971,6 @@ async def unsuppress_topic_for_project(
|
||||
return {"project_id": project_id, "topic_id": topic_id, "suppressed": False}
|
||||
|
||||
|
||||
|
||||
|
||||
async def relate_rules(
|
||||
from_rule_id: int, to_rule_id: int, kind: str, note: str = "",
|
||||
) -> dict:
|
||||
@@ -1136,7 +1018,7 @@ async def unrelate_rules(relation_id: int) -> dict:
|
||||
# ── The staleness sweep (milestone 312) ────────────────────────────────
|
||||
|
||||
async def rules_due_for_verification(
|
||||
older_than_days: int = 0, tier: str = "", never_only: bool = False,
|
||||
older_than_days: int = 0, never_only: bool = False,
|
||||
) -> dict:
|
||||
"""Which standing rules assert a FACT that nobody has confirmed lately.
|
||||
|
||||
@@ -1163,9 +1045,6 @@ async def rules_due_for_verification(
|
||||
Args:
|
||||
older_than_days: only rules last verified longer ago than this.
|
||||
Never-checked rules always qualify. 0 = no age filter.
|
||||
tier: "always_on" or "conditional" to narrow. An always-on constraint
|
||||
that has gone false is the expensive kind — it is preloaded into
|
||||
every session, so a wrong one is wrong everywhere at once.
|
||||
never_only: only rules nobody has ever verified.
|
||||
|
||||
NOT filterable by project, deliberately: a project reaches rules through
|
||||
@@ -1175,7 +1054,7 @@ async def rules_due_for_verification(
|
||||
"""
|
||||
uid = current_user_id()
|
||||
rules = await rulebooks_svc.rules_due_for_verification(
|
||||
uid, older_than_days=older_than_days, tier=tier, never_only=never_only,
|
||||
uid, older_than_days=older_than_days, never_only=never_only,
|
||||
)
|
||||
return {
|
||||
"rules": [rulebooks_svc.verification_row(r) for r in rules],
|
||||
@@ -1228,14 +1107,13 @@ def register(mcp) -> None:
|
||||
for fn in (
|
||||
list_rulebooks, get_rulebook, create_rulebook, update_rulebook, delete_rulebook,
|
||||
list_topics, create_topic, update_topic, delete_topic,
|
||||
list_rules, list_always_on_rules, get_rule,
|
||||
list_rules, get_rule,
|
||||
create_rule, create_project_rule, update_rule, delete_rule,
|
||||
create_preference, update_preference,
|
||||
relate_rules, unrelate_rules,
|
||||
subscribe_project_to_rulebook, unsubscribe_project_from_rulebook,
|
||||
suppress_rule_for_project, unsuppress_rule_for_project,
|
||||
suppress_topic_for_project, unsuppress_topic_for_project,
|
||||
exclude_always_on_rulebook, include_always_on_rulebook,
|
||||
rules_due_for_verification, mark_rule_verified,
|
||||
rule_history,
|
||||
):
|
||||
|
||||
@@ -40,7 +40,6 @@ async def _search_rules(uid: int, q: str, limit: int) -> dict:
|
||||
"title": rule.title,
|
||||
"statement": rule.statement,
|
||||
"when_to_apply": rule.when_to_apply or "",
|
||||
"tier": rule.tier,
|
||||
"why": rule.why or "",
|
||||
"how_to_apply": rule.how_to_apply or "",
|
||||
"verify_with": rule.verify_with or "",
|
||||
@@ -282,7 +281,7 @@ It is an UPPER BOUND per surface: a pull records the door it came
|
||||
`surfaced` VS `ambient` IS THE READING THAT MATTERS HERE. `surfaced` counts
|
||||
rules a ranker chose — today only the write-path arm — and those are claims
|
||||
a pull can settle. `ambient` counts BULK DELIVERIES: the SessionStart
|
||||
preload, `list_always_on_rules`, and the `rules_payload` surfaces
|
||||
preload and the `rules_payload` surfaces
|
||||
(`enter_project`, `get_project`, `get_milestone`, `start_planning`,
|
||||
`get_task`), which hand over the whole applicable set at once with nobody
|
||||
choosing anything. A large `ambient` says the resident set is big and
|
||||
|
||||
Reference in New Issue
Block a user