# Gallery Entry Shapes — v3 (Canonical Home + Mirror Model) > **Status**: DRAFT v3 for operator review (2026-07-22) > **Author**: Mavis > **Replaces**: v1 (`position-gallery-entry-shapes-recipes-and-code.md`), v2 (`position-gallery-entry-shapes-revised.md`) > **Trigger**: Operator's 2026-07-22 17:35 message — "if an app has its own repo its canonical own modules live in its repo its borrowed modules live either in gallery or app repo that made them. but each app can push a copy to gallery and probably should in most cases, its only a mirrored copy for easy access. if an app doesn't have a repo yet then all its modules are either in gallery or in another repo. If their is a module in gallery that doesn't have an outside canonical source its canonical is the gallery and it must be marked as such" --- ## TL;DR **Every module has a canonical home. The gallery is a publisher, not an author.** - An app with its own repo **owns** its originals in that repo - Borrowed modules are **pointed at** (canonical lives at the source app) - The gallery has **mirrors** for cross-app modules — for discovery and easy access - A module with no outside canonical home is **gallery-original** — the gallery IS canonical, and it's marked as such There is no "promotion" — modules are born where they're born. The gallery mirror is optional, recommended for cross-app discoverability. **The single sentence rule:** > Every module has a canonical home. The canonical home is where the work happens. The gallery has a mirror if and only if the module is cross-app reusable. The mirror is for discovery and consumption, not for editing. --- ## 1. The 4 module kinds Every module has a `_module_kind` field (in `module.json` or `module-meta.json`) that declares its canonical home: | `_module_kind` | Canonical home | Gallery has it? | Why | |---|---|---|---| | `original` | the app repo it's in | optional mirror | App's own work; mirrored for discoverability | | `borrowed` | another app repo (via `_canonical_source`) | already mirrored by source | App uses a module made elsewhere | | `gallery-original` | the gallery itself | yes (it's the gallery) | Module has no other home; gallery IS canonical | | `gallery-mirror` | an external repo (via `_canonical_source`) | yes (it's a mirror) | Module is cross-app; gallery mirror for discovery | **The annotation in `module.json`:** ```json // Case 1: app's own original { "_module_kind": "original", "_canonical_source": null, "name": "vibecoder-toolbar", ... } // Case 2: app borrowed from elsewhere { "_module_kind": "borrowed", "_canonical_source": "avidtech6/ai-shell/ai-toast", "name": "ai-toast", ... } // Case 3: gallery owns this (no outside home) { "_module_kind": "gallery-original", "_canonical_source": null, "name": "ai-helper", ... } // Case 4: gallery mirror of external { "_module_kind": "gallery-mirror", "_canonical_source": "avidtech6/ai-shell/ai-toast", "name": "ai-toast", ... } ``` --- ## 2. The 3 cases, with examples ### Case 1: app has its own repo (vibecoder, freshmail-2, fvs, etc.) ``` avidtech6/vibecoder-standalone/ ← the canonical home for vibecoder ├── modules/ │ ├── vibecoder-toolbar/ ← ORIGINAL — vibecoder's own work │ │ ├── module.json (_module_kind: "original") │ │ ├── recipe-book/ │ │ ├── src/ │ │ └── tests/ │ ├── vibecoder-chat-panel/ ← ORIGINAL │ ├── ai-toast/ ← BORROWED (canonical at ai-shell) │ │ ├── module.json (_module_kind: "borrowed", │ │ │ _canonical_source: "avidtech6/ai-shell/ai-toast") │ │ ├── recipe-book/ ← copied from ai-shell, kept in sync │ │ ├── src/ ← copied from ai-shell │ │ └── tests/ │ └── ai-mic/ ← BORROWED ├── recipe-book/ ← vibecoder's own recipe book ├── package.json └── ... ``` In vibecoder's repo: some modules are **originals** (vibecoder's own work, canonical here) and some are **borrowed** (used by vibecoder but the canonical work happens elsewhere, in this case `ai-shell`). The borrowed ones point at their canonical source. ### Case 2: module has no outside home (gallery-original) ``` avidtech6/fv-module-gallery/modules/ai-helper/ ← canonical IS the gallery ├── module.json (_module_kind: "gallery-original", │ _canonical_source: null) ├── recipe-book/ ├── src/ └── tests/ ``` When a module has no other canonical home, the gallery IS canonical. The `_module_kind: "gallery-original"` field makes this explicit. These are modules that are born in the gallery (or modules whose original home was retired, e.g. `freshmail-1` was archived and its modules could be re-homed as `gallery-original`). ### Case 3: module is at an app repo AND mirrored in gallery (cross-app) ``` avidtech6/ai-shell/ai-toast/ ← canonical home (the original) ├── module.json (_module_kind: "original") ├── recipe-book/ ├── src/ └── tests/ # ↓ AI-shell's CI pushes a mirror to the gallery on release avidtech6/fv-module-gallery/modules/ai-toast/ ← MIRROR copy, synced from ai-shell ├── module.json (_module_kind: "gallery-mirror", │ _canonical_source: "avidtech6/ai-shell/ai-toast") ├── recipe-book/ ← mirror ├── src/ ← mirror └── tests/ ← mirror ``` The gallery copy is a mirror. It can be: - **Pull-synced** (workflow pulls from `_canonical_source` on a schedule) — for read-only mirrors - **Pushed by the app** (the app's CI pushes its originals to the gallery on every release) — for app-driven mirrors (recommended) - **Forked** (gallery copy has its own life, with `forked_from` field) — for divergent evolution (rare) **The vibecoder case is the "pushed by the app" model:** vibecoder's CI pushes its originals to the gallery on every release, so the gallery has mirrors of vibecoder-toolbar, vibecoder-chat-panel, etc. for other apps to discover. --- ## 3. The full gallery structure (v3) ``` avidtech6/fv-module-gallery/ ← the gallery (publisher + discovery) ├── modules/ ← cross-app modules (mirrors OR originals) │ ├── ai-toast/ ← _module_kind: "gallery-mirror", src: ai-shell │ ├── ai-mic/ ← _module_kind: "gallery-mirror", src: ai-shell │ ├── ai-helper/ ← _module_kind: "gallery-original" │ ├── vibecoder-toolbar/ ← _module_kind: "gallery-mirror", src: vibecoder-standalone │ ├── vibecoder-chat-panel/ ← _module_kind: "gallery-mirror", src: vibecoder-standalone │ └── ... │ ├── bundles/ ← cross-app bundles │ ├── ai-bundle/ ← lists cross-app modules │ │ ├── module.json │ │ ├── bundle-manifest.json │ │ └── bundle-features.md │ └── ... │ ├── apps/ ← app entries (optional, for discoverability) │ ├── vibecoder-standalone/ ← thin entry: just metadata + recipe book mirror │ │ ├── module.json (_module_kind: "app-pointer", │ │ │ _canonical_source: "avidtech6/vibecoder-standalone") │ │ ├── recipe-book/ ← mirror of vibecoder's recipe book │ │ └── ... ← no modules/ subfolder (they're at vibecoder) │ ├── freshcloud-mail/ ← thin entry │ │ ├── module.json (_module_kind: "app-pointer", │ │ │ _canonical_source: "avidtech6/freshmail-2") │ │ ├── recipe-book/ ← mirror of freshmail-2's recipe book │ │ └── bundles/freshcloud-mail-engine-bundle/ ← mirror of the bundle manifest │ ├── freshcards-standalone/ │ ├── vibescope-standalone/ │ ├── fvs/ ← FreshVibeStudio │ └── ... │ ├── registry.json ← the index (lists all entries + their kinds) │ └── gallery-pact/ ← doctrine copy + schemas ├── registry-plan.md ├── module-meta.schema.json ← updated to include _module_kind ├── ... └── fvw/ ← FVW v8 doctrine (mirrored) ``` --- ## 4. Who owns what (the answer to "vibecoder still has all the code, just some marked as copy and some as original") **Yes, exactly.** vibecoder-standalone still has all the code. The `_module_kind` field in each module's `module.json` declares: - `original` — vibecoder does the work here. vibecoder is the canonical home. - `borrowed` — vibecoder uses this, but the work happens at `_canonical_source` (e.g. ai-shell). vibecoder's copy is a consumer copy, kept in sync. The split inside vibecoder's repo is: ``` vibecoder-standalone/modules/ ├── vibecoder-toolbar/ ← _module_kind: "original" │ (vibecoder maintains this) ├── vibecoder-chat-panel/ ← _module_kind: "original" │ (vibecoder maintains this) ├── ai-toast/ ← _module_kind: "borrowed" │ _canonical_source: "avidtech6/ai-shell/ai-toast" │ (vibecoder CONSUMES this from ai-shell) ├── ai-mic/ ← _module_kind: "borrowed" │ _canonical_source: "avidtech6/ai-shell/ai-mic" └── ... ``` **No code is moved.** vibecoder still has all the source. The annotation just declares where the work happens. **The gallery mirror workflow:** 1. vibecoder's CI on release: pushes its `_module_kind: "original"` modules to `gallery/modules//` (as `_module_kind: "gallery-mirror"`) 2. vibecoder's CI on pull from ai-shell: updates its `_module_kind: "borrowed"` modules to match ai-shell's source 3. The gallery never edits vibecoder's originals — it just mirrors them --- ## 5. freshcloud-mail fits cleanly ``` avidtech6/freshmail-2/ ← canonical home for freshcloud-mail ├── modules/ ← the 17 mail-engine modules (all originals) │ ├── mail-engine-smtp/ │ │ ├── module.json (_module_kind: "original") │ │ ├── recipe-book/ │ │ └── src/ ← PHP │ ├── mail-engine-imap-client/ │ └── ... (15 more) ├── bundles/ │ └── freshcloud-mail-engine-bundle/ │ ├── module.json │ ├── bundle-manifest.json │ └── bundle-features.md ├── recipe-book/ ← freshcloud-mail's recipe book ├── src/ ← the app code (FreshCloud\Mail\*) ├── api/ ← bridge endpoints └── tests/ # ↓ freshmail-2's CI pushes mirrors to the gallery (or not — they're mail-specific, may skip) avidtech6/fv-module-gallery/apps/freshcloud-mail/ ← thin app entry ├── module.json (_module_kind: "app-pointer", │ _canonical_source: "avidtech6/freshmail-2") ├── recipe-book/ ← mirror of freshmail-2/recipe-book ├── bundles/freshcloud-mail-engine-bundle/ ← mirror of the bundle manifest │ ├── module.json │ ├── bundle-manifest.json │ └── bundle-features.md └── ... ← NO modules/ subfolder (the 17 modules stay in freshmail-2, they're mail-specific, no gallery mirror) ``` **The 17 mail-engine modules don't go in the gallery** because: - They're mail-specific (not cross-app) - No other app needs them - Mirroring them to the gallery would be pointless - The freshcloud-mail app entry has a mirror of the bundle manifest that lists them, so they're discoverable through the app **If someday someone wants to use mail-engine-smtp in another app** — they fork it, give it a new canonical home, and a new mirror appears in the gallery. No "promotion" needed. --- ## 6. The constitutional rule (final, v3) > **Every module has a canonical home.** The canonical home is one of: > - The app repo where the work happens (`_module_kind: "original"`) > - Another app repo at `_canonical_source` (`_module_kind: "borrowed"`) > - The gallery itself (`_module_kind: "gallery-original"`) > - An external repo at `_canonical_source`, mirrored to gallery (`_module_kind: "gallery-mirror"`) > > **The gallery is a publisher and discovery surface, not an author.** Gallery mirrors are for `npm install` and registry lookup. The source repo is where the work happens. > > **Apps with their own repos push mirrors of their originals to the gallery on release.** This is recommended for discoverability, not required. > > **Modules with no outside canonical home are gallery-originals.** The gallery IS canonical. This is explicit (`_module_kind: "gallery-original"`, `_canonical_source: null`). > > **Recipe books live with the canonical home.** Gallery mirrors of recipe books are sync'd from canonical. > > **Apps that consume borrowed modules don't create a second gallery mirror.** The borrowing app just uses the canonical source (which itself has a gallery mirror). --- ## 7. Comparison: v1, v2, v3 | Question | v1 | v2 | v3 | |---|---|---|---| | Where do freshcloud-mail's 17 modules live? | `gallery/modules/` (top-level) | `gallery/apps/freshcloud-mail/modules/` | `avidtech6/freshmail-2/modules/` (canonical) — no gallery mirror | | Who owns the work? | Gallery | Gallery | Source repo (apps are sovereign) | | Is VibeCoder a gallery entry? | Not addressed | Yes, with full `recipe-book/` | Yes, but THIN: just metadata + recipe book mirror + pointer | | What's the "slightly different case"? | "App vs module" | "App-specific vs cross-app" | "Canonical home is wherever the work happens" | | Promotion? | "Code defaults in gallery" | "Promoted when 2+ apps use" | Not a thing — modules are born where they're born | | Mirror type? | "code_location: gallery/external" | Same | 4 explicit kinds via `_module_kind` | | FVW section? | §17.5 (new) | §17.5 (revised) | §17.5 (Canonical Home + Mirror Model) — covers the 4 kinds | --- ## 8. Open questions for operator 1. **The 4 kinds** — is `_module_kind: "original" | "borrowed" | "gallery-original" | "gallery-mirror"` the right 4? Or should `original` and `borrowed` collapse into a single "app-side" kind, with the source repo just always declared? 2. **The `_module_kind` location** — should it be in `module.json`, `module-meta.json`, or a separate `_module-kind.json`? I'd say `module-meta.json` (since module.json is the constitutional doc, module-meta.json is the metadata). 3. **The mirror workflow** — pull-from-source (gallery pulls on schedule) vs push-from-app (app CI pushes on release)? I'd say push-from-app is the default; pull-from-source is the fallback. 4. **The `apps/` folder** — should it exist at all? Or just register apps in `registry.json` as a different entry type? My recommendation: thin `apps/` folders, parallel to `modules/`, for app discovery. 5. **The forked case** — when a gallery mirror diverges from its source (gallery adds a facet, changes an export), is it still `_module_kind: "gallery-mirror"` or does it become `_module_kind: "gallery-fork"` with `forked_from: "..."`? I'd say add a `forked_from` field to the existing kinds; no new kind needed. 6. **Recipe book sync** — when the canonical recipe book updates, the gallery mirror updates. The app's borrowed modules also need to update their copies. Two syncs. Is that OK or should the app just consume the gallery's mirror directly (no in-repo copy)? --- ## 9. What to do next (v3) ### Step 1: Operator approval Read this doc, confirm or amend the 4 kinds, the `_module_kind` field, the push-from-app mirror workflow. ### Step 2: Add FVW v8.2 doctrine Section §17.5: "Module Canonical Home + Mirror Model" — covers the 4 kinds, the annotation, the mirror workflow. Update CHANGELOG (v8.2 minor bump). ### Step 3: Update registry schema - Add `_module_kind` field to `module-meta.schema.json` - Add `_canonical_source` field - Add `app-pointer` kind - Add `apps/` folder convention ### Step 4: Add `_module_kind` to existing gallery entries - `ai-toast`: `_module_kind: "gallery-mirror"`, `_canonical_source: "avidtech6/ai-shell/ai-toast"` - Update the bundle manifest to declare kinds ### Step 5: Set up mirror workflow - `fv-module-gallery/.github/workflows/accept-mirrors.yml` - Accepts pushes from app repos on release - Validates the mirror matches the source's recipe book (or at least the version field) - Updates registry.json ### Step 6: Migrate freshcloud-mail (the test case) - `gallery/apps/freshcloud-mail/` thin entry (metadata + recipe book mirror + pointer) - No modules/ subfolder (the 17 modules stay in freshmail-2) - Bundle mirror at `apps/freshcloud-mail/bundles/freshcloud-mail-engine-bundle/` - freshmail-2's CI sets up to push its recipe book mirror on release (or just manual sync) ### Step 7: Migrate other apps (sweep, optional) - `gallery/apps/vibecoder-standalone/` (thin entry) - `gallery/apps/freshcards-standalone/` - `gallery/apps/vibescope-standalone/` - `gallery/apps/fvs/` - Each app sets up its CI to push mirrors ### Estimated effort - Step 1: 0 (read this doc) - Step 2: 1 hour (FVW v8.2 §17.5) - Step 3: 1 hour (registry schema update) - Step 4: 30 min (annotate ai-toast + ai-bundle) - Step 5: 2 hours (mirror workflow) - Step 6: 1-2 hours (freshcloud-mail migration) - Step 7: parked or 2-3 hours (sweep) - Total core: ~5-7 hours, can be spread over a week --- ## 10. The "app gallery" question, once more **Was there an app gallery built?** No (confirmed earlier). **Should there be one?** Yes, as `apps/` in the existing `avidtech6/fv-module-gallery`. But it's a **thin pointer folder**, not a full mirror of app code. **What goes in it?** Metadata + recipe book mirror + source_location pointer. No code. **Is this the same as the module gallery?** Yes — one gallery, three top-level folders: `modules/`, `bundles/`, `apps/`. --- ## Related docs - `/workspace/position-gallery-entry-shapes-recipes-and-code.md` (v1, superseded) - `/workspace/position-gallery-entry-shapes-revised.md` (v2, superseded) - `/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 to be updated to v3 model)