Status: Active Owner: Platform Architecture Phase: 2 ofDocumentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
docs/recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md (lands via PR #137)
Companion ADR: ADR-016-workspace-packages-overlay.md
This document explains the overlay workspace model introduced in Phase 2: what it is, why we chose it over a full physical migration, what guarantees it provides today, and what a full migration would look like later.
TL;DR
- The repo is now an npm workspace with one package per architectural unit:
encoreos-app/encoreos-public— Turborepo / Vercel Microfrontends keys only (placeholderpackages/*with noencoreostier metadata; seeMICROFRONTENDS_RUNBOOK.md)@encoreos/platform— Platform Foundation@encoreos/shared— leaf-level reusable UI / lib@encoreos/core-{ce,cl,fa,fm,fw,gr,hr,it,lo,pm,rh}— one per domain core
- Source files have not moved. Each
packages/<name>/package.jsonreferences its source undersrc/<scope>/<slug>/via theencoreos.sourceDirectoryfield. Existing@/cores/hr/...imports keep working unchanged. - Each package’s
package.jsondeclares what it is allowed to depend on.npm run audit:package-boundariesenforces the constitution at the package-metadata layer:- Cores depend only on
@encoreos/platformand@encoreos/shared. @encoreos/platformdepends on@encoreos/sharedonly.@encoreos/shareddepends on neither platform nor any core.- CL is downstream: no other core may declare a dep on
@encoreos/core-cl(constitution §1.4).
- Cores depend only on
- This unlocks Turborepo
--filterso a PR touching one core canturbo run typecheck --filter=@encoreos/core-hr...[origin/prod]and skip cache work for the other 11. - It also unlocks the dependency graph view (
npm ls --workspaces --depth=0, Turborepo’s--graph) so reviewers can see “who depends on whom” at a glance.
Why an overlay (and not a full physical migration)?
The recommendations doc (PR #137) § 5.4 originally proposed a full physical migration:mv src/cores/hr/** packages/core-hr/src/** and rewrite every @/cores/hr/... import. That delivers the strongest possible TS-project-references win (per-core warm typecheck in seconds), but at very high cost:
| Overlay (this PR) | Full physical migration | |
|---|---|---|
| Source files moved | 0 | ~8,300 |
| Imports rewritten | 0 | ~10,000+ |
| PRs required | 1 | 12+ (one per core) |
| Risk to live features | near-zero | meaningful |
| Architecture lint coverage | package-metadata layer (audit:package-boundaries) + import layer (check-architecture.js, unchanged) | same, plus TS project boundaries |
Turborepo --filter works | yes | yes |
tsc --build per-package incremental | no (single TS project, ~8.4 s warm) | yes (~1 s per untouched core) |
| Reversible | trivial — delete packages/<name>/ | invasive |
prod long enough to expose any tooling edge-cases.
Layout
package.json declares:
packages/docs is intentionally excluded from the workspaces glob — it has its own package-lock.json and dependency tree (Docusaurus) that we don’t want hoisted into our root node_modules. Keep it standalone unless we explicitly decide to Turbo-cache docs in CI.
What this package model enforces
At the package-metadata layer (this PR’s contribution)
Everypackages/<name>/package.json carries a structured encoreos block:
npm run audit:package-boundaries walks every package and verifies:
| Rule | Check |
|---|---|
| Cores never depend on another core | No @encoreos/core-* in another core’s dependencies |
| Platform never depends on a core | No @encoreos/core-* in @encoreos/platform’s dependencies |
| Shared depends on neither platform nor any core | No @encoreos/platform or @encoreos/core-* in @encoreos/shared’s dependencies |
| CL is downstream | No package declares a dep on @encoreos/core-cl (it carries encoreos.downstreamOnly: true) |
At the import-statement layer (unchanged)
scripts/utils/check-architecture.js continues to scan every .ts/.tsx file for cross-core imports, lazy-loading, loading states, and auth patterns. It still runs as the blocking Architecture boundaries step in .github/workflows/build.yml. The two layers are complementary:
- Package metadata catches intent before code is written (“you can’t even declare a dep on another core”).
- Import scan catches accidents in code already written (“someone wrote
import {} from '@/cores/cl/...'insidesrc/cores/hr/”).
What it unlocks immediately
- Turborepo
--filter— Phase-1.A’sturbo.jsonalready declarestypecheck,lint:ci,test:unit,buildtasks. Now that workspaces exist, those tasks can be filtered: npm ls --workspaces --depth=0— instant view of declared dependency edges.- Per-package
package.jsonevolution — when a core needs a new dependency, it declares it locally (closer to the consumer). The rootpackage.jsondoesn’t grow unbounded. - Codegen / scaffolding hooks can target packages individually instead of grepping
src/cores/.
What it does NOT do (yet)
- Per-package
tsc --build. TS project references withcomposite: truewould lettsc --buildskip untouched packages, butcompositerequires emitting declaration files; our app usesnoEmit: true. A future ADR can switch to a hybrid model (composite for downstream consumers; noEmit for the entry app), but that’s out of scope here. - Per-package Vite build. Vite still bundles
src/main.tsxas one app. That’s the right behavior until Phase 3 (Microfrontends) splits zones. - Physical file moves.
src/cores/hr/...is still where HR’s source lives. - eslint-plugin-boundaries. Phase 1.C remains deferred. The structural audit + the existing import-layer audit cover the same ground without adding to the current
lint:ciwarning budget (--max-warnings 1100inpackage.json).
Migrating to a full physical layout (Phase 2.B, deferred)
When the team is ready, the migration is mechanical and per-core:git mv src/cores/hr/** packages/core-hr/src/**- Update
packages/core-hr/package.jsonencoreos.sourceDirectory→./src - Add
packages/core-hr/tsconfig.jsonextending the root, withcomposite: true,outDir: dist,include: ["src/**/*"]. - Update root
tsconfig.app.jsonpaths: change"@/cores/hr/*": ["./src/cores/hr/*"]to"@/cores/hr/*": ["./packages/core-hr/src/*"]. - Add
packages/core-hrto roottsconfig.references.jsonreferences list. - Run
npm run typecheck && npm run lint:ci && npm run test:unit && npm run build. Land that core’s PR. - Repeat for the next core.
lo → fm → it → ce → rh → cl → gr → fw → fa → pm → hr
Each step is its own PR. The overlay model in this PR ensures the steps are independent and reversible.
Operational notes
npm install is now idempotent across packages
When younpm install, npm hoists shared dependencies to the root node_modules and creates symlinks under node_modules/@encoreos/ for each workspace package. The peer-dep flag (--legacy-peer-deps) is still required as documented in AGENTS.md.
Adding a new core (future)
mkdir packages/core-{slug}- Update the
CORESlist inscripts/dev/generate-core-package-jsons.mjs. npm run packages:regenerate-coreswrites the newpackages/core-{slug}/package.json.npm installto wire workspaces.npm run audit:package-boundariesto verify.
Removing a core (future)
- Delete
packages/core-{slug}/ - Remove from the
CORESlist in the generator. npm install.
Why encoreos.sourceDirectory and not main / exports?
We don’t want the packages to be consumable via import {} from '@encoreos/core-hr'. That would bypass the existing @/cores/hr/... alias system and create a parallel import path everyone has to keep in sync. The encoreos.sourceDirectory field is metadata only — read by audit-package-boundaries.ts and by humans, but invisible to bundlers. Vite continues to resolve @/cores/hr/foo via the existing path alias in tsconfig.app.json and vite.config.ts.
See also
packages/*/package.json— the actual package definitionsscripts/audit/audit-package-boundaries.tsscripts/dev/generate-core-package-jsons.mjsscripts/utils/check-architecture.js— the existing import-scan auditor (unchanged)docs/recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md§ 5.4 — the original Phase-2 plan (lands via PR #137)docs/architecture/decisions/ADR-016-workspace-packages-overlay.md— formal ADR for this design choice- Constitution §1 (architecture and module boundaries)
docs/development/TURBOREPO_USAGE.md— Phase 1.A companion (lands via PR #139)