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:
@@ -98,24 +98,58 @@ export const useRulebooksStore = defineStore("rulebooks", () => {
|
||||
delete rulesByTopic.value[id];
|
||||
}
|
||||
|
||||
async function createRule(topicId: number, data: { title: string; statement: string; why?: string; how_to_apply?: string }) {
|
||||
/**
|
||||
* A list row built from a full rule. The row shape is the server's
|
||||
* rule_brief, so every field it carries has to be mirrored here or the two
|
||||
* disagree the moment a list is patched locally instead of re-fetched.
|
||||
*/
|
||||
function toHeader(rule: Rule): api.RuleHeader {
|
||||
return {
|
||||
id: rule.id,
|
||||
title: rule.title,
|
||||
statement: rule.statement,
|
||||
topic_id: rule.topic_id,
|
||||
tier: rule.tier,
|
||||
updated_at: rule.updated_at,
|
||||
when_to_apply: rule.when_to_apply || undefined,
|
||||
arose_from_id: rule.arose_from_id ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function createRule(topicId: number, data: Partial<api.RuleWrite> & { title: string; statement: string }) {
|
||||
const rule = await api.createRule(topicId, data);
|
||||
if (!rulesByTopic.value[topicId]) rulesByTopic.value[topicId] = [];
|
||||
rulesByTopic.value[topicId].push({ id: rule.id, title: rule.title, statement: rule.statement, topic_id: rule.topic_id });
|
||||
rulesByTopic.value[topicId].push(toHeader(rule));
|
||||
return rule;
|
||||
}
|
||||
|
||||
async function updateRule(id: number, data: Partial<Pick<Rule, "title" | "statement" | "why" | "how_to_apply" | "order_index">>) {
|
||||
async function updateRule(id: number, data: Partial<api.RuleWrite>) {
|
||||
const rule = await api.updateRule(id, data);
|
||||
if (currentRule.value?.id === id) currentRule.value = rule;
|
||||
for (const tid of Object.keys(rulesByTopic.value)) {
|
||||
const list = rulesByTopic.value[Number(tid)];
|
||||
const idx = list.findIndex((r) => r.id === id);
|
||||
if (idx >= 0) list[idx] = { id: rule.id, title: rule.title, statement: rule.statement, topic_id: rule.topic_id };
|
||||
if (idx >= 0) list[idx] = toHeader(rule);
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
async function relateRules(
|
||||
fromRuleId: number,
|
||||
data: { to_rule_id: number; kind: api.RuleRelationKind; note?: string },
|
||||
) {
|
||||
await api.relateRules(fromRuleId, data);
|
||||
// Re-read rather than patching locally: the edge reads from BOTH ends, so
|
||||
// the far rule's relations changed too and a local splice would show only
|
||||
// half of what just happened.
|
||||
await fetchRule(fromRuleId);
|
||||
}
|
||||
|
||||
async function unrelateRules(relationId: number, refreshRuleId: number) {
|
||||
await api.unrelateRules(relationId);
|
||||
await fetchRule(refreshRuleId);
|
||||
}
|
||||
|
||||
async function deleteRule(id: number) {
|
||||
await api.deleteRule(id);
|
||||
if (currentRule.value?.id === id) currentRule.value = null;
|
||||
@@ -129,6 +163,6 @@ export const useRulebooksStore = defineStore("rulebooks", () => {
|
||||
fetchRulebooks, fetchTopics, fetchRules, fetchRule,
|
||||
createRulebook, updateRulebook, toggleAlwaysOn, deleteRulebook,
|
||||
createTopic, updateTopic, deleteTopic,
|
||||
createRule, updateRule, deleteRule,
|
||||
createRule, updateRule, deleteRule, relateRules, unrelateRules,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user