10 KiB
monorepo-structure
Purpose
The genesis monorepo is a Turborepo + Bun workspaces tree organised by domain rather than tier. Every package lives at packages/<category>/<pkg>/, where <category> is a one-word domain folder listed in FOLDERS.md and <pkg> 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/<category>/<package>/ where <category> is the folder name from FOLDERS.md and <package> 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/<category>/<package>/AND<category>is listed inFOLDERS.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/, orgaming/directories
Scenario: Cross-tier domain co-location
- WHEN a domain has packages spanning multiple tiers (e.g.
drivewith 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.ymlis 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 --checkis run - THEN it exits with status 0 AND the committed
MANIFEST.mdmatches the regenerated output byte-for-byte
Scenario: MANIFEST.md reflects category moves
- WHEN
bun scripts/folder-reorg/generate-manifest.tsruns 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.jsondeclares it as a dep AND no source file imports it AND no folder namedoverview/exists underpackages/
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.ymlis 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-ORpackage.jsonhas a top-level"python"field. - r3f:
peerDependenciesincludes@react-three/fiberORthree. - react:
peerDependenciesincludesreact(and tier is not r3f). - browser:
package.jsonhas a"browser"field ORengineslacksnode. - server:
binfield is set OR direct dependencies includebun: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 --checkis 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/<category>/<pkg>/ paths. References to legacy universal/<domain>/<pkg>/ or packages/<tier>/<pkg>/ paths SHALL NOT exist in either file.
Scenario: No legacy path references
- WHEN
CLAUDE.mdandopenspec/AGENTS.mdare 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.jsonfiles - THEN every discovered package name appears in
assignment.yml - AND every package in
assignment.ymlthat 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/<any-category>/<any-existing-pkg>/src/<any-file>.tsis run after migration - THEN the log shows commits from before the migration (via
git mvor via the cp-fallback's content-similarity rename detection)
Requirement: Barrel export form
Per-feature module barrels (src/<category>/<feature>/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 <name> 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
<feature>.tsmust not auto-leak through the barrel. - Type-only re-exports remain clean (
export { type X }per symbol). - Tree-shaking is equivalent under
"sideEffects": falsewith rollup or esbuild — there is no bundle-size argument forexport *.
Scenario: Feature-module barrel uses explicit named exports
- WHEN
src/<category>/<feature>/index.tsre-exports the contents of<feature>.ts - THEN the barrel uses
export { Symbol1, type Type1, ... } from "./<feature>.js" - AND the barrel does NOT contain a line starting with
export * from "./<feature>(with or without the.jssuffix)
Scenario: Generated selector index exemption
- WHEN
src/<path>/<primitive>.select/index.tsexists for a per-primitive selector - THEN that file is permitted to contain
export * from "./<primitive>.select.js" - AND its parent directory ends in
.select
Scenario: Namespace re-export at package root is permitted
- WHEN
src/index.tsre-exports a sub-namespace - THEN it MAY use
export * as <name> from "./<path>.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