diff --git a/alembic/versions/0075_retire_design_rulebook_setting.py b/alembic/versions/0075_retire_design_rulebook_setting.py index 933b59b..6a3250a 100644 --- a/alembic/versions/0075_retire_design_rulebook_setting.py +++ b/alembic/versions/0075_retire_design_rulebook_setting.py @@ -1,28 +1,29 @@ -"""retire the design_rulebook_id setting +"""retire the two settings that designated a design source for the app itself Revision ID: 0075 Revises: 0074 Create Date: 2026-08-03 -The /design panel used to compare a design RULEBOOK's prose claims against the -live tokens. That rulebook was imported into the design system and retired, and -the panel now compares the running app against the design system it was -generated from (#2419). Its designation moved with it: +Two keys, retired for the same reason a week apart, so they go in one change +rather than one migration each: - design_rulebook_id -> ui_design_system_id + design_rulebook_id which rulebook described how this app should look + ui_design_system_id which design system this app's own UI was built from -Nothing reads the old key any more, so this deletes the row rather than leaving -an inert one behind (rule #22 — remove the old path, including the setting it -read from). The values are not translatable: a rulebook id and a design system -id are ids in different tables, and guessing a mapping would silently point the -new panel at the wrong system. +Both named a design source for THE RUNNING INSTALL. The design surface is for +the projects an install tracks, and a project already carries its own pointer +(`projects.design_system_id`) — so an install-wide designation had nothing left +to mean. `ui_design_system_id` was introduced by this same migration's first +draft and never reached a deployed database; it is listed here rather than +undone by an 0076 that would reverse a change nobody ran. Deleting settings rows by key is safe in a way dropping a column is not — the table is free-form key/value, so an install that never designated one simply has no row to delete. Downgrade cannot restore what it never recorded, so it is a no-op rather than a -lie: the operator re-designates in Settings. +lie: the pointer lives on the project now, and always did for anyone who set it +there. """ from alembic import op import sqlalchemy as sa @@ -33,10 +34,12 @@ down_revision = "0074" branch_labels = None depends_on = None - def upgrade() -> None: op.execute( - sa.text("DELETE FROM settings WHERE key = 'design_rulebook_id'") + sa.text( + "DELETE FROM settings " + "WHERE key IN ('design_rulebook_id', 'ui_design_system_id')" + ) ) diff --git a/frontend/src/api/design.ts b/frontend/src/api/design.ts deleted file mode 100644 index d8d7d04..0000000 --- a/frontend/src/api/design.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { apiGet } from "@/api/client"; - -export interface UiSystemResponse { - design_system_id: number | null; - title: string | null; -} - -/** The design system this install says its own UI is built from. - * - * Both nulls means none designated — the normal state for a fresh install, not - * an error; the caller shows an explanatory empty state. An id with a null - * title means designated but deleted or unreadable, which is a - * misconfiguration and must not be rendered as "none". */ -export const fetchUiDesignSystem = () => - apiGet("/api/design/ui-system"); diff --git a/frontend/src/components/DesignTabs.vue b/frontend/src/components/DesignTabs.vue deleted file mode 100644 index b00e14d..0000000 --- a/frontend/src/components/DesignTabs.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - - diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 5e9bf81..0d590f7 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -110,15 +110,11 @@ const router = createRouter({ component: () => import("@/views/RulesView.vue"), }, { - // Meta-surface, same family as /rules: it describes the app rather than - // holding the operator's records. - path: "/design", - name: "design", - component: () => import("@/views/DesignView.vue"), - }, - { - // The editable half of the same surface: /design is what the browser - // renders, /design-systems is the record that ought to decide it. + // The design systems this install RECORDS — for the projects it tracks, + // not for the install itself. There was a sibling `/design` that read the + // running app's own stylesheet out of the browser; it could only ever + // inspect the instance it was served from, which made it a mirror rather + // than a tool (#274). path: "/design-systems", name: "design-systems", component: () => import("@/views/DesignSystemsView.vue"), diff --git a/frontend/src/utils/designDrift.ts b/frontend/src/utils/designDrift.ts deleted file mode 100644 index 5bd1332..0000000 --- a/frontend/src/utils/designDrift.ts +++ /dev/null @@ -1,235 +0,0 @@ -/** - * Agreement — does the running app match the design system it was built from? - * - * This panel used to compare a design RULEBOOK's prose claims against the live - * tokens. That rulebook was retired into the design system on 2026-08-01, and - * the feature spent two days rendering a reassuring empty state instead of - * failing (#2419). Its replacement is not the same question re-aimed: comparing - * a design system against a stylesheet generated from that same design system - * would be a tautology. - * - * The question that survives is the one no server can answer. A sheet still has - * to be LOADED and APPLIED, and until now nothing checked that it was. Three - * failures live in that gap: - * - * absent the app has no such token at all — the sheet was never - * regenerated after the record changed, or never loaded - * differs the app has the token with another value — a stale copy of the - * sheet, or a later rule that overrode it - * unrecorded the app declares a token in the record's own family that the - * record has never heard of — hand-editing that outlived its reason - * - * SCOPE, and it is a real limit rather than an omission. This compares the - * record against the TOKENS. A literal hardcoded in a component where a token - * should be referenced is invisible here, because the drift isn't in the tokens - * at all — that check has the component sources and belongs in CI (#2277). - * Saying so in the panel matters: a report that silently omits a category - * invites the reader to conclude the category is clean. - */ -import type { DesignToken } from "@/utils/designTokens"; - -/** The base mode's key in a token's `value_by_mode`, mirroring services/design_stylesheet. */ -export const BASE_MODE = "base"; - -/** One token as the RECORD has it, already narrowed to the mode being checked. */ -export interface RecordedToken { - name: string; - /** Declared value for this mode, or "" when the role is named but unvalued. */ - value: string; - groupName: string | null; -} - -export type AgreementStatus = "ok" | "absent" | "differs" | "unrecorded"; - -export interface Agreement { - name: string; - groupName: string | null; - /** What the record declares, resolved. Empty for an `unrecorded` row. */ - recorded: string; - /** What the browser resolved. Empty for an `absent` row. */ - live: string; - status: AgreementStatus; -} - -/** - * Which declared value applies when the page is in `mode`. - * - * Falls back to base, which is the storage model rather than a convenience: a - * mode block is an OVERRIDE layer, so a token with no entry for the current - * mode is not missing — it is inheriting, exactly as the sheet has it. - */ -export function valueForMode( - valueByMode: Record, - mode: string, -): string { - const own = valueByMode[mode]; - if (own !== undefined && own !== "") return own; - return valueByMode[BASE_MODE] ?? ""; -} - -/** - * Normalise a colour for comparison. - * - * A record writes `#FFFFFF`, a sheet writes `#fff`, and - * getComputedStyle can hand back `rgb(255, 255, 255)` — three spellings of one - * colour, and a comparison that misses any of them over-reports drift, which is - * the failure that gets a panel ignored. The rgb() case is browser-specific and - * therefore has no server-side counterpart, which is exactly why it is here. - */ -export function normalizeColour(value: string): string | null { - const raw = value.trim().toLowerCase(); - - const hex = /^#([0-9a-f]{3,8})$/.exec(raw); - if (hex) { - let digits = hex[1]; - if (digits.length === 3 || digits.length === 4) { - digits = digits.split("").map((c) => c + c).join(""); - } - return digits.length === 6 || digits.length === 8 ? `#${digits}` : null; - } - - // getComputedStyle reports real colour properties as rgb()/rgba(), never as - // authored. Custom properties are token streams and usually come back as - // written, so this arm is insurance rather than the common path. - const rgb = /^rgba?\(([^)]+)\)$/.exec(raw); - if (rgb) { - const parts = rgb[1].split(/[,\s/]+/).filter(Boolean); - if (parts.length < 3) return null; - const channels = parts.slice(0, 3).map((p) => Number(p)); - if (channels.some((n) => !Number.isFinite(n))) return null; - const hexOf = (n: number) => Math.round(n).toString(16).padStart(2, "0"); - const base = `#${channels.map(hexOf).join("")}`; - if (parts.length === 3) return base; - const alpha = Number(parts[3]); - if (!Number.isFinite(alpha) || alpha >= 1) return base; - return `${base}${hexOf(alpha * 255)}`; - } - - return null; -} - -/** - * Compare two CSS values for sameness, not for identical text. - * - * Whitespace inside a compound value is not meaningful — `0 2px 10px` and - * `0 2px 10px` are one shadow — and neither is case, since a custom property - * carries no font names or content strings that would be changed by folding it. - * Colours go through the normaliser first so spelling differences don't read as - * drift. - */ -export function sameValue(a: string, b: string): boolean { - const canon = (v: string) => { - const trimmed = v.trim(); - return normalizeColour(trimmed) ?? trimmed.replace(/\s+/g, " ").toLowerCase(); - }; - return canon(a) === canon(b); -} - -/** - * The families the record claims, as name prefixes. - * - * Used to decide which live tokens count as `unrecorded`. An app's stylesheet - * legitimately carries names the record never owned — Scribe's own sheet keeps - * a `--color-*` alias layer over the design system's `--fs-*` block — and - * reporting those as drift would bury the real findings under a compatibility - * shim. So the record is treated as owning a FAMILY, identified by the prefix - * up to the first separator, and nothing outside it is judged. - * - * Derived from the data rather than configured, because the prefix is the - * install's choice (see design_starter_roles) and hardcoding one would put a - * single operator's naming into every install (rule #115). - */ -export function recordedFamilies(names: Iterable): string[] { - const families = new Set(); - for (const name of names) { - const match = /^(--[A-Za-z0-9]+-)/.exec(name); - if (match) families.add(match[1]); - } - return [...families]; -} - -/** - * Compare the record against the running app. - * - * `resolved` is the record's declared values after the browser has substituted - * `var()` in them (see `resolveDeclared`) — the same treatment the live values - * already received, which is what makes the two comparable. - * - * Tokens the record names but has no value for are SKIPPED, not reported. A - * valueless token is a role awaiting a decision, and the stylesheet already - * reports those under `valueless`; counting them as drift would mean a system - * created with starter roles opens this panel red on day one. - */ -export function compareToApp( - recorded: RecordedToken[], - resolved: Map, - live: DesignToken[], -): Agreement[] { - const liveByName = new Map(); - for (const token of live) liveByName.set(token.name, token.value); - const out: Agreement[] = []; - - for (const token of recorded) { - if (!token.value) continue; - const declared = resolved.get(token.name) ?? token.value; - const actual = liveByName.get(token.name) ?? ""; - out.push({ - name: token.name, - groupName: token.groupName, - recorded: declared, - live: actual, - status: !actual ? "absent" : sameValue(declared, actual) ? "ok" : "differs", - }); - } - - const known = new Set(recorded.map((t) => t.name)); - const families = recordedFamilies(known); - for (const token of live) { - if (known.has(token.name)) continue; - if (!families.some((prefix) => token.name.startsWith(prefix))) continue; - out.push({ - name: token.name, - groupName: null, - recorded: "", - live: token.value, - status: "unrecorded", - }); - } - - return out; -} - -export interface AgreementSummary { - ok: number; - absent: number; - differs: number; - unrecorded: number; - total: number; -} - -export function summarise(agreements: Agreement[]): AgreementSummary { - const summary: AgreementSummary = { - ok: 0, absent: 0, differs: 0, unrecorded: 0, total: agreements.length, - }; - for (const a of agreements) summary[a.status] += 1; - return summary; -} - -/** - * Findings worth leading with. - * - * A panel that opens with every row gets closed and never reopened. `absent` - * leads because it is the one status that can mean the whole sheet is missing; - * `differs` next, because a wrong value is being rendered right now; - * `unrecorded` last, since it is a bookkeeping gap rather than a visible fault. - * `ok` rows are not findings at all and belong behind an expansion. - */ -export function rankAgreements(agreements: Agreement[]): Agreement[] { - const order: Record = { - absent: 0, differs: 1, unrecorded: 2, ok: 3, - }; - return [...agreements].sort((a, b) => { - const byStatus = order[a.status] - order[b.status]; - return byStatus !== 0 ? byStatus : a.name.localeCompare(b.name); - }); -} diff --git a/frontend/src/utils/designTokens.ts b/frontend/src/utils/designTokens.ts deleted file mode 100644 index 802ccc8..0000000 --- a/frontend/src/utils/designTokens.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Design-token inventory — what tokens exist, and what they actually resolve to. - * - * Foundation for the design explorer (milestone #251): the gallery renders - * against these, and the agreement panel compares them to the design system the - * install says its UI is built from (#2419). - * - * DESIGN NOTE — why this parses NAMES but never VALUES. - * Extracting `--foo` from a stylesheet is a trivial, robust regex. Extracting - * its VALUE is not: values contain nested parens, commas inside rgba(), - * `var()` references to other tokens, multi-part shadows, and gradients — and - * `theme.css` has all of those today. So we take the names from the source and - * ask the BROWSER for every value. - * - * That is not just easier, it is more correct. getComputedStyle reports what - * actually won the cascade, resolves `var()` chains, and — critically for this - * milestone — reflects live overrides set on a container, which is exactly what - * the preview surface needs (see #2261). Parsing the source would report what - * the file says rather than what the user is looking at. - * - * It also means this module needs no unit tests to be trustworthy: the only - * logic here is a name regex and a group lookup. The frontend has no test - * runner today (`vue-tsc --noEmit` is the whole check), so keeping the - * error-prone half in the browser rather than in our code is deliberate. - */ -import themeCss from "@/assets/theme.css?raw"; - -export type TokenGroup = - | "color" - | "radius" - | "gradient" - | "glow" - | "focus" - | "layout" - | "other"; - -export type ThemeMode = "light" | "dark"; - -export interface DesignToken { - /** Full custom-property name, including the leading `--`. */ - name: string; - /** Coarse family, derived from the name prefix. */ - group: TokenGroup; - /** Resolved value in the requested context, straight from the browser. */ - value: string; - /** True when the token is re-declared under a mode selector in source. */ - modeAware: boolean; -} - -/** - * Matches a custom-property DECLARATION, and never a `var(--name)` use. - * - * The discriminator is the COLON, not the preceding character. A declaration is - * `--name:`; a reference is `var(--name)` or `var(--name, fallback)` — followed - * by `)` or `,`, never by `:`. So no anchor is needed, and adding one is - * actively wrong: an earlier version required the match to follow `{` or `;`, - * which silently dropped every declaration that came after a comment — - * including `--color-bg`, the first and most-used token in the file. - */ -const DECLARATION = /(--[A-Za-z0-9_-]+)\s*:/g; - -/** Comments are stripped first so a commented-out declaration isn't counted. */ -const COMMENT = /\/\*[\s\S]*?\*\//g; - -/** - * Any mode-override block, whichever mode it names. - * - * This used to hardcode `[data-theme="dark"]`, and that stopped being true the - * day the sheet went dark-first: `:root` now carries dark and - * `[data-theme="light"]` overrides it. The hardcoded selector matched nothing, - * `overriddenInDark` was false for all 186 tokens, and the "mode-aware" flag - * silently vanished from the gallery — a UI that kept rendering, wrongly. - * - * Matching the SHAPE rather than one mode name is what makes that unrepeatable, - * and it is also the only version that holds for an install whose modes aren't - * light and dark (rule #115). `selector_for_mode` in services/design_stylesheet - * emits exactly this shape, so the two ends agree by construction. - */ -const MODE_SELECTOR = /\[data-theme=["']?[\w-]+["']?\]/g; - -const GROUP_PREFIXES: ReadonlyArray<[string, TokenGroup]> = [ - ["--color-", "color"], - ["--radius-", "radius"], - ["--gradient-", "gradient"], - ["--glow-", "glow"], - ["--focus-", "focus"], - ["--page-", "layout"], - ["--sidebar-", "layout"], - ["--chat-", "layout"], -]; - -export function groupFor(name: string): TokenGroup { - for (const [prefix, group] of GROUP_PREFIXES) { - if (name.startsWith(prefix)) return group; - } - return "other"; -} - -/** Every custom property declared anywhere in the stylesheet, in source order, deduped. */ -export function tokenNames(css: string = themeCss): string[] { - const seen = new Set(); - const out: string[] = []; - for (const match of css.replace(COMMENT, "").matchAll(DECLARATION)) { - const name = match[1]; - if (!seen.has(name)) { - seen.add(name); - out.push(name); - } - } - return out; -} - -/** The subset re-declared under a mode selector — i.e. tokens that change with mode. */ -export function modeOverriddenNames(css: string = themeCss): Set { - const bare = css.replace(COMMENT, ""); - const names = new Set(); - for (const match of bare.matchAll(MODE_SELECTOR)) { - if (match.index === undefined) continue; - const open = bare.indexOf("{", match.index + match[0].length); - if (open === -1) continue; - // A custom-property block is flat, so the first `}` closes it. Anything - // nested would be a rule, not a declaration, and has no tokens to find. - const close = bare.indexOf("}", open); - if (close === -1) continue; - for (const name of tokenNames(bare.slice(open, close))) names.add(name); - } - return names; -} - -/** - * Read the resolved value of every token in `host`'s context. - * - * Pass a container to read the tokens as they apply INSIDE it — which is how - * the preview surface reads a scoped override without disturbing the page. - * Defaults to the document root, i.e. the app-wide values. - */ -export function readTokens(host: Element = document.documentElement): DesignToken[] { - const computed = getComputedStyle(host); - const modal = modeOverriddenNames(); - return tokenNames().map((name) => ({ - name, - group: groupFor(name), - value: computed.getPropertyValue(name).trim(), - modeAware: modal.has(name), - })); -} - -/** - * Read a set of DECLARED values as the browser would resolve them. - * - * The point is to compare like with like. A design system records - * `color-mix(in srgb, var(--fs-accent) 15%, transparent)`; the browser reports - * the same token with `var()` already substituted. Comparing those two strings - * marks every derived token as drift, which is a report nobody can read. - * - * So both sides go through the same engine: set the declarations on an - * offscreen probe, read them back, and the substitution is done by the - * implementation that will do it for real rather than by a parser of ours. - * Undeclared references fall through to the page's own values, which is what - * the cascade would do anyway. - */ -export function resolveDeclared(declared: Map): Map { - const probe = document.createElement("div"); - probe.style.display = "none"; - for (const [name, value] of declared) probe.style.setProperty(name, value); - document.body.appendChild(probe); - try { - const computed = getComputedStyle(probe); - const out = new Map(); - for (const name of declared.keys()) { - out.set(name, computed.getPropertyValue(name).trim()); - } - return out; - } finally { - probe.remove(); - } -} - -/** - * Read tokens as they would resolve in a given mode, without touching the page. - * - * Uses an offscreen probe carrying the mode attribute, so the live UI is never - * mutated to take a reading. - * - * KNOWN LIMITATION, and it is a property of the stylesheet rather than of this - * function: mode scoping is one-way. Whichever mode the sheet treats as its - * BASE lives on `:root` and has no attribute selector of its own, so a probe - * can add an overriding mode to a subtree but can never add the base mode back. - * - * The sheet is dark-first today — `:root` carries dark, `[data-theme="light"]` - * overrides it — so light-inside-dark previews work and dark-inside-light ones - * return the light values. That direction flipped when the sheet did, which is - * why this says "the base mode" rather than naming one: callers should treat a - * cross-mode read as best-effort either way. - */ -export function readTokensForMode(mode: ThemeMode): DesignToken[] { - const probe = document.createElement("div"); - probe.setAttribute("data-theme", mode); - probe.style.display = "none"; - document.body.appendChild(probe); - try { - return readTokens(probe); - } finally { - probe.remove(); - } -} - -/** - * Tokens grouped by family, preserving source order within each group. - * - * `overrides` maps a token name to the group it should sit under, and exists - * because the prefix table above can only know the families that shipped with - * the product. An install's own design system knows the groups it authored, so - * a caller holding the record passes them here rather than the taxonomy growing - * one operator's prefixes (rule #115). - */ -export function groupTokens( - tokens: DesignToken[], - overrides: Map = new Map(), -): Map { - const out = new Map(); - for (const token of tokens) { - const group = overrides.get(token.name) ?? token.group; - const bucket = out.get(group); - if (bucket) bucket.push(token); - else out.set(group, [token]); - } - return out; -} - -/** - * Tokens declared in the stylesheet that nothing references with `var()`. - * - * Dead tokens are drift too: `--chat-reading-width` and - * `--chat-context-sidebar-width` outlived the chat subsystem that was deleted - * in the MCP-first pivot, and nothing has referenced them since. Takes the - * corpus of source files to search as an argument so the caller decides what - * "used" means — this module has no opinion about the project layout. - */ -export function unreferencedTokens(tokens: DesignToken[], sources: string[]): DesignToken[] { - const haystack = sources.join("\n"); - return tokens.filter((token) => !haystack.includes(`var(${token.name}`)); -} diff --git a/frontend/src/utils/designValues.ts b/frontend/src/utils/designValues.ts new file mode 100644 index 0000000..6745bb0 --- /dev/null +++ b/frontend/src/utils/designValues.ts @@ -0,0 +1,88 @@ +/** + * Turning a design system's RECORDED values into ones you can look at. + * + * Replaces `designTokens.ts` and `designDrift.ts`, which between them read the + * running app's own stylesheet — names out of a bundled `theme.css`, values out + * of `getComputedStyle(document.documentElement)`. That could only ever describe + * the install serving the page, and the design surface is for the projects an + * install TRACKS (#274). What is left here works on any system's record, + * including one for an app this browser has never loaded. + * + * Nothing in this module reads the document's own tokens or mutates the page. + */ + +/** The base mode's key in `value_by_mode`, mirroring services/design_stylesheet. */ +export const BASE_MODE = "base"; + +/** + * Which declared value applies in `mode`. + * + * Falls back to base, which is the storage model rather than a convenience: a + * mode block is an OVERRIDE layer, so a token with no entry for the current + * mode is not missing — it is inheriting, exactly as the generated sheet has it. + */ +export function valueForMode( + valueByMode: Record, + mode: string, +): string { + const own = valueByMode[mode]; + if (own !== undefined && own !== "") return own; + return valueByMode[BASE_MODE] ?? ""; +} + +/** Every mode any token in the set declares, base first then the rest by name. */ +export function modesPresent( + tokens: { value_by_mode: Record }[], +): string[] { + const modes = new Set(); + for (const token of tokens) { + for (const [mode, value] of Object.entries(token.value_by_mode)) { + if (value) modes.add(mode); + } + } + const rest = [...modes].filter((m) => m !== BASE_MODE).sort(); + return modes.has(BASE_MODE) ? [BASE_MODE, ...rest] : rest; +} + +/** + * Resolve declared values the way a browser would, without applying them. + * + * A record holds `color-mix(in srgb, var(--fs-accent) 15%, transparent)`. Shown + * as text that is a string; shown as a swatch it needs `var()` substituted and + * the mix evaluated. Rather than write a CSS parser, set the declarations on an + * offscreen probe and read them back — the substitution is done by the + * implementation that would do it for real. + * + * Custom properties INHERIT, and `all: initial` does not reset them — so a probe + * sitting in this page would resolve any reference the record leaves undeclared + * against the surrounding app's own tokens. Previewing another project's system + * would then quietly borrow this one's palette wherever that system was + * incomplete, and a token the record already knows is broken (it shows up under + * `unknown_refs`) would render as though it were fine. + * + * So every name referenced but not declared is blanked on the probe first. It + * resolves to nothing, which is what the record says it is. + */ +const VAR_REFERENCE = /var\(\s*(--[A-Za-z0-9_-]+)/g; + +export function resolveDeclared(declared: Map): Map { + const probe = document.createElement("div"); + probe.style.display = "none"; + for (const value of declared.values()) { + for (const match of value.matchAll(VAR_REFERENCE)) { + if (!declared.has(match[1])) probe.style.setProperty(match[1], " "); + } + } + for (const [name, value] of declared) probe.style.setProperty(name, value); + document.body.appendChild(probe); + try { + const computed = getComputedStyle(probe); + const out = new Map(); + for (const name of declared.keys()) { + out.set(name, computed.getPropertyValue(name).trim()); + } + return out; + } finally { + probe.remove(); + } +} diff --git a/frontend/src/views/DesignSystemsView.vue b/frontend/src/views/DesignSystemsView.vue index ff50efd..856957a 100644 --- a/frontend/src/views/DesignSystemsView.vue +++ b/frontend/src/views/DesignSystemsView.vue @@ -1,9 +1,13 @@ - - - - diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index a291711..fd8615f 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -4,7 +4,6 @@ import { useSettingsStore } from "@/stores/settings"; import { useAuthStore } from "@/stores/auth"; import { useToastStore } from "@/stores/toast"; import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getProfile, updateProfile, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type UserProfile } from "@/api/client"; -import { fetchDesignSystems } from "@/api/designSystems"; import type { User } from "@/types/auth"; import PaginationBar from "@/components/PaginationBar.vue"; import TagInput from "@/components/TagInput.vue"; @@ -32,12 +31,6 @@ const kbWritePathThreshold = ref("0.68"); // gate: that one BLOCKS a create and must be unforgiving of noise, this one only // suggests a merge the operator reviews (services/dedup.py). const kbDuplicateThreshold = ref("0.82"); -// Which design system this install's own UI is built from, for the /design -// agreement panel. Empty = none designated, which is the normal state for a -// fresh install rather than a misconfiguration — the panel explains itself when -// unset. Replaced design_rulebook_id when the rulebook was retired (#2419). -const uiDesignSystemId = ref(""); -const designSystems = ref<{ id: number; title: string }[]>([]); const savingKbInject = ref(false); const kbInjectSaved = ref(false); @@ -107,9 +100,6 @@ async function saveKbInject() { kb_writepath_enabled: kbWritePathEnabled.value ? 'true' : 'false', kb_writepath_threshold: String(wpT), kb_duplicate_threshold: String(dupT), - // Empty string DELETES the setting (see routes/settings.py), which is - // exactly right for "no design system" — absent rather than zero. - ui_design_system_id: uiDesignSystemId.value, }); kbInjectSaved.value = true; setTimeout(() => (kbInjectSaved.value = false), 2000); @@ -500,16 +490,6 @@ onMounted(async () => { if (allSettings.kb_duplicate_threshold !== undefined) { kbDuplicateThreshold.value = allSettings.kb_duplicate_threshold; } - uiDesignSystemId.value = allSettings.ui_design_system_id ?? ""; - // Best-effort: the picker degrades to "none available" rather than blocking - // the whole settings page if design systems can't be listed. - try { - designSystems.value = (await fetchDesignSystems()).design_systems.map( - (s) => ({ id: s.id, title: s.title }), - ); - } catch { - designSystems.value = []; - } if (allSettings.notify_task_reminders !== undefined) { notifyTaskReminders.value = allSettings.notify_task_reminders !== "false"; } @@ -1280,25 +1260,12 @@ function formatUserDate(iso: string): string { location, not by resemblance.

-
- - -

- Which design system this interface is supposed to be built from. Once - set, the Design page compares - every token the system declares against what the browser has actually - resolved, and reports the ones the app is missing, renders differently, - or has never heard of. That catches a stylesheet that was regenerated - but never shipped — which the record alone cannot tell you, since the - sheet is generated from it. Leave it as None if this install's - interface isn't described by one of your design systems. -

-
+ +
Quart: app.register_blueprint(knowledge_bp) app.register_blueprint(rulebooks_bp) app.register_blueprint(plugin_bp) - app.register_blueprint(design_bp) app.register_blueprint(design_systems_bp) app.register_blueprint(trash_bp) app.register_blueprint(dashboard_bp) diff --git a/src/scribe/routes/design.py b/src/scribe/routes/design.py deleted file mode 100644 index e54dc15..0000000 --- a/src/scribe/routes/design.py +++ /dev/null @@ -1,45 +0,0 @@ -"""This install's UI surface — which design system it claims to be built from. - -Kept separate from the design-systems CRUD blueprint on purpose. That one is -the RECORD: create a system, move a token, read the cascade. This one answers a -question about the RUNNING APP, and it exists because those are not the same -question. A design system can be a perfect record of a stylesheet the app never -loaded. - -The client owns the other half. `utils/designTokens.ts` reads what the browser -actually resolved, which is the one thing no server can report, and compares it -to what this endpoint's system declares. So the comparison is -"does the app agree with its own sheet?" rather than "is the record -self-consistent?", which would be a tautology — the sheet is generated from the -record (#2419). -""" -from quart import Blueprint, jsonify - -from scribe.auth import get_current_user_id, login_required -from scribe.services import design_systems as ds_svc - -design_bp = Blueprint("design", __name__, url_prefix="/api/design") - - -@design_bp.get("/ui-system") -@login_required -async def get_ui_system(): - """The design system this install designated as the source of its own UI. - - Returns `{"design_system_id": int|null, "title": str|null}`. - - Both nulls is the NORMAL case, not an error — an install that has not - designated one has nothing to check the running app against, and the client - shows an explanatory empty state (rule #115). - - An id with a null title is the third case and the reason the id is returned - separately: designated, but deleted or not readable by this caller. Folding - that into "none designated" is precisely how a feature comes to render a - reassuring empty state forever. - """ - uid = get_current_user_id() - system_id, system = await ds_svc.ui_design_system(uid) - return jsonify({ - "design_system_id": system_id, - "title": system.title if system else None, - }) diff --git a/src/scribe/services/design_systems.py b/src/scribe/services/design_systems.py index 8e2a4ae..522829d 100644 --- a/src/scribe/services/design_systems.py +++ b/src/scribe/services/design_systems.py @@ -37,7 +37,6 @@ from scribe.services.design_cascade import ( resolve_tokens, would_cycle, ) -from scribe.services.settings import get_setting logger = logging.getLogger(__name__) @@ -135,40 +134,6 @@ async def get_design_system(user_id: int, design_system_id: int) -> DesignSystem return system -# Which design system this install's own UI is built from. A plain setting -# rather than a column: no migration, discoverable in the Settings UI (rule -# #25), and honest about being a per-install claim rather than a property of the -# system — the same system can be the record for an app that never loads it. -UI_DESIGN_SYSTEM_SETTING = "ui_design_system_id" - - -async def ui_design_system(user_id: int) -> tuple[int | None, DesignSystem | None]: - """The design system this install says its UI is built from. - - Returns `(id, system)`. Three outcomes, deliberately distinguishable: - - - `(None, None)` — nothing designated. The NORMAL state for any install but - the one that set it up (rule #115), not an error. - - `(id, None)` — designated, but gone or not readable by this caller. A - misconfiguration worth naming rather than silently degrading to "none", - which is exactly the failure that orphaned the panel this feeds (#2419). - - `(id, system)` — designated and readable. - - A non-numeric setting value reads as nothing designated: the value is only - ever written by a `