1 changed files with 551 additions and 0 deletions
@ -0,0 +1,551 @@ |
|||
# Monorepo migration implementation brief |
|||
|
|||
> **Status:** Proposed target architecture, not yet implemented. This document is a handoff for a |
|||
> fresh implementation session. Until that migration is complete, `docs/ARCHITECTURE.md` describes |
|||
> the repository's actual structure. |
|||
|
|||
## Objective |
|||
|
|||
Convert `webavatar-core` from a SvelteKit template whose reusable modules live inside the app into an |
|||
npm-workspaces monorepo with independently versioned, privately distributed packages: |
|||
|
|||
- `@<scope>/webavatar-core` — reusable shared, browser, server, and Vite integration. |
|||
- `@<scope>/webavatar-humanoid` — optional humanoid state and animation functionality. |
|||
- `apps/template` — the SvelteKit application template, demo surfaces, project-owned configuration, |
|||
and composition roots. |
|||
|
|||
The packages must be normal npm-format packages, but they must not be published to the public |
|||
npmjs.com registry. Configure them for a private registry (GitHub Packages is the assumed default; |
|||
substitute the actual internal registry if different). |
|||
|
|||
This migration is about packaging, ownership, and build boundaries. Preserve existing runtime |
|||
behavior and wire protocols unless a change is specifically required to establish a package API. |
|||
|
|||
## Why this change |
|||
|
|||
The current directory convention reduces conflicts between `core`, `project`, and `humanoid`, but |
|||
client projects still have to merge the entire template branch to receive a reusable subsystem |
|||
update. Versioned packages will let a project upgrade core and humanoid independently, receive an |
|||
explicit compatibility error when versions do not match, and avoid merging unrelated routes, |
|||
deployment files, demos, or project content. |
|||
|
|||
The monorepo retains the important development property that core, humanoid, and the reference app |
|||
can be changed and tested together. |
|||
|
|||
## Non-goals |
|||
|
|||
- Do not publish anything publicly. |
|||
- Do not redesign the realtime protocol, tool execution, analytics, logging behavior, or animation |
|||
algorithms as part of the move. |
|||
- Do not make Three.js, the demo avatar, or any renderer part of the humanoid package. |
|||
- Do not make project persona, project tools, UI, or deployment policy part of core. |
|||
- Do not add a global service locator or global `configureHumanoid()` API. |
|||
- Do not preserve old `$lib/core` or `$lib/humanoid` import paths with permanent compatibility |
|||
aliases. Update consumers to the package API. |
|||
- Do not extract every small concern into its own package during the first migration. |
|||
|
|||
## Preconditions for the implementation session |
|||
|
|||
1. Read `AGENTS.md` and the relevant subsystem documentation completely. |
|||
2. Inspect `git status` before editing. At the time this brief was written, the worktree contained |
|||
unrelated changes in `docs/ANALYTICS.md`, `src/lib/server/core/session.ts`, and |
|||
`src/lib/server/core/session.spec.ts`. Preserve all user changes and do not fold them into the |
|||
migration accidentally. |
|||
3. Record a green baseline with the existing `check`, unit-test, lint, and build commands. If the |
|||
baseline is not green, distinguish pre-existing failures from migration regressions. |
|||
4. Decide the private package scope, replacing every `@<scope>` placeholder below. Do not invent a |
|||
scope if the repository or organization configuration does not reveal it. |
|||
|
|||
## Target repository structure |
|||
|
|||
```text |
|||
webavatar/ |
|||
├── package.json |
|||
├── package-lock.json |
|||
├── tsconfig.base.json |
|||
├── eslint.config.js |
|||
├── prettier.config.js |
|||
├── docs/ |
|||
├── packages/ |
|||
│ ├── core/ |
|||
│ │ ├── package.json |
|||
│ │ ├── tsconfig.json |
|||
│ │ ├── README.md |
|||
│ │ └── src/ |
|||
│ │ ├── index.ts |
|||
│ │ ├── protocol.ts |
|||
│ │ ├── logger.ts |
|||
│ │ ├── tools.ts |
|||
│ │ ├── client/ |
|||
│ │ │ └── index.ts |
|||
│ │ ├── server/ |
|||
│ │ │ └── index.ts |
|||
│ │ └── vite/ |
|||
│ │ └── index.ts |
|||
│ └── humanoid/ |
|||
│ ├── package.json |
|||
│ ├── tsconfig.json |
|||
│ ├── README.md |
|||
│ └── src/ |
|||
│ ├── index.ts |
|||
│ ├── client/ |
|||
│ │ └── index.ts |
|||
│ └── eye/ |
|||
│ ├── index.ts |
|||
│ └── client.ts |
|||
└── apps/ |
|||
└── template/ |
|||
├── package.json |
|||
├── vite.config.ts |
|||
├── server.js |
|||
├── tsconfig.json |
|||
├── src/ |
|||
│ ├── lib/project/ |
|||
│ ├── lib/server/project/ |
|||
│ ├── lib/client/project/ |
|||
│ └── routes/ |
|||
└── static/ |
|||
``` |
|||
|
|||
Exact internal filenames may stay close to the current layout. The important change is that package |
|||
boundaries become real `package.json` and `exports` boundaries, not merely directory conventions. |
|||
|
|||
## Ownership and dependency graph |
|||
|
|||
The allowed graph is: |
|||
|
|||
```text |
|||
apps/template ──────────────▶ @<scope>/webavatar-core |
|||
│ ▲ |
|||
└────────▶ @<scope>/webavatar-humanoid |
|||
|
|||
@<scope>/webavatar-humanoid ───────────────┘ |
|||
contracts only |
|||
``` |
|||
|
|||
Rules: |
|||
|
|||
1. Core imports neither humanoid nor app/project code. |
|||
2. Humanoid may import only public, portable core contracts. It must not import core server or |
|||
browser implementations. |
|||
3. The template app may import public exports from both packages. |
|||
4. Cross-package imports use package names. Relative imports never cross a package boundary. |
|||
5. Neither package may use SvelteKit aliases such as `$lib`, `$app`, or `$env`. |
|||
6. Project code remains in `apps/template/src/lib/**/project` and may depend on either package. |
|||
7. Renderer and avatar-model knowledge remains in the app. |
|||
|
|||
Add lint rules that enforce these directions. Documentation alone is insufficient. |
|||
|
|||
## Package 1: `@<scope>/webavatar-core` |
|||
|
|||
### Responsibilities |
|||
|
|||
Move the reusable code currently under these paths into this package: |
|||
|
|||
- `src/lib/core/**` |
|||
- `src/lib/client/core/**`, except the Svelte debug-panel component described below |
|||
- `src/lib/server/core/**` |
|||
|
|||
The route endpoint that mints a session token stays in the app because routing is an application |
|||
concern; it imports token functionality from `@<scope>/webavatar-core/server`. |
|||
|
|||
Move the `SessionPersona` interface out of project code and into a portable core contract. The |
|||
current type-only imports from `server/core` to `project/persona.ts` violate the intended dependency |
|||
direction even though TypeScript erases them at runtime. The project should export a value that |
|||
`satisfies SessionPersona`. |
|||
|
|||
### Public entry points |
|||
|
|||
Create curated index files and export only supported APIs. A starting manifest shape is: |
|||
|
|||
```json |
|||
{ |
|||
"name": "@<scope>/webavatar-core", |
|||
"version": "0.1.0", |
|||
"type": "module", |
|||
"files": ["dist"], |
|||
"exports": { |
|||
".": { |
|||
"types": "./dist/index.d.ts", |
|||
"import": "./dist/index.js" |
|||
}, |
|||
"./client": { |
|||
"types": "./dist/client/index.d.ts", |
|||
"import": "./dist/client/index.js" |
|||
}, |
|||
"./server": { |
|||
"types": "./dist/server/index.d.ts", |
|||
"import": "./dist/server/index.js" |
|||
}, |
|||
"./vite": { |
|||
"types": "./dist/vite/index.d.ts", |
|||
"import": "./dist/vite/index.js" |
|||
} |
|||
}, |
|||
"publishConfig": { |
|||
"registry": "https://npm.pkg.github.com" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The root entry point is portable and contains contracts such as: |
|||
|
|||
- Protocol events and session types |
|||
- Tool definitions |
|||
- `Logger`, `LogEntry`, `LogLevel`, and `NOOP_LOGGER` |
|||
- `SessionPersona` |
|||
|
|||
The `./client` entry point contains browser transports, resilience, client logging, tool dispatch, |
|||
and server-log forwarding. The `./server` entry point contains Node-only implementations. The |
|||
`./vite` entry point contains the Vite dev-server integration so importing the root does not load |
|||
Vite or server code. |
|||
|
|||
Do not export every internal file as a wildcard. The `exports` map is the compatibility boundary |
|||
that future semantic versions will protect. |
|||
|
|||
### Server-only safety |
|||
|
|||
Moving server code into `node_modules` means its path no longer automatically lives under |
|||
SvelteKit's `$lib/server` directory. In the template app, add a local wrapper: |
|||
|
|||
```ts |
|||
// apps/template/src/lib/server/webavatar-core.ts |
|||
export * from '@<scope>/webavatar-core/server'; |
|||
``` |
|||
|
|||
SvelteKit routes and server modules should normally import through this wrapper. Add lint rules that |
|||
forbid `@<scope>/webavatar-core/server` from client and universal app files. Root-only entrypoints |
|||
such as `server.js` may import the package server entry point directly. |
|||
|
|||
### Debug panel decision |
|||
|
|||
Keep the logger contract and client log store in core. For the first migration, move |
|||
`DebugPanel.svelte` and its component test into the template app as UI that consumes the core client |
|||
logger. Do not make the foundational core build responsible for packaging Svelte components during |
|||
the same migration. If multiple client projects later need the panel, extract it in a separate |
|||
`@<scope>/webavatar-devtools-svelte` package as a follow-up. |
|||
|
|||
## Package 2: `@<scope>/webavatar-humanoid` |
|||
|
|||
### Responsibilities |
|||
|
|||
Move these paths into the package: |
|||
|
|||
- `src/lib/humanoid/**` |
|||
- `src/lib/client/humanoid/**` |
|||
|
|||
Keep these in the template app: |
|||
|
|||
- `src/routes/demo/humanoid/**` |
|||
- `static/models/avatar.glb` |
|||
- Three.js and its type dependency |
|||
- Rig/morph-target binding and all rendering code |
|||
|
|||
The package must remain renderer-agnostic and must not acquire a Svelte dependency. Its hand-rolled |
|||
stores may continue to satisfy the Svelte store contract structurally. |
|||
|
|||
### Core compatibility |
|||
|
|||
Humanoid currently needs core's normalized `ServerEvent` contract. Declare core as a peer dependency |
|||
so the app owns the single compatible core installation: |
|||
|
|||
```json |
|||
{ |
|||
"name": "@<scope>/webavatar-humanoid", |
|||
"version": "0.1.0", |
|||
"type": "module", |
|||
"files": ["dist"], |
|||
"peerDependencies": { |
|||
"@<scope>/webavatar-core": "^0.1.0" |
|||
}, |
|||
"devDependencies": { |
|||
"@<scope>/webavatar-core": "^0.1.0" |
|||
}, |
|||
"exports": { |
|||
".": { |
|||
"types": "./dist/index.d.ts", |
|||
"import": "./dist/index.js" |
|||
}, |
|||
"./client": { |
|||
"types": "./dist/client/index.d.ts", |
|||
"import": "./dist/client/index.js" |
|||
}, |
|||
"./eye": { |
|||
"types": "./dist/eye/index.d.ts", |
|||
"import": "./dist/eye/index.js" |
|||
}, |
|||
"./eye/client": { |
|||
"types": "./dist/eye/client.d.ts", |
|||
"import": "./dist/eye/client.js" |
|||
} |
|||
}, |
|||
"publishConfig": { |
|||
"registry": "https://npm.pkg.github.com" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Use the broadest core peer range actually tested. A core release does not require a humanoid release |
|||
when the existing peer range and APIs remain compatible. |
|||
|
|||
### Interaction with core services |
|||
|
|||
Humanoid may depend on core contracts, not ambient runtime implementations. For logging: |
|||
|
|||
```ts |
|||
import { NOOP_LOGGER, type Logger } from '@<scope>/webavatar-core'; |
|||
|
|||
export interface EyeGazeControllerOptions { |
|||
config?: Partial<EyeConfig>; |
|||
logger?: Logger; |
|||
} |
|||
|
|||
export function createEyeGazeController(options: EyeGazeControllerOptions = {}) { |
|||
const logger = options.logger ?? NOOP_LOGGER; |
|||
// Stateful/effectful wrapper may log under `humanoid.eye`. |
|||
} |
|||
``` |
|||
|
|||
The app supplies the implementation: |
|||
|
|||
```ts |
|||
import { clientLogger } from '@<scope>/webavatar-core/client'; |
|||
import { createEyeGazeController } from '@<scope>/webavatar-humanoid/eye/client'; |
|||
|
|||
const gaze = createEyeGazeController({ |
|||
config: projectEyeConfig, |
|||
logger: clientLogger |
|||
}); |
|||
``` |
|||
|
|||
Apply the same dependency-injection pattern to future clocks, randomness sources, metrics, or other |
|||
runtime facilities. Keep reducers, curves, animation composition, and per-frame math pure; log at |
|||
stateful boundaries rather than on every animation frame. |
|||
|
|||
If dependency threading becomes noisy later, add an instance-based `createHumanoidRuntime({ logger, |
|||
...config })` factory. Do not add mutable package-global configuration because it would make SSR, |
|||
tests, multiple avatars, and concurrent configurations interfere with each other. |
|||
|
|||
## Template app: `apps/template` |
|||
|
|||
The template owns: |
|||
|
|||
- SvelteKit routes and application HTML |
|||
- Project persona and tools |
|||
- `server.js` and `vite.config.ts` composition roots |
|||
- Environment examples and deployment configuration |
|||
- Demo routes and demo assets |
|||
- The Three.js humanoid renderer |
|||
- The debug-panel Svelte component during the first migration |
|||
|
|||
Replace existing internal imports as follows: |
|||
|
|||
| Current import family | Target import | |
|||
| ------------------------- | ----------------------------------------------------- | |
|||
| `$lib/core/**` | `@<scope>/webavatar-core` | |
|||
| `$lib/client/core/**` | `@<scope>/webavatar-core/client` | |
|||
| `$lib/server/core/**` | `$lib/server/webavatar-core` in app source | |
|||
| `$lib/humanoid/**` | `@<scope>/webavatar-humanoid` or `/eye` | |
|||
| `$lib/client/humanoid/**` | `@<scope>/webavatar-humanoid/client` or `/eye/client` | |
|||
| `$lib/**/project/**` | Remains app-local project code | |
|||
|
|||
Prefer importing from curated entry points instead of recreating one export per old source file. If |
|||
two old modules expose names that collide, use a deliberate subpath or rename the public API rather |
|||
than leaking the old filesystem layout wholesale. |
|||
|
|||
The custom production server should import built package output through package names. Once that |
|||
works, remove the app's special `tsconfig.server.json`/`build-server` compilation path if it is no |
|||
longer needed. Confirm that adapter-node deployment still includes the runtime package dependencies. |
|||
|
|||
## Workspace and build design |
|||
|
|||
The root package is never publishable: |
|||
|
|||
```json |
|||
{ |
|||
"name": "webavatar-monorepo", |
|||
"private": true, |
|||
"workspaces": ["packages/*", "apps/*"] |
|||
} |
|||
``` |
|||
|
|||
Use ordinary semver ranges between workspaces so npm links a matching local workspace during |
|||
development and published consumers see valid dependency metadata. Keep one root lockfile. |
|||
|
|||
Both reusable packages should ship compiled ESM JavaScript plus declarations under `dist/`: |
|||
|
|||
```json |
|||
{ |
|||
"scripts": { |
|||
"build": "tsc -p tsconfig.json", |
|||
"check": "tsc -p tsconfig.json --noEmit", |
|||
"test": "vitest run" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Use a root `tsconfig.base.json` for common strictness while letting each package select the runtime |
|||
libraries it needs. Do not let browser code gain Node globals merely because server code exists in |
|||
another package entry point. |
|||
|
|||
Root scripts should build in dependency order: |
|||
|
|||
1. Core |
|||
2. Humanoid |
|||
3. Template app |
|||
|
|||
The root `dev` workflow must build packages once, then watch core and humanoid alongside the SvelteKit |
|||
dev server. A small process runner such as `concurrently` is acceptable. Package exports should point |
|||
to `dist`, not to TypeScript source that only Vite understands, because the custom production Node |
|||
server must consume the same artifact that projects install. |
|||
|
|||
Keep `three` in the template's development dependencies. Keep `ws` and `better-sqlite3` with the core |
|||
server package if they remain required by its exported runtime. Treat Vite as a compatible peer/dev |
|||
dependency for the `./vite` integration rather than allowing an unrelated Vite copy to be pulled |
|||
silently into the app. |
|||
|
|||
## Private publication and versioning |
|||
|
|||
Assuming GitHub Packages, add this committed scope mapping without a token: |
|||
|
|||
```ini |
|||
@<scope>:registry=https://npm.pkg.github.com |
|||
``` |
|||
|
|||
Authentication belongs in developer and CI secrets. Never commit a package token. |
|||
|
|||
Version core and humanoid independently. Start both at `0.1.0` unless existing internal releases |
|||
require another baseline. During `0.x` development, document that minor releases may be breaking; |
|||
after `1.0.0`, use normal semantic-versioning expectations. |
|||
|
|||
Suggested tags: |
|||
|
|||
```text |
|||
core-v0.1.0 |
|||
humanoid-v0.1.0 |
|||
``` |
|||
|
|||
For each release: |
|||
|
|||
1. Update the package version and changelog. |
|||
2. Update dependent peer ranges only if compatibility changed. |
|||
3. Build, test, and inspect `npm pack --dry-run` for that workspace. |
|||
4. Install the produced tarball into a consumer fixture outside the workspace dependency graph. |
|||
5. Publish only the affected workspace to the private registry. |
|||
6. Create the corresponding immutable Git tag and release notes. |
|||
|
|||
Manual versioning is sufficient for the first migration. Introduce Changesets or another release |
|||
orchestrator only after the package boundaries and first private release work end to end. |
|||
|
|||
## Migration sequence |
|||
|
|||
Implement in small, reviewable stages while keeping behavior stable: |
|||
|
|||
### 1. Establish the workspace shell |
|||
|
|||
- Add the private root workspace manifest and shared TypeScript configuration. |
|||
- Create package/app directories and manifests. |
|||
- Keep root linting, formatting, and documentation coverage. |
|||
|
|||
### 2. Extract portable core contracts |
|||
|
|||
- Move protocol, logger, tools, and persona contracts first. |
|||
- Add the core root index and export map. |
|||
- Update imports and tests. |
|||
- Eliminate every core-to-project import, including type-only imports. |
|||
|
|||
### 3. Extract core client, server, and Vite entry points |
|||
|
|||
- Move implementation files without behavioral rewrites. |
|||
- Split public entry points by runtime. |
|||
- Move the debug-panel Svelte UI into the app. |
|||
- Make the package build, test, and pack independently. |
|||
|
|||
### 4. Extract humanoid |
|||
|
|||
- Move pure and client humanoid code. |
|||
- Replace the protocol import with the core package's public contract. |
|||
- Add core as a peer/dev dependency. |
|||
- Add logger injection only at effectful wrappers; use `NOOP_LOGGER` by default. |
|||
- Keep Three.js, GLB assets, and renderer components in the app. |
|||
|
|||
### 5. Move and rewire the template app |
|||
|
|||
- Move the SvelteKit shell under `apps/template`. |
|||
- Convert app imports to package entry points. |
|||
- Update `server.js`, `vite.config.ts`, API routes, project tools, demos, and tests. |
|||
- Remove obsolete standalone compilation plumbing only after package imports work in production. |
|||
|
|||
### 6. Enforce boundaries |
|||
|
|||
- Add restricted-import lint rules for core, humanoid, server-only, and app/project directions. |
|||
- Search for leftover `$lib/core`, `$lib/humanoid`, cross-package relative imports, and package imports |
|||
from app-owned paths. |
|||
|
|||
### 7. Add distribution verification |
|||
|
|||
- Pack both packages. |
|||
- Inspect tarball contents. |
|||
- Install tarballs into an isolated consumer. |
|||
- Test private-registry publication with prerelease versions before declaring the migration done. |
|||
|
|||
### 8. Update documentation |
|||
|
|||
- Rewrite `AGENTS.md` orientation for the workspace layout. |
|||
- Update `docs/ARCHITECTURE.md` to describe actual package boundaries and link this document as the |
|||
completed migration rationale or replace it with a permanent packaging document. |
|||
- Update subsystem docs and all source links/import examples. |
|||
- Add package-local READMEs documenting supported exports and compatibility. |
|||
- Remove the proposal warning at the top of this document only when the implementation is complete. |
|||
|
|||
## Verification requirements |
|||
|
|||
The migration is complete only when all of the following pass from a clean install: |
|||
|
|||
1. Root formatting and lint checks. |
|||
2. Core type-check, unit tests, and build. |
|||
3. Humanoid type-check, unit tests, and build. |
|||
4. Template `svelte-check`, unit tests, and production build. |
|||
5. `npm pack --dry-run` for both packages contains only intended runtime files, declarations, |
|||
package metadata, and documentation. |
|||
6. A consumer outside the workspace can install the packed core and humanoid tarballs and import |
|||
every documented entry point. |
|||
7. Browser smoke test of `/demo/humanoid`, including avatar-state and eye-gaze behavior. |
|||
8. Mock-provider smoke test through the template app. |
|||
9. Actual `/demo/realtime` round trip against OpenAI, including a tool call, because the custom |
|||
dev/production wiring and package boundary changed even if the wire protocol did not. |
|||
10. Production `server.js` boots from built package artifacts rather than workspace source files. |
|||
|
|||
Remember the existing server-core HMR limitation while the old layout exists. After extraction, |
|||
document the new package-watch behavior accurately rather than carrying that warning forward without |
|||
re-testing it. |
|||
|
|||
## Acceptance criteria |
|||
|
|||
- Core and humanoid are independently versioned installable artifacts. |
|||
- The root and template app are private and cannot be published accidentally. |
|||
- No package imports app/project code. |
|||
- Core does not import humanoid. |
|||
- Humanoid reaches core facilities through public contracts and explicit dependency injection. |
|||
- The app is the only composition root that chooses concrete client/server implementations. |
|||
- Server-only imports remain mechanically protected. |
|||
- Pure humanoid animation functions remain pure. |
|||
- The package tarballs do not contain demos, GLB assets, project persona/tools, database files, or |
|||
unrelated repository content. |
|||
- The template behaves the same in development and production as it did before the migration. |
|||
- Documentation describes the final filesystem, imports, release process, and compatibility policy. |
|||
|
|||
## Decisions that must survive implementation |
|||
|
|||
1. **Private packages, not copied source:** downstream projects install versions; they do not merge |
|||
package source from the template branch. |
|||
2. **Independent versions:** core and humanoid can release separately. |
|||
3. **One-way dependency:** humanoid may depend on portable core contracts; core never knows humanoid |
|||
exists. |
|||
4. **Dependency injection:** logger and future runtime services are passed into humanoid wrappers, |
|||
not imported as ambient core singletons. |
|||
5. **App-owned composition:** project persona, tools, routes, rendering, environment policy, and |
|||
deployment wiring stay in the template/project. |
|||
6. **Curated exports:** the public API is smaller and more stable than the internal filesystem. |
|||
7. **Artifact parity:** local development, packed consumers, and the production server use the same |
|||
compiled package outputs. |
|||
Loading…
Reference in new issue