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 namewp-rest, gateway-db, contracts — the name doesn't change when you refactor the directory tree. Once an id is 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.yaml may declare a paths glob (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 have paths: null.
  • Optionally tied to a spec — most surfaces have a related_spec pointing 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:

ConceptAnswersExample
**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 field
  • contracts — the shared Zod schema that both sides validate against
  • test-contract — the contract test that pins the schema
  • docs-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.

Source of truth

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

idNameDescriptionRelated spec
concepts-docsArchitecture concept docsCurrent-state architecture docs under concepts/
adrArchitecture Decision RecordAppend-only architectural decision records

Gateway

idNameDescriptionRelated spec
gateway-routesGateway API routesHono route handlers exposed by the Gateway servicegateway-app
gateway-dbGateway DB schemaPostgres schema and migrations backing the Gatewaygateway-app

Contracts

idNameDescriptionRelated spec
contractsShared Zod contractsZod schemas shared between Gateway, WP plugin, and frontend

WordPress headless plugin

idNameDescriptionRelated spec
wp-restWP plugin REST routesCustom REST endpoints in the WordPress headless pluginwordpress-plugins
wp-lexicalWP Lexical editorCustom Lexical blocks and editor integration in the WP pluginwordpress-plugins
wp-hooksWP plugin hooks (AI translation, SEO, indexing)Custom hooks firing on save to push content to the Gatewaywordpress-plugins
wp-mediaWP plugin custom media managerMedia upload integration with Gateway image optimisationwordpress-plugins

Frontend and docs

idNameDescriptionRelated spec
frontend-componentFrontend component (packages/ui, packages/docs-ui)Shared React components in the monorepoui-components
frontend-pageNext.js page / routeA page or route in the Next.js frontend appnextjs-app
docs-siteDocs site content (apps/docs)MDX content and layout in the documentation sitedocs-app

Tests

idNameDescriptionRelated spec
test-contractContract testsTests verifying Zod schemas between Gateway and sources
test-e2eEnd-to-end testsE2E tests exercising the full request path

AI tooling

idNameDescriptionRelated spec
ai-toolingAI 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:

FieldRequiredPurpose
idyesKebab-case identifier referenced from proposals. Must be unique and stable.
nameyesShort human-readable label for UIs and docs.
descriptionyesOne-sentence explanation of what the surface covers.
pathsnoArray of glob patterns for archive-time path verification. Use null when the target directory does not yet exist.
related_specnoName of the openspec capability spec that owns the surface. Use null when no spec owns it.

The YAML shape of a single entry:

yaml
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
6 lines of yaml code

A few rules of thumb:

  • Prefer adding to an existing surface over inventing a new one. Surfaces are coarse-grained on purpose.
  • Use null for paths when the surface's directory does not exist yet. When the scaffolding lands, the same proposal should fill in the globs.
  • Point related_spec at an openspec capability spec if one exists. If the surface is purely organisational (e.g. concepts-docs, adr), leave it null.
  • 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:

markdown
## Surfaces affected
 
- `gateway-routes`
- `contracts`
- `wp-rest`
- `test-contract`
6 lines of markdown code

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.