Files
FabledScribe/frontend/src/assets/rules-shared.css
T
bvandeusenandClaude Opus 5 7038e41ec7
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 52s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m32s
CI & Build / Build & push image (push) Successful in 34s
feat(rules): preferences are writable, and their drift arrives (#3895)
Milestone 399 step 5. Steps 1-4 put preferences into the backend: a kind
column, an inverted write path, a third register in the injected block, a
delivery slot. Nothing the operator could touch. Rule 27 forbids leaving it
there, and here it matters more than usual, because the UI is the only
guard against the risk the milestone named up front — an agent misreads one
session, rewrites a preference, and follows the rewritten version forever
while the operator never sees the moment it changed.

Four things ship.

A preference is DISTINGUISHABLE. `kind` reaches the client (the server has
always sent it in rule_brief) and a preference carries a chip. Force is the
one thing a list of instructions must not leave the reader to infer, and a
row that renders identically to a rule teaches the opposite of both facts
about a preference: it does not bind, and a session may rewrite it.

A preference is WRITABLE. The editor gains the kind as a first-class choice
with the test beside it — what happens when someone does not do this — and
says plainly, when preference is chosen, that sessions rewrite these
without asking and every rewrite is kept.

DRIFT ARRIVES. `GET /api/rules/drift` returns one row per rewritten
preference carrying its latest rewrite: what it said, what it says now, and
the record named by `arose_from_id` that taught the change. Both texts ride
along so the list shows the diff without a call per row. The new pane sits
beside the staleness sweep, because drift belongs to no one rulebook, and
it answers a question the operator would not have thought to ask.

REVERSION IS ONE ACTION, and this is the carve-out worth arguing with.
Milestone 323 refused a one-click restore for rules — "a binding
instruction should not be revertible in one click", because a silent revert
erases the only record of why the rewrite happened. That reasoning turns on
the rewrite being the operator's own decision. A preference's is not: the
agent makes it mid-work without asking, so reverting is a veto over someone
else's edit rather than an undo of your own, and a veto costing more than a
shrug is not supervision. The route refuses anything but a preference (409),
and nothing is erased: the restore goes through update_rule, so it snapshots
too and the history GAINS the revert. An integration test pins that, because
it is the whole basis for the exception.

Tested against real Postgres — every claim is about which rows come back
and in what order, which a stand-in session cannot judge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-18 12:39:13 -04:00

56 lines
2.1 KiB
CSS

/* Shared by the rules panes (RulebookListPane, RuleListPane,
RulebookDetailPane, RuleSweepPane): the pane surface, its heading, and the
title chip. Counting them in this comment went stale the first time a
fourth was added, so it no longer does. Load with
<style src="@/assets/rules-shared.css" /> beside the component's own
scoped block; never restate these there (#2903, milestone 299). */
.pane {
background: var(--fs-surface-hover);
padding: 1rem;
overflow-y: auto;
}
.pane header h2 {
font-family: Fraunces, serif;
font-style: italic;
margin: 0 0 0.5rem 0;
}
.form-buttons { display: flex; gap: 0.5rem; }
/* A small marker beside a rule's title. Two of these appeared within one
milestone (tier, then verification) and were byte-identical; a third would
have drifted. The pane's italic serif title is inherited by anything inside
it, so the chip resets family and style explicitly. */
/* A rule with no trigger cannot be retrieved, and since milestone 394
retrieval is the only delivery — so this marks a rule that will never
reach a session. Warning rather than error: the rule is not broken, it is
unreachable, and the fix is one field away. */
.rule-chip-inert {
color: var(--fs-warning-fg);
background: color-mix(in srgb, var(--fs-warning) 12%, var(--fs-surface-raised));
}
.rule-chip {
margin-left: 0.4rem;
font-family: var(--fs-font-body);
font-style: normal;
font-size: 0.62rem;
color: var(--fs-text-secondary);
background: var(--fs-surface-raised);
border-radius: var(--fs-radius-pill);
padding: 0.05rem 0.4rem;
vertical-align: middle;
}
/* A PREFERENCE, marked because force is the one thing a list of instructions
must not leave the reader to infer. A preference does not bind — ignoring it
costs consistency, not correctness — and it is the one kind the agent
rewrites on its own, so a row that renders identically to a rule teaches the
opposite of both facts.
The accent, not the warning colour: nothing is wrong with a preference. It
is a different KIND, and the marker says which. */
.rule-chip-preference {
color: var(--fs-accent);
background: var(--fs-accent-soft);
}