features / X-cross-cutting / design.md

X design

Written against spec.md in this folder. Cross-cutting rules cover the deploy, the safety checks, the time budgets, keyboard and contrast, and the 30-day success queries. Feature specs own the screens and the row shapes. This design names the modules that enforce the rules and the data those rules read.

The current release is the editor. postgresInUse() in lib/storage/mode.ts (F07) is false. Drafts stay in the browser. Sign-in, the Inbox, agent keys, and the success queries are not started. ADR-0034 is Proposed. X-Q-007 blocks treating X-REQ-013, X-REQ-014, and X-REQ-017 as accepted. The modules below still implement those requirements.

X adds no error code. A refusal uses the code owned by the feature named in the spec.

Modules

Current release

These modules ship. They stay the path while the current release is the editor.

ModuleRole
app/layout.tsx, app/page.tsx, app/[project]/page.tsx, app/[project]/[...slug]/page.tsxOne Next.js app. No sign-in route and no session.
app/api/files/route.tsGET and POST return 404 unless KB_LOCAL is the string 1.
app/api/mcp/route.tsGET returns 404 unless KB_LOCAL is 1. When it is 1, the content directory comes from KB_DIR.
lib/store/fs-store.tsResolves the content directory from KB_DIR on the server.
lib/markdown/schema.tssanitizeSchema. The safe tags are the sanitizer default plus details, summary, kbd, mark, sub, and sup.
components/markdown-parts.tsxRuns rehype-sanitize with sanitizeSchema after raw HTML is parsed.
components/blocks.tsxMermaidBlock sets securityLevel: "strict". usesRemoteData and ChartBlock refuse a remote chart URL before vega-embed.
components/use-editor-keys.tsThe editor shortcuts, with no pointer.
components/file-sidebar.tsxSearch control. The accessible name is the visible word Search.
components/document-chrome.tsxShare control, accessible name Share. Settings control, accessible name Settings. Status line words.
components/outline-panel.tsxOutline region, accessible name Outline.
components/settings-host.tsxSettings dialog, accessible name Settings.
components/workspace-store.tsWorking copy and browser-only pages in localStorage.
components/history-store.tsBrowser version history in localStorage.
components/draft-store.tsEditing mode in localStorage. It does not post a draft.
components/disk-sync.tsCopies a local project to disk when KB_LOCAL is 1. That copy is not a server draft.

The Vercel project is markdown-kb-editor. The deploy named in specs/PLAN.md is markdown-kb-editor.vercel.app. There is no app/sign-in/page.tsx in this release.

Not started

These modules are not in the tree. Each one calls the feature module named beside it. X does not replace that feature.

ModuleCallsRule
lib/charts/parse.ts, lib/charts/csv-data.ts, lib/charts/metrics-data.tsChartBlock, before vega-embedChart sources beyond inline values. F05 owns the parsers.
lib/assets/signed-url.tsassets in the storage contractSigned URL for an uploaded asset. The page does not receive blob_url.
lib/review/inbox.ts, components/review/inbox-list.tsxF12Inbox row within 2 seconds.
F17 document index and palettePostgres full-text search and pg_trgmSearch under 100 milliseconds. Not components/page-search.tsx.
lib/agent/log-experiment.tsget_template, search, propose_change in lib/agent/tools.tsOne sentence to one proposal within 10 seconds.
app/globals.css, app/theme.cssToken colorsContrast ratios in both themes.
components/workspace-view.tsx and the dialog hostsFiles column, page, outline, then the open dialogFocus order and the dialog trap.
components/document-chrome.tsxPage modeAccessible state Editing or Preview.
lib/auth/permissions.tsspaces.public_readDocuments shown. Proposals, keys, members, and audit events hidden.
lib/agent/keys.tsagent_keysSecret shown once. Stored hash and prefix.
lib/audit/record.tsThe committing transactionOne audit event for reject, role change, key creation, and key revocation.
lib/success/attempts.tslib/agent/proposals.ts when propose_change returnsOne validation-attempt row. No proposal when the result is invalid.
lib/success/queries.tsDocuments, proposals, attempts, usesThe five window queries.
lib/success/visits.tsA space open or a tool callOne space_uses row for a person.
lib/storage/connection.ts, lib/assets/signed-url.tsOne Postgres database, one blob storeLater hosting stays one Next.js app.

lib/review/merge.ts does not call lib/audit/record.ts. The merge audit row is the one insert inside append_revision (F07). A second insert would break the one-event rule.

Keyboard completion for the Inbox, Timeline, and Metrics uses the bindings those features already own (F12, F16, F18, F20). X adds no shortcut.

Data

Current release

localStorage keys, on the device:

KeyHolds
markdown-kb:workspace:{project}Working copy, including a page created in the browser. The legacy key markdown-kb:workspace is the same store for the legacy project.
markdown-kb:history:{id}Browser snapshots for one page. A snapshot is not a revision.

No client module reads process.env.KB_DIR or process.env.KB_LOCAL. The server modules in the table above read them. The client JavaScript of the deploy contains neither name and contains no agent-key secret. No agent-key secret exists in this release.

usesRemoteData walks every nested object and array. A string property named url that matches /^https?:/i is remote. ChartBlock then does not call vega-embed. The message is Charts can only use inline data.

Chart sources, once they are on

Inline data.values stay inline. An allowed CSV path is the path in F05-REQ-014. A metrics query is the object in F05-REQ-015. Any other source, including data.url of ../secret.csv, returns chart_source with the message and hint in the spec, and does not call the embedder. The ^https?: refusal stays in front of this check.

Signed asset address

assets.blob_url is the stored object address. The address written into a page is the signed URL from lib/assets/signed-url.ts for that row. The raw blob_url is not copied into the HTML.

Clocks

A day is 24 hours. The three product clocks have no percentile and no machine (X-Q-001, X-Q-002, X-Q-003, X-Q-004). This design does not add one.

ClockStartsStopsPasses
InboxThe commit that stored the proposalThe row is shown in an Inbox that is already openElapsed time of 2 seconds
SearchThe search call against a space of up to 10,000 documentsThe result list is returnedElapsed time under 100 milliseconds. 100 milliseconds misses
Agent logThe sentence is acceptedpropose_change has stored one valid proposalElapsed time of 10 seconds

The agent-log clock covers get_template, then search, then propose_change. It does not split model time out. X-Q-005 is open. The page-search dialog is not the search clock.

A miss has no extra user-facing error.

Audit row

The row is audit_events in the storage contract: space_id, actor_user_id, agent_key_id, action, target, data, created_at. X does not add a column.

ActionWriterWhen
mergeappend_revisionThe merge transaction
rejectlib/audit/record.ts from lib/review/review-actions.tsThe reject transaction
role_changelib/audit/record.ts from lib/auth/audit.tsThe membership transaction
key_createlib/audit/record.ts from lib/agent/keys.tsThe create transaction
key_revokelib/audit/record.ts from lib/agent/keys.tsThe revoke transaction

Each action inserts one row and commits it with the action. A rollback keeps the previous audit rows and adds none.

Agent key record

agent_keys.key_hash is 64 lowercase hex characters, the SHA-256 of the secret. key_prefix is the visible prefix from F13-REQ-008. The create result includes the secret. After the owner leaves that result, the secret is not read back and is not in the client bundle.

Validation attempt

X adds validation_attempts to the storage contract. That contract is not in this branch.

ColumnRule
space_idThe space of the call
credential_kindkey or grant
credential_idThat key or grant
atDatabase time when propose_change returns
validTrue only when the result is valid
had_get_contextTrue when that same credential had called get_context before this return

An invalid return inserts one attempt and no proposals row. A later get_context does not change had_get_context.

Success windows

The caller passes end, a timestamptz. A 720-hour window starts at end minus 720 hours. A 168-hour window starts at end minus 168 hours. A timestamp is inside when it is greater than or equal to the start and less than end.

lib/success/queries.ts returns the measurement. It does not return a pass or a fail. "Under" is strict.

QueryReturnsTarget, compared by the caller
Experiments recordedCount of documents with type experiment in the space whose spaces.slug is memento, created_at inside the 720-hour window. Archived documents stay in the count.100% of runs the owner knows. The product returns the count (X-Q-006).
Agent-proposal shareOf that same set, the percentage whose creating proposal has agent_key_id set or oauth_grant_id set.80% or more
Median reviewMedian of reviewed_at minus created_at, in minutes.Under 2 minutes. 2 minutes misses
Validation failuresPercentage of attempts in the 720-hour window with had_get_context true that have valid false. Attempts with had_get_context false are excluded.Under 10%. 10% misses
External buildersCount of distinct user_id with a space_uses row in the 168-hour window, excluding a user whose membership on that use's space has role owner.5. Optional

The creating proposal is the proposal id on the document's earliest revision. A null proposal id is a human direct save: it stays in the share's denominator and stays out of the numerator. oauth_grant_id is the column F12 adds. This design does not add it.

Share is numerator / denominator * 100, exact. The median sorts durations ascending. An odd count takes the middle duration. An even count takes the exact mean of the two central durations. Minutes are seconds divided by 60, exact. A zero denominator returns null, not 0. A window with no decided proposal returns a null median.

A proposal counts toward the median only when status is merged or rejected and reviewed_at is inside the window. open, changes_requested, and withdrawn are outside.

Space use

X adds space_uses to the storage contract. That contract is not in this branch.

ColumnRule
space_idThe space that was opened or whose tool was called
user_idThe person. A tool call uses the key's owning user or the grant's user
kindopen or tool
atDatabase time of the open or the call

A caller with no user id is not a person and is not stored. The current release writes no rows.

Accessible state

The visible status line stays Viewing, Editing, or Markdown source. The page column also exposes a state the accessibility tree reads:

ModeAccessible state
previewPreview
edit or sourceEditing

The state is a visually hidden status inside the page column. Its text is exactly that word. Turning editing off returns Preview.

Focus

Tab order while all three columns are shown and no dialog is open: the files column, then the page, then the outline, top to bottom inside each. A column that is not shown is skipped, including the outline below the 860px shell breakpoint and a closed file drawer. While a dialog is open, Tab stays inside it. When it closes, focus returns to the control that opened it.

Public read

While spaces.public_read is true, a caller with no membership receives the document and receives no proposal, agent key, member, or audit row. The codes and hints stay F11-REQ-030. The deployed share sheet does not set public_read.

State

StateWhere it livesWhat it changes
Current releaseNo session, postgresInUse() falseFile routes 404 unless KB_LOCAL is 1. Drafts stay in localStorage.
KB_LOCALServer environmentThe string 1 enables /api/files and /api/mcp. Any other value, including unset, is 404.
Chart sources beyond inlineThe F05 parsers are wiredThe allowlist runs. Until then, only the remote-URL refusal runs.
Ledger database in usepostgresInUse() trueOne Next.js app, one Postgres database, one blob store. No queue, no worker, no separate MCP service. MCP stays app/api/mcp/route.ts.
Dialog openThe dialog hostFocus is trapped, then restored to the opener.
Themedocument.documentElement.dataset.themelight or dark. Both are measured for contrast.

Contrast, measured on both themes: body text at least 4.5:1, text of at least 24px or at least 18.5px and bold at least 3:1, a component and the focus indicator at least 3:1 against the adjacent color.

Contracts

None of these files are in this branch.

ContractBranchWhat X uses
specs/contracts/db.sqlorigin/cursor/spec-storage-db-f59cspaces.public_read, documents, proposals, agent_keys, assets, audit_events. X adds validation_attempts and space_uses there when those tasks are implemented. This branch does not add the file.
specs/contracts/tokens.mdorigin/cursor/foundations-specs-839bThe colors both themes paint. Contrast is measured on those colors.
specs/contracts/keymap.mdorigin/cursor/foundations-specs-839bShortcuts the editor already runs. X adds no binding.
specs/contracts/errors.mdorigin/cursor/design-tasks-format-types-validation-279eX adds no code. chart_remote, chart_source, permission_denied, and audit_immutable stay with F05, F11, and F15.

F04 owns the safe-tag list. F05 owns chart parsers. F12 owns the Inbox row. F13 owns the three tools and the key record. F15 owns sign-in secrets, once sign-in exists. F16 owns metric points, not these success queries. F17 owns the document index. The page-search dialog stays a filter of pages already loaded in the browser.