feat(rules): the rule editor asks when it applies, and the list shows its age (#3029, milestone 307 step 3, UI)
CI & Build / Python lint (push) Successful in 6s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Failing after 27s
CI & Build / integration (push) Successful in 38s
CI & Build / Python tests (push) Successful in 1m27s
CI & Build / Build & push image (push) Skipped

Rule 27 — the schema and both doors shipped with no human surface, so step 3
was not shippable until this.

RuleEditorSlideOver gains the trigger, the tier, the areas, and a read-only
view of the rule's edges. The tier is a radio pair carrying the test itself
rather than a bare toggle: 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. It also says why conditional is not a demotion —
it costs nothing when irrelevant, which is what lets a rule be as long as it
needs to be. The relations block states the rule the whole milestone turns on:
rules that FAIL TOGETHER are linked, never merged.

RuleListPane shows the trigger and the LAST-CHANGED DATE on every row, and
marks conditional only — always_on is the default and badging every row would
say nothing. The date is the cheap triage the FabledCurator case wanted: a
rule whose age predates the capability it duplicates is visible at a glance
instead of needing a get_rule to find out.

ProjectRulesTab's inline create form gains the same two fields, because a
project rule bloats exactly the way a family one does — FabledCurator has 23
of them.

Two type fixes the new shapes forced, both worth keeping:
- toHeader() in the store: a list row is the server's rule_brief, so patching
  a list locally has to mirror every field it carries or the two disagree.
  There were two hand-built four-field literals doing that job.
- ApplicableRules.rules / .project_rules are now described AS RuleHeader
  rather than as two more hand-written shapes — the same builder produces
  them, so the same type should describe them.

groupByRulebookAndTopic skips a null-topic rule rather than widening
TopicGroup to accept one: a rule carries topic_id XOR project_id, so a null
topic in that list means something is wrong upstream, and a widened type would
hide it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 14:23:40 -04:00
co-authored by Claude Opus 5
parent ffb7a0fe38
commit 8b60d552d2
5 changed files with 302 additions and 33 deletions
+24 -1
View File
@@ -13,8 +13,17 @@ const emit = defineEmits<{
<header><h2>Rules</h2></header>
<ul>
<li v-for="r in rules" :key="r.id" @click="emit('open-rule', r.id)">
<div class="title">{{ r.title }}</div>
<div class="title">
{{ r.title }}
<!-- Only conditional is marked: always-on is the default and
badging every row would say nothing. -->
<span v-if="r.tier === 'conditional'" class="tier-chip" title="Arrives when its trigger fires, rather than in every session">conditional</span>
</div>
<div class="statement">{{ r.statement }}</div>
<div v-if="r.when_to_apply || r.updated_at" class="meta">
<span v-if="r.when_to_apply" class="trigger">{{ r.when_to_apply }}</span>
<span v-if="r.updated_at" class="age" :title="`Last changed ${r.updated_at}`">{{ r.updated_at }}</span>
</div>
</li>
</ul>
<button class="new-rule" @click="emit('create-rule', topicId)">+ New rule</button>
@@ -35,5 +44,19 @@ li {
li:hover { background: var(--fs-surface-hover); }
.title { font-family: Fraunces, serif; font-style: italic; font-size: 1.05em; }
.statement { font-size: 0.9em; opacity: 0.8; margin-top: 0.25rem; }
.meta { display: flex; align-items: baseline; gap: 0.5rem; margin-top: 0.35rem; font-size: 0.75em; }
.trigger { flex: 1; min-width: 0; color: var(--fs-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.age { color: var(--fs-text-tertiary); font-variant-numeric: tabular-nums; flex-shrink: 0; }
.tier-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;
}
.new-rule { cursor: pointer; }
</style>