Ottili Foundation Lab
The conformance report for the Ottili Foundation Framework. Every scenario below is a real configuration of the shipped packages — nothing here is a mock or a screenshot. The application itself lives at /c/acme/dashboard.
Coverage
Product accents (SoT §14.5)
Each swatch paints its label in the foreground the Foundation selects for that accent. LD3's green reaches only 3.77:1 against white, so it resolves to dark text rather than shipping an inaccessible button.
#0B0B0F → #ffffff (19.64:1)
#0B0B0F → #ffffff (19.64:1)
#7C3AED → #ffffff (5.70:1)
#059669 → #111318 (4.93:1)
#1D4ED8 → #ffffff (6.70:1)
#C2521B → #ffffff (4.65:1)
#B91C1C → #ffffff (6.47:1)
Density (SoT §14.11)
compact
topbar 48px · row 36px · field 32px
comfortable
topbar 56px · row 44px · field 36px
spacious
topbar 64px · row 52px · field 40px
Feature states (SoT §13.2)
All nine states, each explaining itself rather than rendering a silent disabled control.
Available
available
Read only
read_only
Locked by plan
locked_by_plan
Missing permission
missing_permission
Missing connection
missing_connection
Coming soon
coming_soon
Disabled by policy
disabled_by_policy
Degraded (retryable)
degraded
Unavailable
unavailable
Scenarios
Light, dark and system themes
Every semantic token rendered in all three modes, including high contrast.
Exercisable §14.3, §14.4, §14.17All five product accents
Each accent with its automatically-selected button foreground. LD3's green resolves to dark text because it only reaches 3.77:1 against white.
Exercisable §14.5 vs §14.17Compact, comfortable and spacious
Topbar, row, field, button and sidebar metrics at each density.
Exercisable §14.11All six breakpoints
Shell layout from xs through 2xl, including the mobile navigation drawer.
Exercisable §16.2, §16.4Keyboard and screen-reader paths
Skip links, focus return, roving sidebar navigation, live-region announcements.
Exercisable §14.17Session lifecycle
Every §12.3 transition, including that a blocked write is never replayed.
Exercisable §12.3, §12.4Enterprise sign-in states and draft recovery
§12.6's twelve states are not twelve ways of saying "sign in again". /states steps through all of them and shows the split that decides whether a person gets help: MFA and step-up the user clears themselves, an IP allowlist or device approval only an administrator can, and a removed membership nobody can — so it offers a company switch rather than a retry, and drops the deep link, because returning someone to a page they no longer have access to repeats the refusal. §12.4's draft rule rides along: work is offered back, never resubmitted, discarded when the account changed, when the company changed, or across a password reset.
Exercisable §12.4, §12.6Company switch
Route carry-over, record-route fallback, unsaved-changes guard and the post-switch invalidation effects.
Exercisable §11.3Account switch
Routed through Auth with select_account — not a company switch.
Exercisable §11.4Locked and suspended products
A locked product never links into the product itself.
Exercisable §11.5All nine feature states
Every §13.2 state rendered, each explaining itself.
Exercisable §13.2, §14.16Navigation customization
Company, role and user layers — including that customization can never reveal an entry the actor lacks permission for.
Exercisable §15.4, §18.7Degraded capability states
Each shared capability reported degraded, with the honest UI that follows.
Exercisable §32.4Metric, and what its colour means
§47.5's Metric existed as stylesheet classes in the design system with no component to use them, so the Lab wrote its own markup against `.ott-widget__metric` — the one §47 component any app had hand-rolled. Promoting it fixed the rule those styles encoded: `[data-direction="up"]` was green and `[data-direction="down"]` red, unconditionally. For churn, error rate, cost per order or open incidents a fall is the win, so that rule paints a genuinely good number red — identically in every product, because the design system decides. Direction and sentiment are now separate inputs, and the dashboard's open-orders widget is the case in point: −4 open orders now reads as good news. The change is also announced in words, since colour and an arrow are the same information twice and neither survives a screen reader.
Exercisable §47.5, §14.17§16.2 breakpoints, honestly applied
§16.2's table already existed in `tokens.ts` — with no consumers. The stylesheets used three hand-picked numbers instead: `1024` and `720` in shell.css, `760` in tokens.css. `max-width: 1024px` includes 1024, which §16.2 puts in `lg`, so at exactly the width a 1024x768 tablet reports in landscape the sidebar became a fixed drawer while the layout still reserved desktop space for it. All three are now the chapter's boundaries, and `__tests__/breakpoints.test.ts` reads the shipped CSS so the constant and the stylesheet cannot drift again. /states lists the ranges and resolves probe widths through the contract.
Exercisable §16.2Container queries for reusable components
§16.2 asks for container queries by name, and the distinction is real: a capability surface can be mounted in a page, a workbench panel, an inspector rail or a widget slot, and one inside a 360px inspector on a 2560px monitor needs the narrow layout that a viewport query would deny it. The shared capability surface is now a container and lays out narrow by its own width. The shell chrome deliberately stays on media queries — `container-type: inline-size` implies containment, which would reposition the fixed drawer and scrim relative to the container instead of the viewport, and a drawer really does overlay the viewport.
Exercisable §16.2Mobile baseline and forced viewport
§16.5 lists what must work on a phone, and `__tests__/mobile-baseline.test.ts` checks each entry has a route — built for the owner role, since a permission-filtered navigation would make the check pass by hiding things. Reachability is the floor rather than the whole requirement; §16.4's "keine versteckten Kernaktionen" is not something a test can settle. The Lab's force-mobile scenario now sets the design system's own `data-ott-viewport` attribute instead of re-implementing the drawer CSS, which it did until this wave — meaning the reference app was demonstrating a layout the framework did not ship.
Exercisable §16.3, §16.4, §16.5ICU messages, plural rules and locale-aware formatting
§17.2's runtime, which was entirely absent. /localization renders every example through the shipped formatter for whichever locale is selected — switch to `ru` and the plural table changes *category*, which is the thing a screenshot of German copy cannot prove. Plural categories come from `Intl.PluralRules` rather than `count === 1`, because Russian is one of §17.1's five locales and has four of them. Money keeps the company's currency in every locale while the locale decides the shape: inferring the currency from the locale gives a number that is correctly formatted and factually wrong.
Exercisable §17.1, §17.2Missing keys as release errors
§17.2 says missing keys are release errors and forbids raw keys in production — one rule from two sides, because a translator that falls back to the key turns a missed translation into a defect nobody notices. `createTranslator` throws under the development policy and, in production, degrades to a humanised label rather than the identifier, reporting every miss. The enforcement is a build-time gate: `__tests__/i18n-catalogues.test.ts` checks all five locales against English, and a gap fails. An extension catalogue reaching outside its namespace is refused rather than silently trimmed, because the catalogue is merged once per locale — one extension defining `actions.save` would change the word on every Save button in the product.
Exercisable §17.2Locale resolution order
§17.3's five steps encoded once instead of reassembled at each call site — the previous helper took an unordered candidate array, which is correct everywhere or wrong in one place with nothing to say which. The ordering that surprises is on the page: an account-wide preference loses to the company default, while an explicit user preference beats it, because those two are different things. `de-AT` resolves to `de` rather than to English, which is what keeps "weitere Locales ohne Rebuild" true.
Exercisable §17.3Telemetry emission, without a transport
§32.1's twelve event names, the safety check and the route templater existed for several waves with nothing calling them. `TelemetrySink` makes them reachable and deliberately ships no transport: a console emitter would let the framework claim telemetry while producing nothing anybody reads. What is worth having before a collector is the policy — `assertTelemetrySafe` enforced at the emit boundary rather than at each call site, batching, flush on page-hide (`visibilitychange`, not `unload`, which does not fire when a mobile browser kills a tab), per-*trace* sampling so a trace is never shredded into fragments, and a failed flush that is counted rather than retried. /observability emits real events through it and can take the collector down.
Exercisable §32.1, §32.2Trace context and telemetry safety
§32.2 lists seven fields every request must carry and the transport was sending three. The four it was missing are the four that turn a report into a diagnosis: which attempt failed, which server span served it, which product asked, and which company context revision it ran against — the last being the one that answers "it worked this morning". /observability shows all seven as headers, and exercises the chapter's closing rule: a route is templated before it is recorded, and an event carrying a customer name or a search term is refused at runtime, because that mistake is invisible until somebody reads the telemetry store.
Exercisable §32.1, §32.2Performance and bundle budgets
§32.3's six timing targets, checked and reported with how far over rather than just that something is slow. A breach is an engineering signal, not an error shown to the customer — they already lived through the slow interaction. Product and extension bundle budgets stay separate because they fail differently: a product over budget is its own team's decision, while an extension over budget slows a page belonging to somebody who never installed it.
Exercisable §32.3Circuit breaker
§28.4's item, and the join §32.4 was built without: the degradation runtime could say what to do when Files is down while nothing said *when* Files is down. The shipped `FoundationClient` now keeps failure accounting per service and refuses a request locally once a breaker is open, so a call to a failing backend fails fast instead of hanging for the whole timeout — doing that on every retry is how one unhealthy service becomes a page that never finishes loading. /observability drives a real client against a stub transport, so what is demonstrated is the integration rather than the reducer, and shows the degradation plan that follows. Two rules decide whether a breaker helps or hurts, and both are exercisable: a 4xx does not count against the service, because otherwise one person repeatedly hitting a permission boundary trips it for everybody; and it trips on a failure *ratio* rather than a streak, because alternating failure and success never produces two in a row and would leave a service erroring on half its requests running forever. Half-open lets exactly one probe through.
Exercisable §28.4Declared and observed health as one answer
The capability manifest carries a `health` the *server* declares; a circuit breaker carries what *this client* has observed. Neither subsumes the other, and two health models would be the worst outcome — a surface reading one while the status bar reads the other. `resolveCapability` takes the worse of the two, which is right in both directions: a network partition or a bad edge node looks perfectly healthy from the server while the person at the screen cannot use it, and a replica returning stale reads succeeds on every request and is still wrong. Observed degradation reaches the capability's `state`, not just a badge — left `available`, a Files surface renders "No files yet" for a backend it simply cannot reach, which is the most misleading thing a data surface can say.
Exercisable §28.4, §32.4Maintenance windows
Existed only as a notification category, which is a different thing: a notification is read later by whoever opens the inbox, and planned downtime has to reach the person about to start a two-hour import now. /observability shows the banner at five moments across one window. Two of them carry the argument — it warns *before* the window opens, because telling somebody the system is unavailable while they are already staring at a failed save is a caption rather than a warning; and it has a word for overrunning that carries no new estimate, because a banner still reading "back by 14:00" at 14:30 was a promise and is visibly false, and a second guess only spends the trust the first one lost.
Exercisable §28.4Per-service degraded mode
§32.4 gives five services five different answers, and /observability drives them from the scenario switcher's own backend setting. AI is hidden because a visibly failing assistant is worse company than an absent one; Files is reduced rather than removed because a vanished file panel reads as deleted files; Notifications falls back to a deliberately slow poll so every open tab does not join the incident; Search hands its job to navigation rather than stranding people; Flows keeps history readable and refuses to start a run whose outcome could not be reported. None of them takes the product down, which is the point of the chapter.
Exercisable §32.4Command palette
Cmd/Ctrl+K across the §20.2 search domains.
Exercisable §20Workbench command execution
The full §21.5 pipeline, including that medium risk and above never executes without an explicit decision.
Exercisable §21.5, §21.7Notification inbox and transport
Grouping, optimistic read with rollback, and the honest SSE→polling fallback.
Exercisable §23White-label publish gates
Contrast, font, asset and injection gates — publish blocked, with reasons.
Exercisable §18.8, §48Dashboard grid
Drag, resize, keyboard move, collision handling and the responsive fallback.
Exercisable §22.3Widget runtime and legacy import
Widget states plus an import that reports what it cannot migrate.
Exercisable §22.2, §22.7Settings composition
Personal settings with effective-value explanation and reset-to-inherited.
Exercisable §24Ottili AI surface
Real SSE streaming, incremental rendering, Stop and Regenerate, tool calls with their real execution and approval states, and citations.
Exercisable §25.1Ottili Files surface
File browser against the real storage service. Upload is hidden, not broken, when the actor cannot write.
Exercisable §25.2Ottili Flows canvas
Canvas, node palette, insertion, selection, edges, zoom and validation — fully keyboard operable. Node types match the real engine.
Exercisable §25.3Help surface
Page-aware help from the route registry, documentation search against the real docs service, shortcut reference, status link and support ID.
Exercisable §25.7Approvals inbox
Approval inbox with the no-self-approval and view-only states (§13.3).
Exercisable §25.4Activity timeline
Read-only history; audit is produced server-side and is never client-mutable.
Exercisable §25.5Task Center
Background work with determinate progress only where the job reports it.
Exercisable §25.6Extension runtime
Manifest validation, compatibility, deterministic activation, conflict rollback, lifecycle and the kill switch. /settings/extensions runs a real activation pass over eight demonstration extensions.
Exercisable §27Sandboxed host and RPC bridge
The host mounts a sandboxed iframe without allow-same-origin and answers postMessage through the permission-checked bridge. /c/{slug}/apps/{extensionId} mounts it and logs every bridge call.
Partly exercisable §27.5No third-party extension origin serves a working iframe here, so the Lab exercises the mount, the load timeout and the failure states — not a successful round trip. The bridge itself is covered by 40 unit tests including forged origins and borrowed extension ids.
Version blocklist
§33.4's ninth control, and not the kill switch under another name: the kill switch stops an extension, the blocklist stops *versions*. /settings/extensions applies Ottili's published list to what is installed — Paused Connector is blocked with a fix, so the remedy is an update and the company's own Enable is refused until it happens; Quarantined Tools is withdrawn with no fix, so the remedy is removal. A third entry is malformed on purpose and the validator drops it before it is applied, because a range that matches nothing leaves a vulnerable version running while the record says it was handled.
Exercisable §33.4Update policy and data retention
§34.1's Console list named both and neither had runtime — and retention was already making a promise, since `describeExtensionState` tells the customer their data is kept until the period ends. /settings/extensions now answers with a date and says what a reinstall would actually get back. The update table shows the two cases where the platform decides instead of the company: a security fix is applied over a pin and says it overrode the policy, and an automatic update stops at a major version rather than breaking a working integration unattended.
Exercisable §34.1Permission-aware download
§33.5 asks downloading separately from previewing, because rendering a document and handing over its bytes are different acts — once the file leaves, nothing Ottili does governs it. Each row in /files resolves access against the file's *owning* root before offering the link, so a file in a nested root the actor cannot see offers no download; a still-processing file offers none either, and an export policy that blocks one is named rather than hinted at.
Exercisable §33.5Version history and rollback
§18.8's flow ends `publish → active → rollback` and the editor could only publish. /settings/branding lists every version with what it changed, and rolling back publishes a *new* version rather than erasing the one being replaced. One fixture version was published under the old rules and is refused today, because a rolled-back document is re-validated against the gates as they stand now.
Exercisable §18.8Panels and customer-safe logs
§21.2's seven core panels plus two Lab ones, with permission gating and arrow-key cycling. The Logs panel is the one that matters: the Lab hands it four lines that must never reach a customer — a private IP, an internal hostname, a stack frame and a connection string — and the panel withholds all four and says how many without naming them.
Exercisable §21.2, §10.5, §33.5Favorites, recents and navigation badges
§19.3/§19.4 in the sidebar: favorites are company-scoped so another company's pinned page is never rendered, the current page can be pinned and unpinned, and recents are pruned, scoped and **re-checked against permission on read** — switch to viewer and the Permissions entry disappears rather than showing as locked. The approvals badge shows nothing at all once the backend is degraded, because a zero would claim nothing is waiting.
Exercisable §19.1, §19.3, §19.4Scoped views, upload safety and preview rules
§25.2's governing rule made real: virtual roots decide every question, a file in a nested root the actor cannot see is not rendered at all, a read-only root is read-only for everyone, an SVG is downloaded rather than previewed (§33), a share may never exceed the sharer's own access, and a filename is validated before it reaches storage. /files mounts three roots covering each case.
Exercisable §25.2Job outcomes, estimates and actions
The §25.6 runtime: `partial` as its own outcome rather than a success or a failure, an estimate refused when it would be guesswork, and cancel/retry gated on the job's own cancellable/retryable rather than on its state word. /jobs covers all six states.
Exercisable §25.6Audit as compliance history
§25.5's distinction made real: `assertServerAudit` refuses anything the client could have produced, a missing sequence is reported rather than filled, correlated rows are grouped into the one action that caused them, and export is requested from the server rather than assembled here. /activity renders it below the activity list.
Exercisable §25.5Chains, SLA, delegation and bulk decisions
The §25.4 runtime: SLA with an at-risk band before the breach, chain position, delegation recording whose authority was used, and a bulk plan that says what will *not* happen before the user confirms. /approvals renders four requests covering decidable, self-raised, step-up and delegated.
Exercisable §25.4Widget data providers
The §22.5 registry with cache, cancellation, retry, partial data, dependency health and telemetry. /dashboard runs four real providers covering complete data, partial data with a named degraded upstream, a hard failure whose internal message never reaches the customer, and a provider nobody registered.
Exercisable §22.5Company-scoped event bus
Validation before dispatch, company scoping at publish, wildcard subscriptions and failure isolation. /realtime publishes through the real FoundationEventBus, including events it must refuse.
Exercisable §31.1, §31.2, §46Missed-event recovery
Revision ledger, gap detection and the resync plan. /realtime drops the stream, publishes while it is down, and shows that reconnecting does not clear `resyncRequired` until revisions have actually been compared.
Exercisable §31.3PWA adapter
Install-prompt capture, periodic background sync, and the §29.6 offline cache policy. The Lab is itself an installable PWA: /manifest.webmanifest and /sw.js are generated from one PwaProductConfig, and /distribution registers that worker for real and reads back what the browser actually cached.
Exercisable §29.3Tauri adapter
Keychain-backed secure storage, scoped file system, deep links, updater and system-browser auth, over a host-injected bridge. /distribution shows it both with and without that bridge.
Partly exercisable §29.4There is no packaged Tauri build, so the bridge is supplied by the Lab rather than by a real host. What is genuinely exercised is the adapter's behaviour in both cases — including reporting every native capability unsupported when the bridge is absent, which is what a browser tab sees.
Electron adapter
The same contract over Chromium/Node, sharing the desktop implementation with Tauri.
Partly exercisable §29.5No packaged Electron build; the host bridge is supplied by the Lab, as for Tauri.