Planning artifacts (milestones v1.0-v1.2.1, v1.3 queue, PROJECT.md, STATE.md, config) now live alongside the code they describe. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
333 lines
16 KiB
Markdown
333 lines
16 KiB
Markdown
# Architecture Patterns: Display-Layer Fork Isolation
|
|
|
|
**Domain:** TypeScript monorepo fork (Paperclip → Nexus)
|
|
**Researched:** 2026-03-30
|
|
**Confidence:** HIGH — based on direct codebase inspection + verified patterns
|
|
|
|
---
|
|
|
|
## Recommended Architecture
|
|
|
|
The core constraint is: **every file Nexus touches is a potential rebase conflict site.**
|
|
The architecture goal is therefore to minimize the number of upstream files modified by concentrating all fork-specific content into new files that upstream will never create.
|
|
|
|
### Isolation Strategy: Minimal-Touch with Fork Overlay
|
|
|
|
```
|
|
Upstream files Fork overlay files
|
|
───────────── ──────────────────
|
|
constants.ts → [keep AGENT_ROLE_LABELS.ceo = "CEO", change display via wrapper]
|
|
CompanyRail.tsx → (modify inline — unavoidable, low risk)
|
|
OnboardingWizard → nexus/OnboardingWizard.nexus.tsx (new file, rewire import)
|
|
onboard.ts (CLI) → modify inline strings only (no logic change)
|
|
SOUL.md / AGENTS → replace file content (same path, different content)
|
|
```
|
|
|
|
**Two categories of change, each with a different isolation strategy:**
|
|
|
|
| Category | Strategy | Conflict Risk |
|
|
|----------|----------|--------------|
|
|
| New files added by Nexus | Add-only (upstream never touches these) | Zero |
|
|
| Upstream files with string changes | Inline edit, minimal diff | Low — strings rarely conflict |
|
|
| Upstream files requiring logic changes | Wrapper/replacement file, rewire import | Medium — requires Vite alias or import swap |
|
|
|
|
---
|
|
|
|
## Component Boundaries
|
|
|
|
| Component | Responsibility | Fork Change Type |
|
|
|-----------|---------------|-----------------|
|
|
| `ui/src/lib/nexus-labels.ts` | Central display-string registry (NEW file) | New file — zero conflict risk |
|
|
| `ui/src/components/OnboardingWizard.tsx` | Multi-step first-run UX | Inline rewrite — file is owned by Nexus entirely |
|
|
| `packages/shared/src/constants.ts` | `AGENT_ROLE_LABELS` map | Inline string change only — change `ceo: "CEO"` to `ceo: "Project Manager"` |
|
|
| `ui/src/pages/Companies.tsx` | "New Company" button, "Companies" breadcrumb | Inline string change — 2-3 occurrences |
|
|
| `cli/src/commands/onboard.ts` | Terminal output strings | Inline string change — no logic change |
|
|
| `server/src/onboarding-assets/ceo/` | PM agent template content | File content replacement — same paths |
|
|
| `server/src/home-paths.ts` | `.paperclip` → `.nexus` home dir | Inline constant change — single string |
|
|
| `ui/src/components/CompanyRail.tsx` | Sidebar rail icon (`Paperclip` lucide icon) | Single import swap |
|
|
|
|
---
|
|
|
|
## Isolation Pattern 1: Central Label Registry (New File)
|
|
|
|
Create `ui/src/lib/nexus-labels.ts` as a new file. This file is pure Nexus — upstream will never create it, so it can never conflict.
|
|
|
|
```typescript
|
|
// ui/src/lib/nexus-labels.ts [NEXUS-OWNED FILE]
|
|
// Central display vocabulary. Never referenced by upstream.
|
|
// All UI components import from here instead of hardcoding strings.
|
|
|
|
export const NEXUS_LABELS = {
|
|
// Entity names
|
|
workspace: "Workspace",
|
|
workspaces: "Workspaces",
|
|
projectManager: "Project Manager",
|
|
owner: "Owner",
|
|
|
|
// Actions
|
|
addAgent: "Add Agent",
|
|
removeAgent: "Remove Agent",
|
|
|
|
// Onboarding
|
|
onboardingRootPrompt: "Choose your root directory",
|
|
onboardingTitle: "Welcome to Nexus",
|
|
|
|
// App identity
|
|
appName: "Nexus",
|
|
cliCommand: "nexus",
|
|
} as const;
|
|
```
|
|
|
|
**Usage pattern in existing components:** Import `NEXUS_LABELS` and replace the hardcoded string. The diff in the upstream file is minimal — a one-line import addition and a string substitution.
|
|
|
|
**Conflict profile:** The import addition is a single new line at the top of the file. String substitutions are isolated to specific JSX attributes. These lines are unlikely to be touched by upstream changes because upstream will not add an import from `nexus-labels`.
|
|
|
|
---
|
|
|
|
## Isolation Pattern 2: Inline String Replacement (Low-Conflict Edits)
|
|
|
|
For files with a small number of hardcoded display strings, edit inline with targeted changes. Prefix all changed lines with a `// [nexus]` comment on the preceding line so they are trivially identified during rebase conflict resolution.
|
|
|
|
**Example — `packages/shared/src/constants.ts` line 53:**
|
|
```typescript
|
|
// [nexus] display label override
|
|
ceo: "Project Manager",
|
|
```
|
|
|
|
**Example — `ui/src/pages/Companies.tsx` line 72:**
|
|
```typescript
|
|
// [nexus] breadcrumb rename
|
|
setBreadcrumbs([{ label: "Workspaces" }]);
|
|
```
|
|
|
|
**Example — `ui/src/pages/Companies.tsx` line 96:**
|
|
```typescript
|
|
// [nexus] button rename
|
|
New Workspace
|
|
```
|
|
|
|
The `// [nexus]` marker serves three purposes:
|
|
1. Identifies fork-owned lines during `git diff` triage
|
|
2. Signals to the developer during a rebase conflict which side is Nexus vs upstream
|
|
3. Enables `grep -r '\[nexus\]'` to produce a complete inventory of modified lines at any time
|
|
|
|
---
|
|
|
|
## Isolation Pattern 3: File Content Replacement (Onboarding Assets)
|
|
|
|
The `server/src/onboarding-assets/ceo/` files (SOUL.md, AGENTS.md, HEARTBEAT.md, TOOLS.md) are plain prose. They have no code entanglement. Replace their content entirely.
|
|
|
|
**Strategy:** Keep the same file paths. Write Nexus-specific content. Upstream changes to these files will produce conflicts, but:
|
|
- Upstream changes to `ceo/SOUL.md` are relatively rare (onboarding prose is stable)
|
|
- When conflicts occur, resolution is manual prose review — not code logic
|
|
- The directory itself is not renamed (`ceo/` stays `ceo/`) to avoid path-level conflicts
|
|
|
|
**PM and Engineer templates:** Add new template subdirectories under `server/src/onboarding-assets/`:
|
|
- `server/src/onboarding-assets/pm/` — new directory, zero conflict risk
|
|
- `server/src/onboarding-assets/engineer/` — new directory, zero conflict risk
|
|
|
|
---
|
|
|
|
## Isolation Pattern 4: Build-Time File Swap via Vite Alias (High-Complexity Components)
|
|
|
|
For components that require substantial structural changes (primarily `OnboardingWizard.tsx`), use Vite's `resolve.alias` to swap the import at build time. This keeps the upstream file untouched.
|
|
|
|
**Existing Vite config** (`ui/vite.config.ts`) already uses `resolve.alias`:
|
|
```typescript
|
|
resolve: {
|
|
alias: {
|
|
"@": path.resolve(__dirname, "./src"),
|
|
lexical: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"),
|
|
},
|
|
},
|
|
```
|
|
|
|
**Add a Nexus override alias:**
|
|
```typescript
|
|
// ui/vite.config.ts [nexus]
|
|
resolve: {
|
|
alias: {
|
|
"@": path.resolve(__dirname, "./src"),
|
|
lexical: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"),
|
|
// [nexus] component overrides
|
|
"@/components/OnboardingWizard": path.resolve(
|
|
__dirname, "./src/nexus/OnboardingWizard.tsx"
|
|
),
|
|
},
|
|
},
|
|
```
|
|
|
|
**New file:** `ui/src/nexus/OnboardingWizard.tsx` — entirely Nexus-owned, never conflicts.
|
|
|
|
**Upstream file:** `ui/src/components/OnboardingWizard.tsx` — left unmodified. Any upstream updates to it are absorbed without conflict because the alias bypasses it.
|
|
|
|
**Tradeoff:** This pattern is only worth the complexity for large rewrites (100+ lines changed). For small string changes, inline edits are lower overhead. Apply to `OnboardingWizard.tsx` only.
|
|
|
|
**Confidence:** HIGH — Vite alias file swapping is a documented pattern used in white-label React apps. The existing config already demonstrates the alias syntax.
|
|
|
|
---
|
|
|
|
## Isolation Pattern 5: Home Directory Pointer Mechanism
|
|
|
|
The `~/.nexus` pointer file is Nexus-specific infrastructure. The approach:
|
|
|
|
1. Modify `server/src/home-paths.ts` — change the single default string `".paperclip"` to `".nexus"`. This is a one-line change; conflict risk is minimal because upstream rarely changes default paths.
|
|
|
|
2. Create `~/.nexus` as a single-line text file containing the root path. This is runtime data, not code.
|
|
|
|
3. The `PAPERCLIP_HOME` env var override still works — Nexus does not rename it (display-only constraint).
|
|
|
|
**Inline change in `server/src/home-paths.ts`:**
|
|
```typescript
|
|
// [nexus] home dir rename
|
|
const DEFAULT_HOME = ".nexus";
|
|
```
|
|
|
|
---
|
|
|
|
## Data Flow: How Changes Propagate
|
|
|
|
```
|
|
nexus-labels.ts (NEW)
|
|
└── imported by: Companies.tsx, CompanyRail.tsx, InstanceSidebar.tsx, etc.
|
|
└── display strings centralized — upstream files only gain one import line
|
|
|
|
constants.ts (MODIFIED, minimal)
|
|
└── AGENT_ROLE_LABELS.ceo = "Project Manager"
|
|
└── used by: AgentConfigForm.tsx, NewAgent.tsx, ApprovalPayload.tsx
|
|
└── no other changes needed in those files
|
|
|
|
OnboardingWizard.nexus.tsx (NEW)
|
|
└── aliased via vite.config.ts (one-line alias addition)
|
|
└── upstream OnboardingWizard.tsx untouched
|
|
|
|
onboarding-assets/ceo/*.md (MODIFIED content, same paths)
|
|
└── loaded by default-agent-instructions.ts (unchanged)
|
|
|
|
onboarding-assets/pm/ (NEW directory)
|
|
onboarding-assets/engineer/ (NEW directory)
|
|
└── loaded by new template selector in OnboardingWizard.nexus.tsx
|
|
```
|
|
|
|
---
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
### Anti-Pattern 1: Renaming Upstream Files or Directories
|
|
|
|
**What:** Renaming `CompanyRail.tsx` → `WorkspaceRail.tsx`, or `onboarding-assets/ceo/` → `onboarding-assets/pm/`
|
|
**Why bad:** Git tracks renames as delete + add. During `git rebase upstream/master`, if upstream makes changes to `CompanyRail.tsx`, the patch will not apply to `WorkspaceRail.tsx`. You get an unresolved conflict that requires manual merge of the upstream diff into the renamed file.
|
|
**Instead:** Keep all upstream file paths. Use wrapper files or content replacement. Reserve new names for new files only.
|
|
|
|
### Anti-Pattern 2: Renaming TypeScript Identifiers in Upstream Files
|
|
|
|
**What:** Renaming `companyService` → `workspaceService`, `CompanyContext` → `WorkspaceContext`
|
|
**Why bad:** Any upstream commit touching those files produces a merge conflict on every renamed symbol. The conflict surface grows proportionally to how many usages exist (currently hundreds of import sites).
|
|
**Instead:** Leave all identifiers unchanged. The mapping from internal name to display name happens in `nexus-labels.ts` and the `AGENT_ROLE_LABELS` constant only.
|
|
|
|
### Anti-Pattern 3: Squashing All Nexus Commits
|
|
|
|
**What:** Maintaining Nexus changes as a single squashed "fork" commit
|
|
**Why bad:** During `git rebase upstream/master`, all conflicts appear in one commit resolution session, making them impossible to isolate. A single upstream change to `constants.ts` forces you to re-resolve every Nexus change in that file simultaneously.
|
|
**Instead:** Keep one atomic `[nexus]` commit per change area (labels, onboarding, home dir, templates). Small commits rebase cleanly. Conflicts are isolated.
|
|
|
|
### Anti-Pattern 4: Package Name Renames
|
|
|
|
**What:** `@paperclipai/shared` → `@nexusai/shared`
|
|
**Why bad:** Every upstream file that imports from `@paperclipai/*` will conflict because Nexus has rewritten the import path. This is effectively every file in the monorepo.
|
|
**Instead:** Keep all `@paperclipai/*` package names. This is explicitly in scope as "out of scope" in PROJECT.md.
|
|
|
|
### Anti-Pattern 5: Centralizing All Changes in One File
|
|
|
|
**What:** Putting all Nexus overrides in `constants.ts` or `App.tsx`
|
|
**Why bad:** High-traffic upstream files accumulate the most conflicts. Concentrating fork changes there maximizes conflict exposure.
|
|
**Instead:** Prefer adding new files (zero conflict risk) over modifying high-traffic upstream files.
|
|
|
|
---
|
|
|
|
## Scalability Considerations
|
|
|
|
| Concern | Now (v1) | Future upstream rebases |
|
|
|---------|----------|------------------------|
|
|
| Label changes | 1 constants.ts edit + nexus-labels.ts | nexus-labels.ts never conflicts; constants.ts conflict is isolated to 1 line |
|
|
| Onboarding | OnboardingWizard aliased via Vite | Upstream OnboardingWizard changes ignored automatically |
|
|
| Template content | ceo/ files replaced in-place | Manual prose merge if upstream edits ceo/ — rare |
|
|
| New upstream entities | Zero action needed | New upstream files have no Nexus edits |
|
|
| New Nexus features | Add to nexus/ directory | Zero conflict risk — new files only |
|
|
|
|
---
|
|
|
|
## Implementation Order (Least to Most Conflict Risk)
|
|
|
|
This order ensures each phase can be validated and rebased independently before the next phase adds more change surface.
|
|
|
|
### Phase 1 — Foundation (zero upstream file changes)
|
|
1. Create `ui/src/nexus/` directory
|
|
2. Create `ui/src/lib/nexus-labels.ts` with full label registry
|
|
3. Create `server/src/onboarding-assets/pm/` and `engineer/` template directories
|
|
4. Add `[nexus]` commit: "add nexus overlay directory and label registry"
|
|
|
|
**Why first:** Establishes the containment structure with no upstream file touches. Safe to rebase at any point.
|
|
|
|
### Phase 2 — Constants and Labels (1 upstream file, 1-line change)
|
|
1. Edit `packages/shared/src/constants.ts` — change `ceo: "CEO"` to `ceo: "Project Manager"` in `AGENT_ROLE_LABELS`
|
|
2. Add `[nexus]` commit: "rename CEO display label to Project Manager"
|
|
|
|
**Why second:** Single file, single line. Easiest conflict to resolve if upstream touches the same line.
|
|
|
|
### Phase 3 — Home Directory (1 upstream file, 1-line change)
|
|
1. Edit `server/src/home-paths.ts` — change default home dir string to `.nexus`
|
|
2. Edit `cli/src/config/home.ts` — same change
|
|
3. Add `[nexus]` commit: "change default home dir from .paperclip to .nexus"
|
|
|
|
**Why third:** Low-risk lines. Home dir defaults are very rarely changed by upstream.
|
|
|
|
### Phase 4 — UI String Renames (several upstream files, inline strings only)
|
|
1. Edit `ui/src/pages/Companies.tsx` — rename "Companies" breadcrumb and "New Company" button to "Workspaces" / "New Workspace"
|
|
2. Edit `ui/src/components/CompanyRail.tsx` — swap `Paperclip` lucide icon for a different icon
|
|
3. Edit `ui/src/pages/CompanySettings.tsx`, `InstanceSidebar.tsx` — display-string renames
|
|
4. Edit `cli/src/commands/onboard.ts` — terminal output strings
|
|
5. One `[nexus]` commit per file changed
|
|
|
|
**Why fourth:** More files touched, but changes are string-only. Each commit is independently rebaseable. `// [nexus]` markers make conflict resolution mechanical.
|
|
|
|
### Phase 5 — Onboarding Redesign (Vite alias + new file)
|
|
1. Add Vite alias in `ui/vite.config.ts` pointing `OnboardingWizard` to `nexus/OnboardingWizard.tsx`
|
|
2. Write `ui/src/nexus/OnboardingWizard.tsx` as a full replacement (root dir picker, PM + Engineer auto-create)
|
|
3. Replace `server/src/onboarding-assets/ceo/` file content with PM-framed prose
|
|
4. One `[nexus]` commit: "redesign onboarding for single-dev workspace flow"
|
|
|
|
**Why last:** Most complex change. The Vite alias approach means upstream `OnboardingWizard.tsx` can evolve freely without conflicting. Template content is the highest natural-language conflict risk but lowest structural risk.
|
|
|
|
---
|
|
|
|
## Rebase Workflow
|
|
|
|
```bash
|
|
# Pull upstream changes
|
|
git fetch upstream
|
|
git rebase upstream/master
|
|
|
|
# For each [nexus] commit, git will pause on conflicts.
|
|
# Expected conflict files per phase:
|
|
# Phase 2: packages/shared/src/constants.ts (1 line)
|
|
# Phase 3: server/src/home-paths.ts, cli/src/config/home.ts (1 line each)
|
|
# Phase 4: ui/src/pages/*.tsx, cli/src/commands/onboard.ts (string lines)
|
|
# Phase 5: server/src/onboarding-assets/ceo/*.md (prose), ui/vite.config.ts (1 line)
|
|
#
|
|
# Resolution rule: keep [nexus] version for any line marked // [nexus]
|
|
# accept upstream for everything else
|
|
|
|
# After rebase, verify no Nexus labels reverted:
|
|
grep -r '\[nexus\]' /Volumes/UsbNvme/repos/nexus --include="*.ts" --include="*.tsx"
|
|
```
|
|
|
|
---
|
|
|
|
## Sources
|
|
|
|
- Codebase inspection: `/Volumes/UsbNvme/repos/nexus/` (direct analysis, HIGH confidence)
|
|
- Vite resolve.alias documentation: https://vite.dev/config/shared-options (HIGH confidence)
|
|
- White-label file-swap pattern: https://krasimirtsonev.com/blog/article/whitelabel-react-apps (MEDIUM confidence — describes Webpack, pattern is equivalent in Vite)
|
|
- Fork rebase best practices: https://joaquimrocha.com/2024/09/22/how-to-fork/ (MEDIUM confidence)
|
|
- Atomic commit strategy for forks: https://medium.com/@ruthmpardee/git-fork-workflow-using-rebase-587a144be470 (MEDIUM confidence)
|