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
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:
@@ -27,7 +27,10 @@ const expandedRuleIds = ref<Set<number>>(new Set());
|
||||
const ruleDetails = ref<Record<number, { why: string; how_to_apply: string }>>({});
|
||||
|
||||
const showProjectRuleForm = ref(false);
|
||||
const newProjectRule = ref({ title: "", statement: "", why: "", how_to_apply: "" });
|
||||
const newProjectRule = ref({
|
||||
title: "", statement: "", why: "", how_to_apply: "",
|
||||
when_to_apply: "", tier: "always_on" as "always_on" | "conditional",
|
||||
});
|
||||
|
||||
async function load() {
|
||||
applicable.value = await getProjectApplicableRules(props.projectId);
|
||||
@@ -90,14 +93,20 @@ interface RulebookGroup {
|
||||
function groupByRulebookAndTopic(rules: ApplicableRules["rules"]): RulebookGroup[] {
|
||||
const byRulebook = new Map<number, RulebookGroup>();
|
||||
for (const r of rules) {
|
||||
// A rule carries topic_id XOR project_id. Only rulebook-scoped rules reach
|
||||
// this list, so a null topic would be a server-side contradiction — skip
|
||||
// it rather than widen the group's type to accommodate a case that means
|
||||
// something is wrong upstream.
|
||||
if (r.topic_id === null) continue;
|
||||
const topicId = r.topic_id;
|
||||
let rb = byRulebook.get(r.rulebook_id);
|
||||
if (!rb) {
|
||||
rb = { rulebook_id: r.rulebook_id, rulebook_title: r.rulebook_title, topics: [] };
|
||||
byRulebook.set(r.rulebook_id, rb);
|
||||
}
|
||||
let topic = rb.topics.find((t) => t.topic_id === r.topic_id);
|
||||
let topic = rb.topics.find((t) => t.topic_id === topicId);
|
||||
if (!topic) {
|
||||
topic = { topic_id: r.topic_id, topic_title: r.topic_title, rules: [] };
|
||||
topic = { topic_id: topicId, topic_title: r.topic_title, rules: [] };
|
||||
rb.topics.push(topic);
|
||||
}
|
||||
topic.rules.push(r);
|
||||
@@ -113,8 +122,13 @@ async function submitProjectRule() {
|
||||
title: newProjectRule.value.title.trim() || undefined,
|
||||
why: newProjectRule.value.why.trim() || undefined,
|
||||
how_to_apply: newProjectRule.value.how_to_apply.trim() || undefined,
|
||||
when_to_apply: newProjectRule.value.when_to_apply.trim() || undefined,
|
||||
tier: newProjectRule.value.tier,
|
||||
});
|
||||
newProjectRule.value = { title: "", statement: "", why: "", how_to_apply: "" };
|
||||
newProjectRule.value = {
|
||||
title: "", statement: "", why: "", how_to_apply: "",
|
||||
when_to_apply: "", tier: "always_on",
|
||||
};
|
||||
showProjectRuleForm.value = false;
|
||||
await load();
|
||||
}
|
||||
@@ -219,6 +233,24 @@ watch(() => props.projectId, load);
|
||||
placeholder="Statement (required) — the actionable instruction, 1-2 sentences"
|
||||
rows="2"
|
||||
></textarea>
|
||||
<textarea
|
||||
v-model="newProjectRule.when_to_apply"
|
||||
placeholder="When to apply — the trigger, not the instruction"
|
||||
rows="2"
|
||||
></textarea>
|
||||
<div class="tier-row">
|
||||
<label>
|
||||
<input v-model="newProjectRule.tier" type="radio" value="always_on" />
|
||||
Always on
|
||||
</label>
|
||||
<label>
|
||||
<input v-model="newProjectRule.tier" type="radio" value="conditional" />
|
||||
Conditional
|
||||
</label>
|
||||
<span class="tier-hint">
|
||||
Conditional if you had to name a system, an artifact or a moment to state the trigger.
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="newProjectRule.why"
|
||||
placeholder="Why (optional) — the rationale"
|
||||
@@ -345,6 +377,11 @@ watch(() => props.projectId, load);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tier-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; font-size: 0.85rem; }
|
||||
.tier-row label { display: inline-flex; align-items: center; gap: 0.3rem; }
|
||||
.tier-row input { accent-color: var(--fs-accent); }
|
||||
.tier-hint { flex: 1; min-width: 12rem; font-size: 0.75rem; color: var(--fs-text-tertiary); }
|
||||
|
||||
.excluded-note { margin: 0 0 0.5rem; color: var(--fs-text-tertiary); font-size: 0.85rem; }
|
||||
.chip-excluded { opacity: 0.8; text-decoration: line-through; }
|
||||
.chip-excluded .chip-remove { text-decoration: none; }
|
||||
|
||||
Reference in New Issue
Block a user