Files
FabledGrimoire/frontend/src/api/rulebooks.ts
T
bvandeusenandClaude Opus 4.7 43a860c3ac
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 49s
CI & Build / Build & push image (push) Successful in 1m7s
feat(rules): project-scoped rules (S3)
Rules can now belong to either a rulebook topic OR a single project,
enforced by a CHECK constraint (exactly-one of topic_id/project_id).
Adds the create_project_rule MCP tool + REST endpoint, surfaces
project-scoped rules in get_project/get_task/start_planning under a
new project_rules field, and adds a project Rules tab section with an
inline create form so the operator can author project rules from the
UI without rulebook ceremony.

- migration 0059: rules.project_id (FK projects ON DELETE CASCADE),
  topic_id now nullable, CHECK ck_rule_topic_xor_project, index on
  project_id
- model: Rule gains project_id; to_dict exposes it
- service: create_project_rule with project-ownership guard; list_rules
  with project_id filter UNIONs subscription-derived + project-scoped;
  get_applicable_rules adds a project_rules field; get_rule / update_rule
  / delete_rule fetch via a shared _fetch_owned_rule that handles both
  rulebook and project ownership paths
- trash: project delete cascades to project-scoped rules
- MCP: create_project_rule tool registered; _INSTRUCTIONS mentions both
  create_rule and create_project_rule paths
- REST: POST /api/projects/<id>/rules (statement required, title derived
  if omitted)
- frontend: Rule type gains nullable topic_id + project_id; createProjectRule
  client; ProjectRulesTab.vue gains a "Project rules" section with inline
  create form and per-rule expand/delete
- tests: register count → 18; create_project_rule unit tests (required
  fields, title derivation, explicit-title pass-through); applicable_rules
  shape tests now include project_rules; trash cascade test updated to
  expect 5 executions

S1+S2 (always_on flag + Scribe-first prompt) shipped in 658348f.
S4 (enter_project handshake) follows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 01:10:18 -04:00

150 lines
5.2 KiB
TypeScript

import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client";
export interface Rulebook {
id: number;
owner_user_id: number;
title: string;
description: string;
always_on: boolean;
created_at: string | null;
updated_at: string | null;
}
export interface RulebookTopic {
id: number;
rulebook_id: number;
title: string;
description: string;
order_index: number;
created_at: string | null;
updated_at: string | null;
}
export interface Rule {
id: number;
topic_id: number | null;
project_id: number | null;
title: string;
statement: string;
why: string;
how_to_apply: string;
order_index: number;
created_at: string | null;
updated_at: string | null;
}
export interface RuleHeader {
id: number;
title: string;
statement: string;
topic_id: number | null;
}
export interface ApplicableRules {
rules: {
id: number;
title: string;
statement: string;
topic_title: string;
rulebook_title: string;
}[];
project_rules: {
id: number;
title: string;
statement: string;
}[];
truncated: boolean;
subscribed_rulebooks: { id: number; title: string }[];
}
// ── Rulebooks ───────────────────────────────────────────────────────
export async function listRulebooks(): Promise<Rulebook[]> {
const data = await apiGet<{ rulebooks: Rulebook[] }>("/api/rulebooks");
return data.rulebooks;
}
export async function getRulebook(id: number): Promise<Rulebook & { topics: RulebookTopic[] }> {
return apiGet(`/api/rulebooks/${id}`);
}
export async function createRulebook(data: { title: string; description?: string }): Promise<Rulebook> {
return apiPost("/api/rulebooks", data);
}
export async function updateRulebook(id: number, data: Partial<{ title: string; description: string; always_on: boolean }>): Promise<Rulebook> {
return apiPatch(`/api/rulebooks/${id}`, data);
}
export async function deleteRulebook(id: number): Promise<void> {
return apiDelete(`/api/rulebooks/${id}`);
}
// ── Topics ─────────────────────────────────────────────────────────
export async function listTopics(rulebookId: number): Promise<RulebookTopic[]> {
const data = await apiGet<{ topics: RulebookTopic[] }>(`/api/rulebooks/${rulebookId}/topics`);
return data.topics;
}
export async function createTopic(rulebookId: number, data: { title: string; description?: string; order_index?: number }): Promise<RulebookTopic> {
return apiPost(`/api/rulebooks/${rulebookId}/topics`, data);
}
export async function updateTopic(id: number, data: Partial<{ title: string; description: string; order_index: number }>): Promise<RulebookTopic> {
return apiPatch(`/api/rulebook-topics/${id}`, data);
}
export async function deleteTopic(id: number): Promise<void> {
return apiDelete(`/api/rulebook-topics/${id}`);
}
// ── Rules ──────────────────────────────────────────────────────────
export async function listRules(filters: { rulebook_id?: number; topic_id?: number; project_id?: number } = {}): Promise<Rule[]> {
const params = new URLSearchParams();
if (filters.rulebook_id) params.set("rulebook_id", String(filters.rulebook_id));
if (filters.topic_id) params.set("topic_id", String(filters.topic_id));
if (filters.project_id) params.set("project_id", String(filters.project_id));
const qs = params.toString();
const data = await apiGet<{ rules: Rule[] }>(`/api/rules${qs ? `?${qs}` : ""}`);
return data.rules;
}
export async function getRule(id: number): Promise<Rule> {
return apiGet(`/api/rules/${id}`);
}
export async function createRule(topicId: number, data: { title: string; statement: string; why?: string; how_to_apply?: string; order_index?: number }): Promise<Rule> {
return apiPost(`/api/rulebook-topics/${topicId}/rules`, data);
}
export async function updateRule(id: number, data: Partial<{ title: string; statement: string; why: string; how_to_apply: string; order_index: number }>): Promise<Rule> {
return apiPatch(`/api/rules/${id}`, data);
}
export async function deleteRule(id: number): Promise<void> {
return apiDelete(`/api/rules/${id}`);
}
// ── Subscriptions ──────────────────────────────────────────────────
export async function subscribeProject(projectId: number, rulebookId: number): Promise<void> {
await apiPost(`/api/projects/${projectId}/rulebook-subscriptions`, { rulebook_id: rulebookId });
}
export async function unsubscribeProject(projectId: number, rulebookId: number): Promise<void> {
return apiDelete(`/api/projects/${projectId}/rulebook-subscriptions/${rulebookId}`);
}
export async function getProjectApplicableRules(projectId: number): Promise<ApplicableRules> {
return apiGet(`/api/projects/${projectId}/rules`);
}
export async function createProjectRule(
projectId: number,
data: { statement: string; title?: string; why?: string; how_to_apply?: string },
): Promise<Rule> {
return apiPost(`/api/projects/${projectId}/rules`, data);
}