From 00c7badc3ff637d0e48503659173fd778bef8d01 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 21 Aug 2026 22:09:18 -0400 Subject: [PATCH] =?UTF-8?q?feat(inception):=20UI=20=E2=80=94=20New-project?= =?UTF-8?q?=20modal=20step=202,=20InceptionCard=20on=20the=20project=20pag?= =?UTF-8?q?e,=20Rules=20tab=20shows=20excluded=20always-on=20rulebooks;=20?= =?UTF-8?q?REST=20exclusion=20routes=20(#2883,=20milestone=20297=20step=20?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - components/InceptionCard.vue: the one form, two homes — mode="create" in the New-project modal's second step (emits the choices; the create carries `inception`), mode="decide" on ProjectView for the owner of an undecided project (loads that project's defaults, records the decision). Always-on rulebooks listed checked (uncheck = exclude), others unchecked (check = subscribe), design system select, seed-Systems toggle (disabled once the project has Systems). Tokens only; modal canon (#2855); .btn-* canon. - ProjectView: the card while undecided, one "Inheritance decided via … · …" line after; onDecided refreshes the project. - ProjectRulesTab: "Excluded always-on rulebooks" section with include-back. - api/inception.ts (types, fetchInceptionDefaults, decideInception); api/rulebooks.ts: ApplicableRules.excluded_always_on, exclude/include wrappers; REST POST/DELETE /api/projects//exclusions/rulebooks/. Co-Authored-By: Claude Fable 5 --- frontend/src/api/inception.ts | 42 ++++ frontend/src/api/rulebooks.ts | 13 ++ frontend/src/components/InceptionCard.vue | 189 ++++++++++++++++++ .../src/components/rules/ProjectRulesTab.vue | 35 +++- frontend/src/views/ProjectListView.vue | 36 +++- frontend/src/views/ProjectView.vue | 31 +++ src/scribe/routes/rulebooks.py | 26 +++ tests/test_inception_rules.py | 7 + 8 files changed, 367 insertions(+), 12 deletions(-) create mode 100644 frontend/src/api/inception.ts create mode 100644 frontend/src/components/InceptionCard.vue diff --git a/frontend/src/api/inception.ts b/frontend/src/api/inception.ts new file mode 100644 index 0000000..05773e1 --- /dev/null +++ b/frontend/src/api/inception.ts @@ -0,0 +1,42 @@ +/** Project inception (milestone 297): what a project was decided to inherit. */ +import { apiGet, apiPost } from "@/api/client"; + +export interface InceptionChoices { + exclude_always_on_rulebooks: number[]; + subscribe_rulebooks: number[]; + design_system_id: number | null; + seed_systems: boolean; +} + +export interface InceptionRecord { + decided_at: string; + decided_by: number | null; + via: "mcp" | "ui" | "legacy"; + choices: InceptionChoices; +} + +export interface InceptionDefaults { + always_on_rulebooks: { id: number; title: string }[]; + other_rulebooks: { id: number; title: string }[]; + excluded_always_on: { id: number; title: string }[]; + subscribed_rulebooks: { id: number; title: string }[]; + design_system_id: number | null; + design_systems: { id: number; title: string }[]; + systems: number; +} + +export interface InceptionDecision { + project_id: number; + inception: InceptionRecord; + effects: { excluded: number[]; subscribed: number[]; design_system_id: number | null; systems_seeded: string[] }; +} + +export const emptyChoices = (): InceptionChoices => ({ + exclude_always_on_rulebooks: [], subscribe_rulebooks: [], design_system_id: null, seed_systems: false, +}); + +export const fetchInceptionDefaults = (projectId: number) => + apiGet(`/api/projects/${projectId}/inception/defaults`); + +export const decideInception = (projectId: number, choices: InceptionChoices) => + apiPost(`/api/projects/${projectId}/inception`, { choices }); diff --git a/frontend/src/api/rulebooks.ts b/frontend/src/api/rulebooks.ts index 858d854..1c52447 100644 --- a/frontend/src/api/rulebooks.ts +++ b/frontend/src/api/rulebooks.ts @@ -71,6 +71,8 @@ export interface ApplicableRules { }[]; truncated: boolean; subscribed_rulebooks: { id: number; title: string }[]; + /** Always-on rulebooks this project opted out of at inception (milestone 297). */ + excluded_always_on: { id: number; title: string }[]; } // ── Rulebooks ─────────────────────────────────────────────────────── @@ -181,3 +183,14 @@ export async function suppressTopicForProject(projectId: number, topicId: number export async function unsuppressTopicForProject(projectId: number, topicId: number): Promise { return apiDelete(`/api/projects/${projectId}/suppressions/topics/${topicId}`); } + +// ── Always-on exclusions (milestone 297) ──────────────────────────────────── + +export async function excludeAlwaysOnRulebook(projectId: number, rulebookId: number): Promise { + await apiPost(`/api/projects/${projectId}/exclusions/rulebooks/${rulebookId}`, {}); +} + +export async function includeAlwaysOnRulebook(projectId: number, rulebookId: number): Promise { + await apiDelete(`/api/projects/${projectId}/exclusions/rulebooks/${rulebookId}`); +} + diff --git a/frontend/src/components/InceptionCard.vue b/frontend/src/components/InceptionCard.vue new file mode 100644 index 0000000..b37699b --- /dev/null +++ b/frontend/src/components/InceptionCard.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/frontend/src/components/rules/ProjectRulesTab.vue b/frontend/src/components/rules/ProjectRulesTab.vue index 4749279..cd989b0 100644 --- a/frontend/src/components/rules/ProjectRulesTab.vue +++ b/frontend/src/components/rules/ProjectRulesTab.vue @@ -2,10 +2,18 @@ import { ref, onMounted, watch } from "vue"; import { useRouter } from "vue-router"; import { - getProjectApplicableRules, subscribeProject, unsubscribeProject, - listRulebooks, getRule, createProjectRule, deleteRule, - suppressRuleForProject, unsuppressRuleForProject, - suppressTopicForProject, unsuppressTopicForProject, + getProjectApplicableRules, + subscribeProject, + unsubscribeProject, + listRulebooks, + getRule, + createProjectRule, + deleteRule, + suppressRuleForProject, + unsuppressRuleForProject, + suppressTopicForProject, + unsuppressTopicForProject, + includeAlwaysOnRulebook, } from "@/api/rulebooks"; import type { ApplicableRules, Rulebook } from "@/api/rulebooks"; @@ -35,6 +43,11 @@ async function subscribe(rulebookId: number) { await load(); } +async function includeBack(rulebookId: number) { + await includeAlwaysOnRulebook(props.projectId, rulebookId); + await load(); +} + async function unsubscribe(rulebookId: number) { if (!confirm("Unsubscribe from this rulebook for this project?")) return; await unsubscribeProject(props.projectId, rulebookId); @@ -172,6 +185,17 @@ watch(() => props.projectId, load); +
+

Excluded always-on rulebooks

+

Opted out at inception — these do not bind this project.

+
+ + {{ rb.title }} + + +
+
+

Project rules

@@ -321,6 +345,9 @@ watch(() => props.projectId, load);