Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Status: Active Owner: Platform Architecture Phase: 2 of 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-publicTurborepo / Vercel Microfrontends keys only (placeholder packages/* with no encoreos tier metadata; see MICROFRONTENDS_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.json references its source under src/<scope>/<slug>/ via the encoreos.sourceDirectory field. Existing @/cores/hr/... imports keep working unchanged.
  • Each package’s package.json declares what it is allowed to depend on. npm run audit:package-boundaries enforces the constitution at the package-metadata layer:
    • Cores depend only on @encoreos/platform and @encoreos/shared.
    • @encoreos/platform depends on @encoreos/shared only.
    • @encoreos/shared depends on neither platform nor any core.
    • CL is downstream: no other core may declare a dep on @encoreos/core-cl (constitution §1.4).
  • This unlocks Turborepo --filter so a PR touching one core can turbo 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 moved0~8,300
Imports rewritten0~10,000+
PRs required112+ (one per core)
Risk to live featuresnear-zeromeaningful
Architecture lint coveragepackage-metadata layer (audit:package-boundaries) + import layer (check-architecture.js, unchanged)same, plus TS project boundaries
Turborepo --filter worksyesyes
tsc --build per-package incrementalno (single TS project, ~8.4 s warm)yes (~1 s per untouched core)
Reversibletrivial — delete packages/<name>/invasive
Phase 2 ships the overlay first because it captures most of the velocity win at none of the migration risk. A future Phase 2.B can still do the file moves once the overlay has lived in prod long enough to expose any tooling edge-cases.

Layout

packages/
  encoreos-app/
    package.json                 # name: "encoreos-app" — microfrontends / Turborepo key only (no src)
  encoreos-public/
    package.json                 # name: "encoreos-public" — microfrontends / Turborepo key only (no src)
  platform/
    package.json                 # name: "@encoreos/platform"
                                 # encoreos.sourceDirectory: "../../src/platform"
  shared/
    package.json                 # name: "@encoreos/shared"
                                 # encoreos.sourceDirectory: "../../src/shared"
  core-ce/
    package.json                 # name: "@encoreos/core-ce"; deps on platform+shared
  core-cl/
    package.json                 # name: "@encoreos/core-cl"; deps on platform+shared;
                                 # encoreos.downstreamOnly: true (constitution §1.4)
  core-fa/  ...                  # 11 cores total
  docs/                          # pre-existing Docusaurus site; NOT in workspaces glob

src/                             # source files unchanged; this is what packages reference
  cores/{ce,cl,...,rh}/
  platform/
  shared/
The root package.json declares:
{
  "workspaces": [
    "packages/encoreos-app",
    "packages/encoreos-public",
    "packages/platform",
    "packages/shared",
    "packages/core-*"
  ]
}
Note: 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)

Every packages/<name>/package.json carries a structured encoreos block:
{
  "name": "@encoreos/core-hr",
  "encoreos": {
    "tier": "core",            // "platform" | "shared" | "core"
    "coreSlug": "hr",
    "sourceDirectory": "../../src/cores/hr",
    "constitutionRefs": ["§1.2", "§1.3", "§1.4"]
  },
  "dependencies": {
    "@encoreos/platform": "*", // npm workspaces use literal '*'
    "@encoreos/shared": "*"
  }
}
npm run audit:package-boundaries walks every package and verifies:
RuleCheck
Cores never depend on another coreNo @encoreos/core-* in another core’s dependencies
Platform never depends on a coreNo @encoreos/core-* in @encoreos/platform’s dependencies
Shared depends on neither platform nor any coreNo @encoreos/platform or @encoreos/core-* in @encoreos/shared’s dependencies
CL is downstreamNo 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/...' inside src/cores/hr/”).

What it unlocks immediately

  • Turborepo --filter — Phase-1.A’s turbo.json already declares typecheck, lint:ci, test:unit, build tasks. Now that workspaces exist, those tasks can be filtered:
    # Only run typecheck for HR core and its dependencies
    npx turbo run typecheck --filter=@encoreos/core-hr
    
    # Affected-since-prod (PR-shaped invocation)
    npx turbo run typecheck lint:ci --filter=...[origin/prod]
    
  • npm ls --workspaces --depth=0 — instant view of declared dependency edges.
  • Per-package package.json evolution — when a core needs a new dependency, it declares it locally (closer to the consumer). The root package.json doesn’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 with composite: true would let tsc --build skip untouched packages, but composite requires emitting declaration files; our app uses noEmit: 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.tsx as 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:ci warning budget (--max-warnings 1100 in package.json).

Migrating to a full physical layout (Phase 2.B, deferred)

When the team is ready, the migration is mechanical and per-core:
  1. git mv src/cores/hr/** packages/core-hr/src/**
  2. Update packages/core-hr/package.json encoreos.sourceDirectory./src
  3. Add packages/core-hr/tsconfig.json extending the root, with composite: true, outDir: dist, include: ["src/**/*"].
  4. Update root tsconfig.app.json paths: change "@/cores/hr/*": ["./src/cores/hr/*"] to "@/cores/hr/*": ["./packages/core-hr/src/*"].
  5. Add packages/core-hr to root tsconfig.references.json references list.
  6. Run npm run typecheck && npm run lint:ci && npm run test:unit && npm run build. Land that core’s PR.
  7. Repeat for the next core.
Order of migration (smallest first to validate the recipe): 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 you npm 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)

  1. mkdir packages/core-{slug}
  2. Update the CORES list in scripts/dev/generate-core-package-jsons.mjs.
  3. npm run packages:regenerate-cores writes the new packages/core-{slug}/package.json.
  4. npm install to wire workspaces.
  5. npm run audit:package-boundaries to verify.

Removing a core (future)

  1. Delete packages/core-{slug}/
  2. Remove from the CORES list in the generator.
  3. 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