---
id: fvcms.pact.anti-drift.001
freshvibe_way_version: v8
date: 2026-08-04
last_rewritten: 2026-08-04 (Phase 4 of plan a452, FWV v8.3 alignment)
---

# FreshVibe CMS — Anti-Drift Rules

Anti-drift prevents unintended changes during refactoring. It ensures freshvibe-cms evolves without breaking existing functionality or accumulating technical debt.

## The 7 Refactoring Rules

R1 Verification-before-refactor: freeze the shadow before any change. Why: without a baseline, "did the refactor break anything?" is unanswerable.
R2 Safe Diff Protocol: one small change at a time. Why: large diffs are undebuggable; small diffs are reversible.
R3 No invention: don't add new things during a refactor. Why: a refactor reorganises; adding features during a refactor doubles the risk.
R4 Explicit binary verification: every step has a pass/fail test. Why: "looks good" is not a verification.
R5 DOM hierarchy verification: extracted DOM matches modularised DOM. Why: the page should look the same after a refactor.
R6 Behaviour/state verification: extracted behaviour matches modularised behaviour. Why: clicks, edits, and saves should work the same.
R7 Drift detection: continuous comparison between modularised and original. Why: catch drift early, not at the end.

## The 3-phase Drift Discovery Protocol (a499 Finding 3, added in v14)

The 7 refactoring rules (R1-R7) and 20 hard gates (G-01 to G-20) above
define what drift IS. This section defines how we FIND drift. The
discovery loop runs on three cadences:

### Phase 1: DETECT (every page load)

At page load, the runtime runs a fast in-browser validator. It checks
5 invariants (the same I1-I5 from `invariants.md`):

- I1: `dna/dna.json` exists and parses
- I2: DNA version matches the consuming app's `package.json` version
- I3: 9-folder layout matches canonical (assets, components, controllers,
  migrations, templates, tests, hooks, locales, config)
- I4: `codex.md` events match implementation emits (sample 5 events
  per page load)
- I5: `recipe.md` ingredients all exist as files

Failures are logged to console + a per-page `drift-detected` event
fires. The chrome shows a yellow "drift detected" badge if any
invariant fails.

### Phase 2: DISCOVER (weekly + on push)

A weekly cron + every push to main runs `fvcms discover`:

```bash
fvcms discover --source=/path/to/freshvibe-cms \
               --consumers=/path/to/consumer/apps \
               --vendored=/path/to/vendored/copies
```

The discoverer walks 3 trees:

1. **Source** — the canonical freshvibe-cms checkout
2. **Consumers** — apps that vendor freshvibe-cms (e.g. Oscar's Tree
   Academy, VibeCoder standalone, freshcloud-mail)
3. **Vendored** — copies of freshvibe-cms dropped into other repos
   (the `fv-vendored-marker` comment in `bootstrap.js` is the signal)

For each consumer + vendored copy, it diffs against source and
classifies the diff as:
- **intentional** (in `dna.allowed_drift`) → ignore
- **drift** (semantic divergence: missing files, mismatched versions,
  changed APIs) → flag for review
- **patch** (bug fix in consumer that should be PR'd back to source) →
  create a "consider backporting" PR

Output: `app-recipe/snapshot/drift-discovery-{date}.md` with the
classification + recommended actions.

### Phase 3: REPAIR

The repair step has 3 sub-cases:

- **Auto-repair** (simple): re-run the canonical build, re-vendor to
  consumers, bump the `fv-vendored-marker` version comment. Safe
  because no semantic change.
- **Operator-approved repair** (medium): drift is a real divergence
  (e.g. consumer added a custom panel). Show the diff, let the
  operator approve merging the consumer's change back to source (or
  accept the divergence and add it to `dna.allowed_drift`).
- **Fork-required repair** (complex): drift is incompatible with
  the source's design. Fork the consumer off the canonical version,
  or revert the consumer to a compatible version. The DAO
  (constitution §11) may need to vote.

The 12 failure modes (F-01 to F-12) below describe specific
runtime errors and their recovery actions; they are the
"reactive" drift response, while this 3-phase protocol is the
"proactive" drift response.

## The 20 Hard Gates

G-01: Every panel has a removePanel handler
G-02: Every behaviour traces to a test in the Atlas
G-03: No commented-out code in main branch
G-04: chrome.js never uses framework-specific selectors in Layer A
G-05: Every block has a recipe.md
G-06: Every module.json declares a tier (1, 2, or 3 per §17)
G-07: The Constitution (app-pact.md) is the only file that defines the system
G-08: Every store mutation goes through assertOp(scope, op)
G-09: No localStorage writes outside the store
G-10: Every panel mounts via the panel manager (not direct DOM)
G-11: Every block renderer is pure (no side effects on import)
G-12: The Atlas is the source of truth for surface-behaviour-module-file-test chains
G-13: Every test maps to a behaviour in the Atlas
G-14: shadow/ directory is read-only (never edited in place)
G-15: Every commit message starts with the phase prefix (phase-1.1, phase-2, etc)
G-16: The vendored FWV v8.3 is never edited in place (use a worktree)
G-17: Every operator-facing string is operator-friendly (no jargon walls)
G-18: Every public API has at least one Playwright test
G-19: App-DNA and App-Atlas have the same app_id
G-20: The phase plan (a452) is the contract — no scope drift without operator signoff

## The 12 Failure Modes

F-01 chrome.js fails to load (CDN 404, MIME type wrong, syntax error). Detection: chrome.mount error handler fires. Recovery: show fallback error page, link to docs.
F-02 annotation.json missing on host page. Detection: chrome.mount sees no regions. Recovery: log error + show empty-state UI with "Add region" prompt.
F-03 Multiple chrome instances mounted. Detection: chrome.count > 1. Recovery: unmount all but first.
F-04 Block renderer throws on bad props. Detection: render(props) throws. Recovery: catch + show error placeholder in block + log.
F-05 Panel mount fails (DOM not ready). Detection: mountPanel sees no container. Recovery: retry on DOMContentLoaded.
F-06 Drift between Atlas and code. Detection: scripts/drift-detector.py --full. Recovery: show drift report, mark affected panels stale.
F-07 localStorage quota exceeded. Detection: setItem throws. Recovery: show "storage full" toast, suggest export.
F-08 Content type schema not found. Detection: content-admin.open(typeId). Recovery: show "type not found" in UI.
F-09 Operator deletes a block currently selected. Detection: block removed from DOM. Recovery: clear selection.
F-10 Operator deletes a region with blocks. Detection: region removed. Recovery: cascade-delete blocks, clear selection.
F-11 Chrome toggle race condition. Detection: rapid clicks cause state desync. Recovery: debounce toggle by 200ms.
F-12 Module JSON not parseable. Detection: JSON.parse error. Recovery: log + skip module + show warning in dev panel.

## Drift Log

Drift is logged in app-recipe/snapshot/drift-log.md (created in Phase 7). Each entry: {date, surface, drift_type, severity, fix}.

## End of Anti-Drift Rules. See also: app-pact/invariants.md (testable), app-pact/app-pact.md (Constitution), app-pact/shadow/v0.8.0-pre-fwv-v8-alignment/ (rollback target).