# CMS Module Doctrine

The rules every file in this module must obey. Per FWV v8 §06 (file < 200 lines) and §10 (module as cluster).

## §1. File-size discipline

Every `.js`, `.html`, `.css`, `.md` file in `cms/` **MUST be < 200 lines**.

If a file approaches 200 lines, split at a natural seam:
- Init / state / render / event handlers
- Helpers → `_helpers.js`
- Per-content-type code → dedicated file

## §2. Storage adapter boundary

**No raw `localStorage.*` calls anywhere except `storage/local-storage-adapter.js`.**

All other modules use `getStorageAdapter()` from `storage/adapter.js`:
```js
const adapter = getStorageAdapter();
await adapter.get(key);
await adapter.set(key, value);
```

Tests for non-adapter code run with a `MemoryStorageAdapter` (in-memory map).

## §3. Schema-as-source-of-truth

A content type is a `Schema` instance. The schema drives:
- Editor form rendering
- Validation (delegates to `validator.js`)
- AI prompt text generation
- CSV bulk import header mapping
- App-side render filtering

**Adding a content type = one file in `schemas/`.**

## §4. Write gate

All CMS writes go through `cmsUpsert()` / `cmsDelete()`. Direct adapter writes for content are forbidden.

The store is the only entry point that runs validation + sets author/timestamps + logs activity.

## §5. Activity log mandatory

Every `cmsUpsert` / `cmsDelete` / `login` / `logout` / `setup` / `password_reset` / `import` / `export` calls `logActivity()`.

No exceptions. Audit trail is non-optional.

## §6. Sanitization at DOM boundary

Every user-supplied string that touches DOM is escaped first via `escapeHtml()`.

Rich content (markdown field type) goes through `sanitizeHtml()` with allowlist.

CSP headers set in `admin.html`.

## §7. URL allowlist

For `url` and `image` field types, URLs are validated:
- Protocols: `http:` or `https:` only
- Hosts: no localhost, no private IPs (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16), no `.local`

Defense in depth: server-side re-validates at apply time (v2.0).

## §8. Auth lifecycle

PBKDF2-SHA256 (310k iterations, 16-byte salt). Session in sessionStorage (7-day expiry). 5-attempt lockout (5-min cooldown). 12-word recovery phrase.

Constant-time comparison for all hash checks.

Dev-mode bypass requires explicit opt-in (`{prefix}:admin:devMode === 'true'`).

## §9. File naming

- `*.js` — implementation
- `_*helpers.js` — shared helpers (prefix underscore)
- `*.html` — views (login, setup, dashboard)
- `*.css` — styles
- `*.md` — docs (doctrine, recipe, codex, README)
- `module.json` — manifest (single, at root)

## §10. Public API

Consumers import only from `index.js`. Internal layout can change without breaking callers.

```js
import { Schema, validate, getStorageAdapter } from './index.js';
```

## §11. Pressure-test fixes baked in

The Track-0-relevant fixes are:

| Fix | Where |
|---|---|
| **A.L1** — `fv-content-id` is full SHA-1 (40 hex chars), not 16 | Schema class generates IDs |
| **A.L2** — `fv-module` confidence scoring | Schema class accepts `confidence: 0..1` |
| **A.L4** — Stable identity across rebuilds | Schema class supports `identityKey` for persistence |

Other pressure-test fixes deferred to Track 5 (hardening).

## §12. Schema DSL constraints

Field types form a **closed enum**:
`string`, `textarea`, `markdown`, `url`, `integer`, `number`, `boolean`, `array`, `enum`, `references`, `image`, `datetime`, `color`.

Unknown types are rejected at Schema construction.

## §13. Reserved meta fields

`createdAt`, `updatedAt`, `createdBy`, `updatedBy` are reserved. The store sets them. Validators skip them.

## §14. No silent failures

Every operation returns success or throws. No swallowed errors.

`Result = { ok: true, value } | { ok: false, errors[], warnings[] }` for expected failure modes (validation).

`throw new Error(...)` for unexpected failure modes (storage failure, crypto failure).

## §15. Test coverage

| Component | Target |
|---|---|
| Schema class | 100% line coverage |
| Validator | 100% line coverage, all 13 field types |
| Storage adapter | round-trip + edge cases |

Enforced by `tests/` directory + `node --test`.

— cloud Mavis, 2026-07-02