# App-Codex — freshvibe-cms (the behaviour contract)

**Locked 2026-08-04 07:28 BST as part of a452 Phase 3 (App-Codex).**
**Per FWV v8 §22 — the 8 contract items (C1-C8) for codex.md.**
**App-level codex.** Per-module codex.md files are written in Phase 5 (per-module Recipe Books).

The codex is the **behavioural contract** for the freshvibe-cms package. It describes the package's behaviour, state machine, side effects, input validation, failure modes, user simulations, and reproducibility. Per §22.1, the codex is "pure behaviour — no rules, no metadata, no workflow, no DNA".

## C1. Behaviour inventory

The package's behaviours are inventoried in the App-Trace Atlas at `app-trace-atlas/atlas.json` (39 behaviours as of Phase 2). The high-level behaviour groups:

### Group: Chrome toggle
- **Behaviour:** `behaviour.dev-btn.toggle-chrome` — Toggle the editing chrome (on = panels appear, off = panels hide)
  - **Source:** `app-fragments/fvcms-dev-btn/index.js` — the `{ }` button's click handler
  - **Trigger:** `click on #fvcms-dev-btn`
  - **Effect:** Sets `chrome.enabled = !chrome.enabled` in the store; triggers a chrome re-render
  - **State change:** `chrome.enabled` boolean

### Group: Panel lifecycle
- **Behaviour:** `behaviour.panel-manager.mount-panel` — Mount a panel (panel creation)
  - **Source:** `app-fragments/fvcms-panel-manager/index.js` — `mountPanel(id, options)`
  - **Trigger:** `chrome.button.click or programmatic`
  - **Effect:** Creates a new panel DOM node, adds it to the panel manager state, mounts its sub-components
  - **State change:** `panels.<id>` object in panel manager state
- **Behaviour:** `behaviour.panel-manager.dock-to-edge` — Dock a panel to a viewport edge when dragged near one
  - **Source:** `app-fragments/fvcms-panel-manager/index.js` — `dockToEdge(panelId, edge)`
  - **Trigger:** `drag-end within 50px of edge`
  - **Effect:** Sets `panel.docked = edge` and re-renders all panels on the same edge with new positions
  - **State change:** `panels.<id>.docked = 'left' | 'right' | 'top' | 'bottom'`
- **Behaviour:** `behaviour.panel-manager.auto-hide-on-inactivity` — Slim a docked panel to header bar after 20s inactivity (unless pinned)
  - **Source:** `app-fragments/fvcms-panel-manager/index.js` — auto-hide timer
  - **Trigger:** `no mouse/keyboard for 20s on pinned=false panel`
  - **Effect:** Adds `slim` class to panel, removes after hover/focus
  - **State change:** `panels.<id>.slim = true`
- **Behaviour:** `behaviour.panel-manager.remove-panel` — Completely remove a panel (not just hide)
  - **Source:** `app-fragments/fvcms-panel-manager/index.js` — `removePanel(panelId)`
  - **Trigger:** `click on panel × button`
  - **Effect:** Removes panel DOM node, removes from panel manager state
  - **State change:** `panels.<id>` deleted

### Group: Selection
- **Behaviour:** `behaviour.selection.set-selected` — Set the currently selected region/block
  - **Source:** `app-fragments/editor-selection/index.js` — `setSelected(elementId, type)`
  - **Trigger:** `click on [data-fv-region] or [data-fv-block]`
  - **Effect:** Sets `selection.current` to the new element, clears previous selection styling
  - **State change:** `selection.current = { id, type }`
- **Behaviour:** `behaviour.selection.clear-selected` — Clear the current selection
  - **Source:** `app-fragments/editor-selection/index.js` — `clearSelected()`
  - **Trigger:** `keydown.escape or click on body`
  - **Effect:** Sets `selection.current = null`, removes selection styling
  - **State change:** `selection.current = null`

### Group: Region manipulation
- **Behaviour:** `behaviour.region.apply-property` — Apply property change to a region
  - **Source:** `app-fragments/editor-region-actions/index.js` — `applyProperty(regionId, key, value)`
  - **Trigger:** `change on region-actions input`
  - **Effect:** Mutates the region's DOM attributes (style, class, data-attr)
  - **State change:** `regions.<id>.<property>` updated
- **Behaviour:** `behaviour.layout.apply-template` — Apply selected layout template to a new region
  - **Source:** `app-fragments/editor-layout-templates/index.js` — `applyTemplate(templateId, regionId)`
  - **Trigger:** `click on layout-template`
  - **Effect:** Inserts the template's HTML structure into the new region
  - **State change:** `regions.<id>.template = templateId`

### Group: Block operations
- **Behaviour:** `behaviour.palette.drag-block-to-region` — Drag block from palette to a region to add it
  - **Source:** `app-fragments/editor-palette/index.js` — drop handler
  - **Trigger:** `drop on [data-fv-region]`
  - **Effect:** Creates a new block instance in the region, mounts its renderer
  - **State change:** `regions.<id>.blocks` array gains new block
- **Behaviour:** `behaviour.block.<type>.render` — Render a specific block type to the page
  - **Source:** `modules/<type>/index.js` — `render(props, slots)`
  - **Trigger:** `region.add-block: <type>`
  - **Effect:** Inserts block HTML, mounts any sub-handlers (e.g. carousel's next/prev)
  - **State change:** `blocks.<blockId>.mounted = true`

### Group: Content admin (records CRUD)
- **Behaviour:** `behaviour.content-admin.list-records` — List all records of a content type
  - **Source:** `app-fragments/editor-content-admin/index.js` + `runtime/content-items/store.js`
  - **Trigger:** `click on content-type in content-admin`
  - **Effect:** Queries the per-site content store, renders the list
  - **State change:** `contentAdmin.<typeId>.records` array updated
- **Behaviour:** `behaviour.content-admin.create-record` — Create a new record
  - **Source:** `app-fragments/editor-content-admin/index.js` + `runtime/content-items/store.js`
  - **Trigger:** `click on '+ New record'`
  - **Effect:** Creates a new record in the store, adds it to the list
  - **State change:** `contentAdmin.<typeId>.records` array gains new record
- **Behaviour:** `behaviour.content-admin.edit-record` — Edit an existing record
  - **Source:** `app-fragments/editor-content-admin/index.js` + `runtime/content-items/store.js`
  - **Trigger:** `click on record row`
  - **Effect:** Opens the record editor, saves on change
  - **State change:** `contentAdmin.<typeId>.records.<recordId>.fields` updated
- **Behaviour:** `behaviour.content-admin.delete-record` — Delete a record (with confirmation)
  - **Source:** `app-fragments/editor-content-admin/index.js` + `runtime/content-items/store.js`
  - **Trigger:** `click on 'Delete'`
  - **Effect:** Removes the record from the store
  - **State change:** `contentAdmin.<typeId>.records.<recordId>` deleted

## C2. State machine

The freshvibe-cms package has **5 main state slices** in its singleton store:

### State slice: `chrome`
- `chrome.enabled: boolean` — is the editing chrome currently active?
- `chrome.activeTab: string` — which tab in the dev panel is open (Overview, Structure, Inspector, Content Admin)

### State slice: `panels`
- `panels.<id>.id: string` — panel identifier
- `panels.<id>.type: string` — panel type (editor-palette, fvcms-cms-panel, etc.)
- `panels.<id>.docked: string | null` — which edge the panel is docked to
- `panels.<id>.slim: boolean` — is the panel in slim (header-only) mode?
- `panels.<id>.pinned: boolean` — is the panel pinned (no auto-hide)?

### State slice: `selection`
- `selection.current: { id: string, type: 'region' | 'block' } | null` — currently selected element
- `selection.hovered: { id: string, type: 'region' | 'block' } | null` — currently hovered element

### State slice: `regions`
- `regions.<id>.id: string` — region identifier
- `regions.<id>.template: string` — which template this region was created from
- `regions.<id>.properties: { background, gradient, parallax, columns }` — region properties
- `regions.<id>.blocks: string[]` — block IDs in this region (ordered)

### State slice: `blocks`
- `blocks.<id>.id: string` — block identifier
- `blocks.<id>.type: string` — block type (heading, button, image, etc.)
- `blocks.<id>.props: { ... }` — block-specific props
- `blocks.<id>.slots: { ... }` — slot contents for blocks that accept children

### State slice: `contentAdmin`
- `contentAdmin.<typeId>.records: Array<{ id, fields }>` — records of this content type
- `contentAdmin.<typeId>.selected: string | null` — currently selected record ID

### Transitions
- `chrome.enabled = false` → `chrome.enabled = true` on `dev-btn.click` (or any panel launcher click) — sets panels.mount on the first panel
- `chrome.enabled = true` → `chrome.enabled = false` on `dev-btn.click` again — unmounts all panels
- `selection.current = null` → `selection.current = { id, type }` on `click on [data-fv-region] or [data-fv-block]`
- `selection.current = { id, type }` → `selection.current = null` on `keydown.escape or click on body`
- `panels[id].slim = false` → `panels[id].slim = true` on `no input for 20s` AND `panels[id].pinned = false`
- `panels[id].slim = true` → `panels[id].slim = false` on `mouseenter or focusin on panel`
- `regions[id].blocks` grows on `drop on [data-fv-region]` (palette drag)
- `contentAdmin[typeId].records` grows on `click on '+ New record'`
- `contentAdmin[typeId].records.<id>` deleted on `click on 'Delete'`

### Initial state
- `chrome.enabled = false` (chrome off by default)
- `selection.current = null`
- `panels = {}` (no panels mounted)
- `regions = {}` (regions come from the host page's `annotation.json`)
- `blocks = {}` (blocks come from region.add-block actions)
- `contentAdmin = {}` (empty until operator opens a content type)

## C3. Side effects

| Side effect | When | Behaviour | Payload |
|---|---|---|---|
| localStorage write `chrome.enabled` | `chrome.enabled` changes | `behaviour.dev-btn.toggle-chrome` | `boolean` |
| DOM mutation: add `slim` class to panel | `panels[id].slim = true` | `behaviour.panel-manager.auto-hide-on-inactivity` | `panelId` |
| DOM mutation: add `selected` class to element | `selection.current = { id, type }` | `behaviour.selection.set-selected` | `id, type` |
| DOM mutation: mutate region attributes | `region.<property> changes` | `behaviour.region.apply-property` | `regionId, property, value` |
| DOM mutation: insert block HTML | `region.add-block: <type>` | `behaviour.block.<type>.render` | `regionId, type, props` |
| localStorage write `contentAdmin.<typeId>.records` | record CRUD | `behaviour.content-admin.{list,create,edit,delete}-record` | `typeId, records` |
| event emission: `fvcms:chrome-toggled` | `chrome.enabled` changes | `behaviour.dev-btn.toggle-chrome` | `boolean` |
| event emission: `fvcms:panel-mounted` | `panels[id]` created | `behaviour.panel-manager.mount-panel` | `panelId, type` |
| event emission: `fvcms:selection-changed` | `selection.current` changes | `behaviour.selection.set-selected` | `{ id, type }` |
| network call: per-site content store | record CRUD | `behaviour.content-admin.*-record` | `typeId, record` |

## C4. Input validation

| Input | Valid range | Default | On invalid |
|---|---|---|---|
| `chrome.activeTab` | one of `['overview', 'structure', 'inspector', 'content-admin']` | `'overview'` | Reject + log warn |
| `selection.current.type` | one of `['region', 'block']` | none | Reject + show error |
| `region.properties.background` | CSS color string | `'transparent'` | Fall back to `'transparent'` |
| `region.properties.columns` | integer 1-12 | `1` | Fall back to `1` |
| `block.props.text` | non-empty string | `''` | Show error in inspector |
| `block.props.url` | valid URL | none | Show error in inspector |
| `contentAdmin.<typeId>.records` | array of `{ id, fields }` | `[]` | Reject non-array |
| Panel `docked` value | one of `['left', 'right', 'top', 'bottom', null]` | `null` | Fall back to `null` |
| `fvcms:chrome-toggled` event payload | boolean | `true` | Reject + log warn |
| `fvcms:panel-mounted` event payload | `{ panelId: string, type: string }` | none | Reject + log warn |

## C5. Failure modes

| Failure | Detection | Recovery |
|---|---|---|
| `annotation.json` missing on host page | chrome.mount sees no regions | Log error + show empty-state UI |
| Content type schema not found | content-admin.open(typeId) | Show "type not found" in UI |
| localStorage quota exceeded | `setItem` throws | Show "storage full" toast, suggest export |
| Block renderer throws on bad props | `render(props)` throws | Catch + show error placeholder in block + log |
| Panel mount fails (DOM not ready) | `mountPanel` sees no container | Retry on `DOMContentLoaded` |
| Drift between Atlas and code | `scripts/drift-detector.py --full` | Show drift report, mark affected panels stale |
| chrome.js fails to load | bootstrap error | Show fallback error page, link to docs |
| Multiple chrome instances mounted | `chrome.count > 1` | Unmount all but first |
| Operator deletes a block currently selected | block removed from DOM | Clear selection |
| Operator deletes a region with blocks | region removed | Cascade-delete blocks, clear selection |

## C6. User simulation list

The following user flows are guaranteed to work (each is implemented as a Playwright test in Phase 7):

1. **Operator opens chrome** — click { } → chrome appears with default tab Overview
2. **Operator adds a region** — click "+ Add region" → layout picker appears → click a template → region added to page
3. **Operator adds a block** — drag a block from the palette to a region → block appears
4. **Operator edits a block** — click block → inspector opens with fields → change text → save → page updates
5. **Operator pins a panel** — drag a panel to the right edge → panel docks to right edge → panel is pinned (no auto-hide)
6. **Operator opens content admin** — click {📋} → content admin panel opens → click a content type → records list shows
7. **Operator creates a record** — click "+ New record" → form opens → fill fields → save → record appears in list
8. **Operator selects an element** — click a region → selection styling applied → inspector shows region fields
9. **Operator clears selection** — press Escape → selection cleared
10. **Operator closes chrome** — click { } again → chrome disappears
11. **Operator reloads the page** — chrome state restored from localStorage, panels unmounted but ready
12. **Operator drags a panel off the edge** — drag panel away from edge → panel becomes floating, not docked
13. **Operator opens the dev panel from the breadcrumb** — click a breadcrumb crumb → that element is selected, inspector updates
14. **Operator right-clicks a block** — context menu appears with delete/duplicate/move options
15. **Operator saves a content type for reuse** — open editor-save-for-reuse panel → review the auto-detected types

## C7. Reproducibility test

The freshvibe-cms package is **deterministic** in the following sense:

1. **Same `annotation.json` + same content → same rendered HTML.** No randomness, no timestamp-based IDs, no non-deterministic ordering.
2. **Same operator actions in same order → same final state.** The state machine is pure: same input + same state → same output state.
3. **Same code version → same behaviour.** No external API calls except the optional per-site content store (which is the consumer's choice, not the package's).
4. **The Atlas is a deterministic mapping** of the code. Same code → same Atlas (modulo test files which may be added over time).

**How to verify reproducibility**:
1. Build the package: `npm run build`
2. Capture a snapshot of the rendered HTML for a known `annotation.json` (e.g. `demo/annotation.json`)
3. Rebuild
4. Diff snapshots — should be byte-identical (or differ only in the timestamp/UUID fields which are deterministic per build)

## C8. Shape matrix (per §30.1)

The shape matrix records, for every (surface, mode) pair, what the surface looks like. The package has 4 modes:

- **mode=browse** — operator is reading the page, no editing
- **mode=edit** — chrome is on, panels are visible, regions are clickable
- **mode=preview** — chrome is hidden but the layout is the same as browse
- **mode=inert** — chrome is on but all interactive elements are disabled (e.g. for AI inspection)

| Surface | browse | edit | preview | inert |
|---|---|---|---|---|
| page content (regions, blocks) | visible, clickable in edit | visible, clickable | visible, not clickable | visible, not clickable |
| outline tags | hidden | visible on hover | hidden | hidden (always) |
| inspector panel | hidden | visible when something selected | hidden | hidden |
| palette panel | hidden | visible (operator mounts it) | hidden | hidden |
| content admin | hidden | visible (operator mounts it) | hidden | hidden |
| breadcrumb | hidden | visible at bottom of viewport | hidden | hidden |
| context menu | never visible | visible on right-click | never visible | never visible |
| dev button ({ }) | always visible, clickable | always visible, clickable | always visible, clickable | visible, but click does nothing |
| selection styling | none | yellow outline on selected element | none | none |

## End of App-Codex

The per-module codex.md files (one per panel + one per block) are written in Phase 5 (per-module Recipe Books, 4-6h, the biggest phase). They follow the same C1-C8 structure but are scoped to a single module.

Continue with `app-recipe/recipe.md` (Phase 3 follow-up) or skip to Phase 4 (App-Pact complete).
