Surfaces inventory
The .ai/surfaces.yaml file — the moving parts of the monorepo that every openspec proposal must declare.
What is a surface?
A surface is a named area of the system where changes happen, used as a checklist at proposal time so you don't forget any moving parts. It's not a package, not a file path, not a spec — it sits one level up from all of those. Think of it as a stable label for a concern that can span code, config, contracts, tests, and docs.
A surface usually has these properties:
- Stable name —
wp-rest,gateway-db,contracts— the name doesn't change when you refactor the directory tree. Once anidis published, it's referenced from every proposal that has touched the surface; renaming would break historical traceability. - Cross-cutting OK — one surface can span multiple packages, and one package can host multiple surfaces.
apps/wp-plugin-itu-headless/will eventually host four surfaces (wp-rest,wp-lexical,wp-hooks,wp-media) as it grows. - Bigger than a file, smaller than a project — granular enough that "I'm changing X" is unambiguous, broad enough that you don't list 50 surfaces per change. The inventory is intentionally short.
- Optional path mapping — each entry in
.ai/surfaces.yamlmay declare apathsglob (e.g.apps/docs/**) that the archive step uses to verify a change actually touched what it claimed. Surfaces whose code doesn't exist yet havepaths: null. - Optionally tied to a spec — most surfaces have a
related_specpointing at the openspec capability spec that owns the behaviour in that area. The surface is the where; the spec is the what.
Surface vs. spec vs. package
These three concepts are distinct and easy to conflate. The mental model:
| Concept | Answers | Example |
|---|---|---|
| **Package** | Where does the code physically live? | apps/wp-plugin-itu-headless/ |
| **Spec** | What should this capability *do*? | openspec/specs/wordpress-plugins/spec.md |
| **Surface** | Which moving part of the system does my change *touch*? | wp-rest, wp-hooks |
A change can touch multiple surfaces (most do). A surface can be touched by many changes over time. Packages and specs are stable artifacts on disk; surfaces are conceptual labels in the inventory.
Why surfaces exist
The inventory exists to guarantee proposals don't silently omit areas they actually modify. The primary failure mode it prevents is forgetting a moving part at proposal time — the kind of omission that ships through code review and only surfaces (pun intended) two weeks later when something downstream silently misbehaves.
A concrete example: imagine a proposal "Add a new summary field to WordPress posts that the Gateway needs to display in the publication list". Without a surface checklist, the obvious change is the WP plugin's REST route — add the field, update the route, ship. Two weeks later the team realises the Gateway never deserialises the field because the Zod contract doesn't know about it, the contract test still passes a lie, and the docs site's API reference is stale.
The same proposal with a surface checklist forces the author to enumerate at scoping time:
wp-rest— the WP plugin route that emits the fieldcontracts— the shared Zod schema that both sides validate againsttest-contract— the contract test that pins the schemadocs-site— the API reference page that documents the field
Now tasks.md has a task per surface and the change can't merge without touching all four. The inventory turned a class of subtle omission bugs into a 30-second checklist exercise at proposal time.
At archive time, the paths globs do a complementary check: if a proposal declared wp-rest but the diff contains no files matching apps/wp-plugin-*/**, the archive step warns you. The inventory doubles as a lightweight post-merge sanity check.
The canonical list lives at .ai/surfaces.yaml. This page is a human-readable mirror — if it drifts from the YAML, the YAML wins. Update the YAML, not the docs page.
The full inventory
Each row below is one surface. related_spec points at an openspec capability spec under openspec/specs/ when one owns the surface; — means no spec yet.
Architecture and decisions
| id | Name | Description | Related spec |
|---|---|---|---|
concepts-docs | Architecture concept docs | Current-state architecture docs under concepts/ | — |
adr | Architecture Decision Record | Append-only architectural decision records | — |
Gateway
| id | Name | Description | Related spec |
|---|---|---|---|
gateway-routes | Gateway API routes | Hono route handlers exposed by the Gateway service | gateway-app |
gateway-db | Gateway DB schema | Postgres schema and migrations backing the Gateway | gateway-app |
Contracts
| id | Name | Description | Related spec |
|---|---|---|---|
contracts | Shared Zod contracts | Zod schemas shared between Gateway, WP plugin, and frontend | — |
WordPress headless plugin
| id | Name | Description | Related spec |
|---|---|---|---|
wp-rest | WP plugin REST routes | Custom REST endpoints in the WordPress headless plugin | wordpress-plugins |
wp-lexical | WP Lexical editor | Custom Lexical blocks and editor integration in the WP plugin | wordpress-plugins |
wp-hooks | WP plugin hooks (AI translation, SEO, indexing) | Custom hooks firing on save to push content to the Gateway | wordpress-plugins |
wp-media | WP plugin custom media manager | Media upload integration with Gateway image optimisation | wordpress-plugins |
Frontend and docs
| id | Name | Description | Related spec |
|---|---|---|---|
frontend-component | Frontend component (packages/ui, packages/docs-ui) | Shared React components in the monorepo | ui-components |
frontend-page | Next.js page / route | A page or route in the Next.js frontend app | nextjs-app |
docs-site | Docs site content (apps/docs) | MDX content and layout in the documentation site | docs-app |
Tests
| id | Name | Description | Related spec |
|---|---|---|---|
test-contract | Contract tests | Tests verifying Zod schemas between Gateway and sources | — |
test-e2e | End-to-end tests | E2E tests exercising the full request path | — |
AI tooling
| id | Name | Description | Related spec |
|---|---|---|---|
ai-tooling | AI tooling skills and config | .ai/ sources, openspec config, provider outputs under .claude/, .cursor/, .codex/ | ai-tool-registry |
Adding a new surface
When a proposal introduces a genuinely new moving part — a new app, a new package, a new category of tests — add it to .ai/surfaces.yaml as part of that proposal. Each entry has five fields:
| Field | Required | Purpose |
|---|---|---|
id | yes | Kebab-case identifier referenced from proposals. Must be unique and stable. |
name | yes | Short human-readable label for UIs and docs. |
description | yes | One-sentence explanation of what the surface covers. |
paths | no | Array of glob patterns for archive-time path verification. Use null when the target directory does not yet exist. |
related_spec | no | Name of the openspec capability spec that owns the surface. Use null when no spec owns it. |
The YAML shape of a single entry:
surfaces: - id: gateway-routes name: Gateway API routes description: Hono route handlers exposed by the Gateway service paths: null # set once apps/gateway exists related_spec: gateway-app
A few rules of thumb:
- Prefer adding to an existing surface over inventing a new one. Surfaces are coarse-grained on purpose.
- Use
nullfor paths when the surface's directory does not exist yet. When the scaffolding lands, the same proposal should fill in the globs. - Point
related_specat an openspec capability spec if one exists. If the surface is purely organisational (e.g.concepts-docs,adr), leave itnull. - Never rename an
id. Ids are referenced from every proposal that has touched the surface — renaming breaks historical traceability. Deprecate and replace instead.
Using surfaces in a proposal
Every openspec proposal must include a ## Surfaces affected section listing the ids it touches, one per line:
## Surfaces affected - `gateway-routes` - `contracts` - `wp-rest` - `test-contract`
The opsx:propose skill checks that each id resolves against .ai/surfaces.yaml. At archive time, opsx:archive uses the paths globs to verify the expected files exist. Forgetting a surface here is the single most common mistake the inventory is designed to catch — when in doubt, enumerate more rather than fewer.