# Gallery Entry Shapes — v4 (Canonical Home is Never Default + AI Asks 4 Questions) > **Status**: LOCKED (final v4 for operator review, 2026-07-22) > **Author**: Mavis > **Replaces**: v1, v2, v3 — all had errors > **Key change in v4**: **The gallery is NEVER the canonical home by default.** Canonical home is wherever the operator decides the work happens. The AI must ask, never assume. v3's "new → gallery canonical" was wrong — a new module's canonical home is wherever it's first written, which the operator chooses. --- ## TL;DR **The gallery is a publisher, not an author.** The canonical home of any module is wherever the operator says the work happens. The gallery is one possible home (for `gallery-original` modules), but it's a specific choice, not a default. **When told "publish to the gallery", the AI asks 4 questions:** 1. **New or revibe?** 2. **Where's the canonical home?** (which repo, which folder) 3. **Publish what?** (recipes only, or recipes + code) 4. **Fork of anything?** (if yes, point at the original) The AI never assumes the gallery is canonical. The operator decides. --- ## 1. The 5 module kinds (v4, locked) | `_module_kind` | Canonical home | When | Who decides | |---|---|---|---| | `original` | the app repo | App does the work | App owner | | `borrowed` | another app repo (via `_canonical_source`) | App uses module made elsewhere | App owner | | `gallery-original` | the gallery itself | Operator chose gallery as long-term canonical (only when nowhere else fits) | Operator | | `gallery-mirror` | an external repo (mirrored to gallery) | Cross-app, mirrored for discovery | Operator decides whether to mirror | | `app-fork` | the app's repo (with `_forked_from` of the original) | App revibed a module | App owner (the revibe is theirs) | **The annotation in `module-meta.json`:** ```json // App's own work { "_module_kind": "original", "_canonical_source": null, ... } // App uses a module made elsewhere { "_module_kind": "borrowed", "_canonical_source": "avidtech6/ai-shell/ai-toast", ... } // Gallery owns this (only when no other home) { "_module_kind": "gallery-original", "_canonical_source": null, ... } // Gallery has a copy of something from elsewhere { "_module_kind": "gallery-mirror", "_canonical_source": "avidtech6/ai-shell/ai-toast", ... } // App revibed a module (it's a fork) { "_module_kind": "app-fork", "_canonical_source": "avidtech6/vibecoder-standalone", "_forked_from": "avidtech6/ai-shell/ai-toast", ... } ``` --- ## 2. The 4 questions the AI must ask When an AI is told "publish X to the gallery", it must ask the operator: ``` AI: "OK, I'll publish this. Before I do, 4 questions: 1. New or revibe? - New = a brand-new module that's never existed - Revibe = a fork/customisation of an existing module 2. Where's the canonical home? (where will the work actually happen?) - In an existing app repo (which one? e.g. avidtech6/freshmail-2) - In the gallery directly (gallery-original, only if nowhere else fits) - In a new repo (you'd create it) 3. Publish what to the gallery? - Recipes only (just the constitutional shape, ~30-200 KB) - Recipes + code (the full module, ~100 KB - a few MB) - Full module + tests + bundle (everything) 4. Fork of anything? - If yes, what is it a fork of? (point at the original) " ``` The AI never assumes. Each answer becomes a field in `module-meta.json` or `module.json`. **Defaults the AI should NOT apply:** - ❌ "New → gallery canonical" (wrong — v3's mistake) - ❌ "Publish with code" (wrong — might be recipes-only) - ❌ "This is its own thing" (wrong — might be a fork) **The only default:** if the operator says "wherever you think", the AI asks again, because this is a decision the operator must make. --- ## 3. What the AI does with the answers | Answers to the 4 questions | AI does | |---|---| | New + canonical in app repo + recipes+code + not a fork | Creates the module in the app repo, sets up CI to push mirror to gallery as `gallery-mirror` | | New + canonical in app repo + recipes only + not a fork | Creates the module in the app repo (with recipe book only), pushes recipe-book mirror to gallery as `gallery-mirror` | | New + canonical in gallery + recipes+code + not a fork | Creates the module in gallery as `gallery-original` (the gallery IS the home) | | Revibe + canonical in app repo + recipes+code + fork of X | Creates the fork in app's `modules/revibe//` per §18.3, sets up fork entry in gallery as `app-fork` | | New + canonical in new repo + recipes only + not a fork | Creates a new repo, sets it as canonical, gallery gets `gallery-mirror` | --- ## 4. The flow for a NEW module (v4, corrected) ``` 1. Operator says: "I want to publish module X to the gallery" 2. AI asks the 4 questions 3. Operator answers, e.g.: - New - Canonical at avidtech6/freshmail-2 - Recipes + code - Not a fork 4. AI: a. Creates the module at freshmail-2/modules/X/ - module.json with _module_kind: "original" - module-meta.json with _canonical_source: null - recipe-book/ (FVW §11: 3+5+2 = 10 items) - src/ (the code) - tests/ (if any) b. Sets up freshmail-2's CI to push mirror on release tag c. Pushes initial mirror to gallery/modules/X/ as gallery-mirror d. Updates gallery/registry.json to add the new entry e. Reports back: "Module X is canonical at freshmail-2, gallery has a mirror" ``` **The AI does NOT create the module directly in the gallery.** The gallery is just where the mirror lives. The canonical is at freshmail-2. **Exception:** if the operator says "canonical in the gallery" (because no app fits), the AI creates it in gallery/modules/X/ as `gallery-original`. This is the only case where the gallery IS the canonical. --- ## 5. The flow for a REVIBE (fork) ``` 1. Operator says: "I want to revibe module X in app A" 2. AI asks the 4 questions (most answers are implicit): - Revibe (operator said so) - Canonical at A (the app doing the revibe) - Recipes + code (default for revibes; operator can override) - Fork of X (the module being revibed) 3. AI: a. Creates the revibe at A/modules/revibe/X/ per §18.3 - module.json with _module_kind: "app-fork" - _canonical_source: "avidtech6/A" - _forked_from: "" - recipe-book/, src/, tests/ b. Sets up A's CI to push the fork entry to gallery c. The fork entry in gallery is at gallery/forks/A/X/ (folder per app, fork per module) d. Original X stays canonical at its original home e. The fork can diverge from the original over time (this is the point) ``` **The fork lives at the app's repo, NOT at the gallery.** The gallery has a fork entry to make the fork discoverable, but the canonical is at the app. --- ## 6. The mirror workflow (when source lives elsewhere) When a module is `gallery-mirror`, the gallery's copy must stay in sync with the source. **Two ways to keep in sync:** **Push from app CI (recommended):** - App's CI on release tag: pushes a mirror of the module to gallery - Gallery's `accept-mirrors.yml` workflow receives the push - Validates the mirror (checksum, recipe book version, etc.) - Updates the gallery's `module-meta.json` with the new version + last_verified - Updates `registry.json` **Pull from source by gallery (fallback):** - Gallery's `pull-mirrors.yml` workflow runs daily - For every `gallery-mirror` entry, pulls the source from `_canonical_source` - If the source has changed, updates the mirror - If the source is dead, marks the mirror as `orphaned` **Default: push from app.** Apps own the timing of their releases. Gallery doesn't unilaterally pull unless scheduled. --- ## 7. The cleanup workflow (orphaned → gallery-original) When a `gallery-mirror`'s source dies: ``` 1. AI runs cleanup-mirrors.yml (daily) 2. For every gallery-mirror, AI checks: - Does _canonical_source repo exist? (GitHub API) - Does the path exist? - Do checksums match? (recipe-book/recipe.md, key src files) 3. If source is dead: - Set _mirror_status: "orphaned" - Set _orphaned_at: - Keep the mirror where it is 4. If source has been dead for 30+ days: - Promote to _module_kind: "gallery-original" - _canonical_source: null - _promoted_from: - _promoted_at: 5. If source comes back: - If it's the same module: _mirror_status: "reattached", resume mirror - If it's a different module now: keep the orphan as gallery-original, create new entry for the new thing ``` **The 30-day threshold** is the default. Operator can override per entry (`_cleanup_threshold_days: 90` or `_cleanup_threshold_days: null` for "never auto-promote"). --- ## 8. Where things live in practice ``` avidtech6/vibecoder-standalone/ ← canonical for vibecoder stuff ├── modules/ │ ├── vibecoder-toolbar/ ← _module_kind: "original" │ ├── ai-toast/ ← _module_kind: "borrowed" │ │ _canonical_source: "avidtech6/ai-shell/ai-toast" │ ├── ai-mic/ ← _module_kind: "borrowed" │ └── revibe/ │ └── ai-toast-vibecoder-style/ ← _module_kind: "app-fork" │ _canonical_source: "avidtech6/vibecoder-standalone" │ _forked_from: "avidtech6/ai-shell/ai-toast" ├── recipe-book/ ← vibecoder's own recipe book └── ... avidtech6/fv-module-gallery/ ← the gallery (publisher) ├── modules/ │ ├── ai-toast/ ← _module_kind: "gallery-mirror" │ │ _canonical_source: "avidtech6/ai-shell/ai-toast" │ ├── ai-helper/ ← _module_kind: "gallery-original" │ │ (canonical IS the gallery; no app owns this) │ ├── vibecoder-toolbar/ ← _module_kind: "gallery-mirror" │ │ _canonical_source: "avidtech6/vibecoder-standalone/vibecoder-toolbar" │ └── ... ├── forks/ │ ├── vibecoder-standalone/ │ │ └── ai-toast-vibecoder-style/ ← _module_kind: "app-fork" │ │ _canonical_source: "avidtech6/vibecoder-standalone" │ │ _forked_from: "avidtech6/ai-shell/ai-toast" │ └── ... ├── bundles/ ├── apps/ ├── registry.json └── gallery-pact/ ├── AI-PUBLISHING-GUIDE.md ← the "how to publish" doc for AIs ├── MIRROR-WORKFLOW.md ├── CLEANUP-WORKFLOW.md └── fvw/ ← FVW v8 doctrine (mirrored) ``` --- ## 9. The constitutional rules (FVW v8.2 §17.5-§17.7) ### §17.5 — Module Canonical Home + Mirror Model > **Every module has a canonical home.** The canonical home is wherever the operator decides the work happens. The gallery is one possible home (`gallery-original`), but never the default. The AI must ask the operator where the canonical home is. > > The `_module_kind` field declares the kind. The 5 kinds: `original`, `borrowed`, `gallery-original`, `gallery-mirror`, `app-fork`. > > Gallery mirrors are for `npm install`, registry lookup, and discovery. The source repo is where the work happens. The mirror is for consumption, not editing. ### §17.6 — Mirror Cleanup + Auto-Promotion > A `gallery-mirror` whose source has been dead for 30+ days (configurable per entry) is auto-promoted to `gallery-original`. The `_promoted_from` field preserves the history. > > If the source comes back as the same module, the mirror is reattached. If it comes back as a different module, the orphan stays as `gallery-original` (legacy), and the new module gets its own entry. ### §17.7 — Revibe = Fork > A revibe (§18) is a fork. The moment an app revibes a module, the revibe is a fork with `_module_kind: "app-fork"`, and the canonical home is the app's repo. The gallery has a fork entry for discoverability, but the canonical is at the app. --- ## 10. The 4 questions, as a checklist (for the AI) ``` [ ] Q1: New or revibe? - If new: it's a brand-new module, no precedent - If revibe: it's a fork of an existing module, point _forked_from [ ] Q2: Where's the canonical home? - In an app repo (which one? which folder?) - In the gallery (only if no app fits) - In a new repo (operator creates it) - The AI never assumes the gallery [ ] Q3: Publish what? - Recipes only (recipe-book/, ~30-200 KB) - Recipes + code (recipe-book/ + src/, ~100 KB - few MB) - Full module + tests + bundle [ ] Q4: Fork of anything? - If yes: _forked_from points at the original - If no: this is a fresh thing The AI asks all 4. The operator answers all 4. The AI then proceeds. ``` --- ## 11. Open questions 1. **The 30-day cleanup threshold** — is 30 days right, or should it be longer? Operator can override per entry. I'd lean 30 days as default. 2. **The fork folder naming** — `gallery/forks/A/X/` (folder per app) vs `gallery/modules/X-fork-A/` (suffix with app). I lean toward `gallery/forks/A/X/`. 3. **The push-vs-pull mirror workflow** — push from app CI is the default; pull from source is the fallback. Confirm? 4. **The `gallery-original` promotion threshold** — is 30+ days of being orphaned right, or should the operator explicitly promote? 5. **The AI doc location** — `gallery-pact/AI-PUBLISHING-GUIDE.md` in the gallery, OR per-app `agents.md`? I'd say gallery (so it's a shared resource for any AI that touches the gallery). --- ## 12. What to do next ### Step 1: Operator approval Read this doc, confirm or amend the 5 kinds, the 4 questions, the 30-day threshold, the push-from-app default. ### Step 2: FVW v8.2 doctrine Add §17.5, §17.6, §17.7 to the canon. Update CHANGELOG. ### Step 3: Gallery infrastructure - `gallery-pact/AI-PUBLISHING-GUIDE.md` — the AI-facing doc - `gallery-pact/MIRROR-WORKFLOW.md` — how the mirror works - `gallery-pact/CLEANUP-WORKFLOW.md` — the orphan → gallery-original flow - Update `module-meta.schema.json` to add `_module_kind`, `_canonical_source`, `_forked_from`, `_mirror_status` - Create `gallery/forks/` folder - Update `registry.json` schema ### Step 4: Workflow files - `.github/workflows/accept-mirrors.yml` — accepts pushes from app CI - `.github/workflows/pull-mirrors.yml` — daily pull from sources (fallback) - `.github/workflows/cleanup-mirrors.yml` — daily orphan check ### Step 5: Annotate existing modules - `gallery/modules/ai-toast/` — add `_module_kind: "gallery-mirror"`, `_canonical_source: "avidtech6/ai-shell/ai-toast"` - Update `registry.json` ### Step 6: Migrate freshcloud-mail (test case) - `gallery/apps/freshcloud-mail/` thin entry - Recipe book mirror - Bundle manifest mirror - freshmail-2's CI sets up to push mirrors (or just manual sync for now) - 17 mail-engine modules stay in freshmail-2 (no gallery mirror — they're mail-specific) ### Step 7: Sweep other apps (parked) - vibecoder-standalone, freshcards-standalone, vibescope-standalone, fvs — thin app entries - Each sets up its own mirror CI - Parked until freshcloud-mail migration proves the pattern ### Estimated effort - Step 1: 0 - Step 2: 1 hour (FVW v8.2) - Step 3: 1-2 hours (docs + schema) - Step 4: 2-3 hours (workflows) - Step 5: 30 min - Step 6: 2-3 hours - Step 7: parked - Total core: ~7-10 hours, can be spread over a week --- ## Related docs - `/workspace/position-gallery-entry-shapes-recipes-and-code.md` (v1, superseded) - `/workspace/position-gallery-entry-shapes-revised.md` (v2, superseded) - `/workspace/position-gallery-entry-shapes-v3.md` (v3, superseded — had the "new → gallery canonical" error) - `/workspace/position-gallery-entry-shapes-v4.md` (THIS DOC, locked) - `/workspace/gallery-investigation.md` - `/workspace/canonical-home-proposal.md` - `/workspace/facets-in-fvw-v8.md` - `/workspace/recipe-book-shape.md` - `/workspace/audit/freshcloud-mail-recipes/README.md` (recipes merged into freshmail-2 on 2026-07-21) - `avidtech6/fv-module-gallery/gallery-pact/registry-plan.md` (the gallery's design doc, needs v4 update)