# monorepo-structure ## Purpose The genesis monorepo is a Turborepo + Bun workspaces tree organised by domain rather than tier. Every package lives at `packages///`, where `` is a one-word domain folder listed in [`FOLDERS.md`](../../FOLDERS.md) and `` is the npm name without the `@suaveplan/` prefix. Tier (universal / browser / react / r3f / server / python) is inferred per package from `package.json` (`"browser"` field, peer deps, `engines`), never encoded in the directory path. Packages land in dependency-order waves at version `0.1.0` until a `1.0.0` graduation release, file history is preserved across the migration via `git mv`, and the canonical view of plugins → packages → tier-overlay → dependency graph lives in a generated `MANIFEST.md` at the repository root that supersedes the legacy `@suaveplan/overview` package. ## Requirements ### Requirement: Domain-first folder layout The monorepo SHALL organise packages into domain folders defined in `FOLDERS.md`. The layout is `packages///` where `` is the folder name from `FOLDERS.md` and `` is the npm name without the `@suaveplan/` prefix. Tier (universal / browser / react / r3f / server / python) is NOT a directory — it is determined per-package by `package.json` deps, the `"browser"` field, peer deps, and `engines`. #### Scenario: Every package lives under a plugin folder - **WHEN** any package directory is inspected - **THEN** its path is `packages///` AND `` is listed in `FOLDERS.md` #### Scenario: No tier-named top-level folders - **WHEN** the repository root is listed - **THEN** there are no `universal/`, `browser/`, `react/`, `r3f/`, `api/`, `server/`, `finance/`, `mcp/`, `python/`, or `gaming/` directories #### Scenario: Cross-tier domain co-location - **WHEN** a domain has packages spanning multiple tiers (e.g. `drive` with universal core, browser P2P plugin, react UI, and api plugin) - **THEN** all of those packages live under the same category folder (`packages/drive/`) ### Requirement: Category → package assignment uniqueness Every package SHALL appear in exactly one category folder. No package may be present in two categories. #### Scenario: Assignment manifest has unique entries - **WHEN** `scripts/folder-reorg/assignment.yml` is parsed - **THEN** the union of all packages across all categories contains no duplicates ### Requirement: Generated MANIFEST.md replaces @suaveplan/overview The repository SHALL contain a generated `MANIFEST.md` at the root that lists every plugin, every package, the inferred tier per package, the dependency graph, and a tier-overlay view. The manifest is regenerated by `bun scripts/folder-reorg/generate-manifest.ts` and a `--check` mode validates that the committed file is up-to-date. #### Scenario: MANIFEST.md is up to date - **WHEN** `bun scripts/folder-reorg/generate-manifest.ts --check` is run - **THEN** it exits with status 0 AND the committed `MANIFEST.md` matches the regenerated output byte-for-byte #### Scenario: MANIFEST.md reflects category moves - **WHEN** `bun scripts/folder-reorg/generate-manifest.ts` runs after a package realignment - **THEN** every moved package appears under its new category in `MANIFEST.md` - **AND** no retired category folder appears in `MANIFEST.md` **Verified by:** `scripts/folder-reorg/generate-manifest.ts --check` #### Scenario: @suaveplan/overview is absent - **WHEN** the repository is searched for `@suaveplan/overview` - **THEN** no `package.json` declares it as a dep AND no source file imports it AND no folder named `overview/` exists under `packages/` ### Requirement: FOLDERS.yml is removed `FOLDERS.yml` SHALL NOT exist at the repository root. The folder layout is self-describing via the `packages/` tree and the per-tier overlay lives in `MANIFEST.md`. #### Scenario: FOLDERS.yml absent - **WHEN** the repository root is listed - **THEN** no file named `FOLDERS.yml` is present ### Requirement: Tier inference from package.json `scripts/folder-reorg/generate-manifest.ts` SHALL infer each package's tier deterministically from its `package.json` and source layout, in this priority order: python → r3f → react → browser → server → universal. - **python**: package directory name has prefix `python-` OR `package.json` has a top-level `"python"` field. - **r3f**: `peerDependencies` includes `@react-three/fiber` OR `three`. - **react**: `peerDependencies` includes `react` (and tier is not r3f). - **browser**: `package.json` has a `"browser"` field OR `engines` lacks `node`. - **server**: `bin` field is set OR direct dependencies include `bun:sqlite`, `node:fs`, `node:net`, `node:cluster`, `pg`, `ioredis`, or similar Node-only modules. - **universal**: none of the above match. #### Scenario: Tier inference is deterministic - **WHEN** `scripts/folder-reorg/generate-manifest.ts --check` is run twice without source changes - **THEN** both runs produce identical tier classifications for every package ### Requirement: Path references in CLAUDE.md and openspec/AGENTS.md updated `CLAUDE.md` and `openspec/AGENTS.md` SHALL reference packages via `packages///` paths. References to legacy `universal///` or `packages///` paths SHALL NOT exist in either file. #### Scenario: No legacy path references - **WHEN** `CLAUDE.md` and `openspec/AGENTS.md` are searched for the regex `(^|[^a-z])(universal|browser|react|r3f|server|finance|mcp|python|gaming)/[a-z]` - **THEN** zero matches are found in either file **Verified by:** `scripts/check-fixture-resolves.ts` (path reference validation) ### Requirement: assignment.yml is the machine-readable mirror of FOLDERS.md `scripts/folder-reorg/assignment.yml` SHALL list every on-disk package exactly once, keyed by its `FOLDERS.md` category. The file SHALL be parseable by the migration script and SHALL produce zero duplicate or orphan errors in `--check` mode. #### Scenario: assignment.yml passes uniqueness check - **WHEN** the migration script parses `assignment.yml` - **THEN** no package name appears under more than one category key - **AND** no package name appears under `__retired__` and also under a category #### Scenario: assignment.yml covers every on-disk package - **WHEN** the migration script discovers all `packages/*/*/package.json` files - **THEN** every discovered package name appears in `assignment.yml` - **AND** every package in `assignment.yml` that does not exist on disk is silently skipped (future package) **Verified by:** `scripts/folder-reorg/realign-to-categories.ts --check` ### Requirement: No hard-coded retired paths in committed documentation No committed file in the repository SHALL contain a hard-coded path to a retired category folder (e.g., `packages/codes/`, `packages/primitives/`, `packages/vault/`, etc.). Documentation, scripts, and workflow files SHALL reference the current category. #### Scenario: Doc-reference sweep is clean - **WHEN** a grep runs for each retired category folder name across the repo root - **THEN** zero matches are found in any committed file **Verified by:** `scripts/folder-reorg/realign-to-categories.ts --check` (orphan detection) AND manual grep verification in `STATUS-OPENSPEC-GAPS.md` ### Requirement: Migration is reversible until merged The folder migration SHALL execute via `git mv` (preserving file history) in a single commit, allowing `git revert` to restore the legacy layout if a regression is found before merge. Filesystems where the rename syscall is blocked (e.g. WSL2/DrvFs with Windows-side handles) MAY use a `cp` + `git rm` fallback that copies tracked files only, preserving rename detection via content similarity in `git log --follow`. #### Scenario: File history preserved - **WHEN** `git log --follow packages///src/.ts` is run after migration - **THEN** the log shows commits from before the migration (via `git mv` or via the cp-fallback's content-similarity rename detection) ### Requirement: Barrel export form Per-feature module barrels (`src///index.ts`) SHALL re-export their module surface using **explicit named re-exports**, not `export * from`. Package-root barrels (`src/index.ts`) MAY use `export * from` or `export * as from` namespace exports until they are migrated under a separate change. Generated selector indexes (paths matching `*.select/index.ts`, plus per-impl-variant forwarders such as `*.native/index.ts` and `*.noble/index.ts` that contain only a single `export *` forwarder line) are exempt because transparent forwarding of the impl surface is the entire purpose of those files. The reasons match `openspec/AGENTS.md` §6a: - The barrel is the public contract; `export *` makes the contract whatever the source file happens to declare today. - Adding a new private helper to `.ts` must not auto-leak through the barrel. - Type-only re-exports remain clean (`export { type X }` per symbol). - Tree-shaking is equivalent under `"sideEffects": false` with rollup or esbuild — there is no bundle-size argument for `export *`. #### Scenario: Feature-module barrel uses explicit named exports - **WHEN** `src///index.ts` re-exports the contents of `.ts` - **THEN** the barrel uses `export { Symbol1, type Type1, ... } from "./.js"` - **AND** the barrel does NOT contain a line starting with `export * from "./` (with or without the `.js` suffix) #### Scenario: Generated selector index exemption - **WHEN** `src//.select/index.ts` exists for a per-primitive selector - **THEN** that file is permitted to contain `export * from "./.select.js"` - **AND** its parent directory ends in `.select` #### Scenario: Namespace re-export at package root is permitted - **WHEN** `src/index.ts` re-exports a sub-namespace - **THEN** it MAY use `export * as from "./.js"` (namespace export) #### Scenario: Repository scan finds no banned barrel forms - **WHEN** `grep -rn '^\s*export\s\+\(type\s\+\)\?\*\s\+from' packages/ --include='index.ts' | grep -v '/dist/' | grep -v '/src/index.ts:' | grep -v '\.select/' | grep -v '\.native/index\.ts:' | grep -v '\.noble/index\.ts:' | grep -v 'export \* as'` is run - **THEN** the output is empty