Files
suaveplan/openspec/specs/monorepo-structure/spec.md
2026-05-27 23:29:53 +01:00

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 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/<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.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/<any-category>/<any-existing-pkg>/src/<any-file>.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/<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>.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/<category>/<feature>/index.ts re-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 .js suffix)

Scenario: Generated selector index exemption

  • WHEN src/<path>/<primitive>.select/index.ts exists 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.ts re-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