# Canonical Base Patterns Design
**Status:** Revised per Architect Feedback (v4 - Record-Level Upgrade)
**Date:** 2026-01-27
**Author:** Claude (with Alex & Berni)
---
## Executive Summary
Design a set of canonical base patterns using a **Container + Minimal Interface Types + Record-Level Upgrade** approach:
- **Minimal Interface Types**: Simple TypeScript interfaces (`TaskLike`, `PersonLike`, `EventLike`)
- **Container Patterns**: Manage items and list available patterns in "Add" menu (no forking)
- **Record-Level Upgrade**: Fork happens when viewing an individual record and needing more fields
- **compileAndRun Integration**: Use existing builtin to dynamically compile forked patterns
- **Lists of Lists**: Containers can include other containers (e.g., Contacts includes AutoImportedGoogleContacts)
- **Duplication as Edge Case**: Handle same-person-different-context with simple `sameAs`, not complex reconciliation
The core insight: **fork-on-demand happens on individual records, not from the container's add dropdown**. Container lists patterns; records offer upgrade options.
---
## Design Decisions Summary
| Decision | Choice | Rationale |
|----------|--------|-----------|
| **Core Model** | Container + Minimal Interfaces + Record-Level Upgrade | Simple, emergent, addresses actual needs |
| **Container Role** | Manager + Pattern Listing | Container lists PersonLike patterns in "Add" menu (no forking) |
| **Fork Location** | Individual record "Upgrade" menu | Fork when viewing a record and needing more fields |
| **Fork Mechanism** | compileAndRun builtin | Dynamically compile forked pattern, replace record in-place |
| **Type Identity** | Minimal interface conformance | TaskLike, PersonLike, EventLike |
| **Nesting** | Lists of lists | Contacts can include AutoImportedGoogleContacts as a sub-list |
| **Duplication** | Simple `sameAs` field | Treat duplicates as edge case, not core concern |
| **Customization** | Record upgrade + fork | When you need "contractor with billing fields", upgrade the record |
---
## Governing Principles
These principles guide how the pattern ecosystem evolves and how users customize their experience.
### 1. Schelling Points for Interoperability
A **schelling point** is something people converge on naturally without explicit agreement.
**Principle:** The ecosystem needs common reference points that everyone uses, enabling patterns to interoperate without central coordination.
```typescript
// The schelling point - minimal, stable, universal
export interface PersonLike {
name: string; // Just this. Everyone can agree on this.
}
// Everything else is optional variance
interface FamilyMember extends PersonLike {
name: string; // core preserved
relationship: string; // added
birthday?: string; // added
dietaryRestrictions?: string[]; // added
}
```
**Key insight:** The schelling point must be **minimal enough that everyone can adopt it** but **meaningful enough to enable interoperability**. A single field (`name`) is often sufficient.
### 2. Fork-on-Demand (Not Pre-Designed Variants)
**Principle:** Don't pre-design N variant patterns. Fork and modify when you actually need a new type.
**The old approach (too prescriptive):**
```
Pre-define: family-member.tsx, employee.tsx, contact.tsx, friend.tsx
User picks from catalog
```
**The new approach (emergent):**
```
1. User has a Contacts container
2. User needs to track a contractor with billing fields
3. User forks an existing PersonLike pattern
4. User (or LLM) adds the fields they need right then
5. New "contractor" pattern is created just-in-time
```
**Container lists available patterns (no forking here):**
```
┌─────────────────────────────────────────────────┐
│ Contacts (container) │
│ │
│ items: [Person, Person, FamilyMember, ...] │
│ │
│ [+ Add Contact ▼] ← just picks existing types │
│ ├─ Contact │
│ ├─ FamilyMember │
│ ├─ Contractor │
│ └─ ...other PersonLike patterns │
└─────────────────────────────────────────────────┘
```
**Fork happens on individual records:**
```
┌─────────────────────────────────────────────────┐
│ John Smith (Contact) │
│ │
│ name: "John Smith" │
│ email: "john@example.com" │
│ │
│ [Upgrade ▼] │
│ ├─ "Add birthday → FamilyMember" │
│ ├─ "Add hourlyRate → Contractor" │
│ └─ "Fork and customize..." │
└─────────────────────────────────────────────────┘
```
**When you need a new type:**
> While viewing John Smith: "I need to track him as a contractor with hourly rate."
The LLM forks Contact, adds the fields, uses `compileAndRun` to compile it, and replaces John's record in-place with the new Contractor type. His existing data (name, email) is preserved.
### 3. Core Schema Preservation
**Principle:** Variants can add anything, but must preserve the core schema fields.
This is the contract that enables the ecosystem:
- A forked pattern with `{ name, birthday, hourlyRate }` still satisfies `PersonLike`
- Containers aggregating `PersonLike` items work with ALL variants
- Breaking the core schema breaks interoperability
```typescript
// VALID: extends core
interface Contractor extends PersonLike {
name: string; // ✓ core preserved
hourlyRate: number; // added when forked
billingAddress: string; // added when forked
}
// INVALID: breaks core
interface BrokenFriend {
nickname: string; // ✗ renamed 'name' - breaks contract
}
```
### 4. Duplication as Edge Case (Simple `sameAs`)
**Principle:** Treat duplicate entities (same person in different contexts) as an edge case, not a core architectural concern.
**Scenario:**
- `contractor.tsx` (work): `{ name: "John Smith", rate: 150, company: "Acme" }`
- `friend.tsx` (personal): `{ name: "John Smith", birthday: "1985-03-15" }`
These might be the **same person** in two different contexts. But this is unusual, not common.
**Simple solution:** Add a `sameAs` field when needed:
```typescript
// When you discover two records are the same person
contractor.sameAs = friendJohn; // Simple reference
```
**Why not complex reconciliation?**
- Most people don't have massive duplicate problems
- When duplicates occur, it's easy to link them
- Over-engineering identity management adds complexity without proportional benefit
**The 80% case:** Most contacts are distinct. Handle duplicates when they arise, not as core infrastructure.
---
### 5. Lists of Lists
**Principle:** Containers can include other containers as sub-lists, not by copying entries but by including the whole list.
**Example:** Contacts contains AutoImportedGoogleContacts
```
┌─────────────────────────────────────────────────────────┐
│ Contacts │
│ │
│ items: [ │
│ Person("Alice"), │
│ Person("Bob"), │
│ FamilyMember("Mom"), │
│ ] │
│ │
│ sub-lists: [ │
│ ┌─────────────────────────────────────────────┐ │
│ │ AutoImportedGoogleContacts (sub-container) │ │
│ │ items: [ │ │
│ │ GoogleContact("Carol"), │ │
│ │ GoogleContact("Dave"), │ │
│ │ GoogleContact("Eve"), │ │
│ │ ] │ │
│ └─────────────────────────────────────────────┘ │
│ ] │
└─────────────────────────────────────────────────────────┘
```
**Why this matters:**
- Don't flatten everything into one list
- Keep provenance clear (Carol is from Google import)
- Can remove entire import source at once
- Can have multiple import sources as separate sub-lists
- Container shows combined view but maintains structure
**Implementation:**
```typescript
interface ContactsContainer {
items: PersonLike[]; // Directly managed contacts
subLists: ContactsContainer[]; // Nested containers (imports, groups, etc.)
// Computed: all contacts including from sub-lists
allItems: PersonLike[];
}
```
---
## Folksonomy Growth
The ecosystem evolves organically through usage, not top-down design.
### How It Works
```
Time 0: Base patterns exist
┌──────────┐ ┌──────────┐ ┌──────────┐
│PersonLike│ │ TaskLike │ │EventLike │
└──────────┘ └──────────┘ └──────────┘
Time 1: Users fork when they need new types
User A needs contractor → forks Contact, adds hourlyRate
User B needs family member → forks Contact, adds birthday
Time 2: Features spread through discovery
User C sees User A's contractor pattern, forks it
User D merges birthday field from User B's pattern
Time 3: De facto standards emerge
"contractor with hourlyRate" becomes common
This is now a discoverable pattern
```
### Container as Pattern Selector
The container's "Add" dropdown **lists all patterns that satisfy the interface**. No forking happens here - it's just selection.
```
┌─────────────────────────────────────────────────────────┐
│ Contacts │
│ │
│ [+ Add Contact ▼] │
│ ├─ Contact ─────────────────> creates new Contact │
│ ├─ FamilyMember ────────────> creates new FamilyMember │
│ ├─ Contractor ──────────────> creates new Contractor │
│ └─ ...other PersonLike patterns │
└─────────────────────────────────────────────────────────┘
```
**The container knows:**
- What patterns satisfy its interface (PersonLike)
- What variants users have created (appear in the list)
**Note:** Fork-on-demand happens at the **record level**, not here. See "Upgrading Records via Fork" below.
### Upgrading Records via Fork
The key UX insight: **fork-on-demand happens when viewing an individual record**, not from the container's add menu.
When you're viewing a record (e.g., a Contact named "John Smith"), the system can suggest upgrades to more specific types:
```
┌─────────────────────────────────────────────────────────┐
│ John Smith (Contact) │
│ │
│ name: "John Smith" │
│ email: "john@example.com" │
│ phone: "555-1234" │
│ │
│ [Upgrade ▼] │
│ ├─ "Add birthday → FamilyMember" │
│ ├─ "Add hourlyRate → Contractor" │
│ ├─ "Add company → Employee" │
│ └─ "Fork and customize..." ────> LLM adds fields, │
│ compileAndRun creates │
│ new pattern, record │
│ is replaced in-place │
└─────────────────────────────────────────────────────────┘
```
**How upgrades work:**
1. **Known type upgrades**: The system knows FamilyMember extends Contact with `birthday`. If user adds birthday, offer to upgrade the record to FamilyMember type.
2. **Custom fork**: User says "I need to track this person's billing address and hourly rate." The LLM:
- Forks the Contact pattern
- Adds the requested fields
- Uses `compileAndRun` to compile the new pattern
- Replaces the record in-place with the new type (preserving existing data)
**The `compileAndRun` builtin enables this:**
```typescript
// packages/runner/src/builtins/compile-and-run.ts
// Takes: { files: [{name, contents}], main, input }
// Returns: { result, error, errors, pending }
// 1. LLM generates forked pattern source
const forkedPattern = {
files: [{ name: "contractor.tsx", contents: generatedSource }],
main: "contractor.tsx",
input: existingRecordData // Preserves John's name, email, phone
};
// 2. compileAndRun compiles and instantiates it
// 3. The result replaces the original record in the container
```
**Upgrade discovery logic:**
- Find patterns that extend the current type
- Show which fields would need to be added
- Offer to add those fields and upgrade the record type
- All existing data is preserved during upgrade
### Emergent Schelling Points
Initial: `PersonLike { name: string }`
Emergent (from usage):
```typescript
// Most friend patterns have birthday - it becomes expected
interface FriendLike extends PersonLike {
name: string;
birthday?: string; // emerged from usage
}
```
The system doesn't mandate this - it emerges from what people actually do.
### Discovery & Adoption
For folksonomy to work, users need to discover what others have done:
- Browse existing patterns that satisfy the interface
- See popular fields across variants
- Fork and modify rather than starting from scratch
- LLM suggests: "This looks like a contractor. Want to use the Contractor pattern?"
---
## Architecture Overview
### Core Concept: Container + Record-Level Upgrade
The container lists available patterns; fork-on-demand happens on individual records.
```
┌─────────────────────────────────────────────────────────────────────┐
│ CONTAINER PATTERN │
│ ┌─────────────────────────────────────────────────────────────────┐│
│ │ Minimal Interface Type: PersonLike { name: string } │
│ └─────────────────────────────────────────────────────────────────┘│
│ │
│ items: [ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Contact │ │ FamilyMember │ │ Contractor │ │
│ │ name: "Alice" │ │ name: "Mom" │ │ name: "John" │ │
│ │ + email │ │ + birthday │ │ + hourlyRate │ │
│ │ + phone │ │ + relationship │ │ + company │ │
│ └────────┬────────┘ └─────────────────┘ └─────────────────┘ │
│ │ │
│ ▼ click to view individual record │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Alice (Contact) │ │
│ │ name: "Alice" email: "alice@..." phone: "555-..." │ │
│ │ │ │
│ │ [Upgrade ▼] ← fork-on-demand happens HERE on records │ │
│ │ ├─ "Add birthday → FamilyMember" │ │
│ │ ├─ "Add hourlyRate → Contractor" │ │
│ │ └─ "Fork and customize..." │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ subLists: [AutoImportedGoogleContacts, WorkContacts, ...] │
│ │
│ [+ Add Contact ▼] ← just lists existing patterns, no forking │
│ ├─ Contact │
│ ├─ FamilyMember │
│ └─ Contractor │
│ │
│ addItem: Stream<{ item: PersonLike }> │
│ mentionable: [all items from items + subLists] │
└─────────────────────────────────────────────────────────────────────┘
```
Key properties:
- **Container "Add" menu lists existing patterns** - just picks a type, no forking
- **Fork-on-demand happens on records** - upgrade Alice from Contact to Contractor
- **compileAndRun enables dynamic fork** - LLM generates pattern, compile it, replace record
- **Data preserved during upgrade** - existing fields carry over to new type
- **Lists of lists**: Containers can include other containers (subLists)
- **Containers expose `addItem` handler** for adding new items
- **Mentionable items** can be @-mentioned from container
---
## Minimal Interface Types
Define simple TypeScript interfaces that many patterns can implement:
```typescript
// packages/api/types/interfaces.ts
/** Minimal interface for task-like items */
export interface TaskLike {
title: string;
done: boolean;
}
/** Minimal interface for person-like items */
export interface PersonLike {
name: string;
}
/** Minimal interface for event-like items */
export interface EventLike {
title: string;
date: string; // ISO date
time?: string; // Optional time
}
/** Minimal interface for calendar-renderable items */
export interface CalendarItem {
title: string;
date: string;
time?: string;
duration?: number; // minutes
}
```
---
## Pattern Variants: Fork-on-Demand, Not Pre-Designed Catalogs
**Key insight:** Don't pre-design a catalog of variants. Start with minimal base patterns and fork when needed.
### Base Patterns (Ship These)
We provide minimal base patterns that users fork:
| Base Pattern | Interface | Fields |
|--------------|-----------|--------|
| `contact.tsx` | PersonLike | name, email?, phone? |
| `task.tsx` | TaskLike | title, done |
| `event.tsx` | EventLike | title, date, time? |
### Example Variants (Emerge from Usage)
These are examples of what users might create by forking, not patterns we pre-build:
| Forked From | User's Variant | Added Fields | Why They Forked |
|-------------|----------------|--------------|-----------------|
| contact.tsx | contractor.tsx | hourlyRate, company, billingAddress | Needed to track freelancers |
| contact.tsx | family-member.tsx | birthday, relationship, dietary | Planning family events |
| task.tsx | shopping-item.tsx | quantity, aisle, store | Grocery shopping workflow |
| event.tsx | potluck.tsx | dishes[], attendees[], whoBringsWhat | Organizing potlucks |
### How Forking Works (Record-Level Upgrade)
Forking happens when viewing an individual record, not from the container's add menu.
```
User (viewing "John Smith" as Contact): "I need to track him as a contractor"
System:
1. LLM forks contact.tsx as the base
2. LLM adds: hourlyRate, company, billingAddress fields
3. compileAndRun compiles the new "contractor.tsx" pattern
4. John's record is replaced in-place with Contractor type
5. Existing data (name, email, phone) is preserved
6. Contractor now appears in container's add menu for future contacts
```
**Using `compileAndRun` for dynamic pattern compilation:**
```typescript
// The LLM generates the forked pattern source
const forkedSource = `
///
export interface Contractor extends PersonLike {
name: string;
hourlyRate: number;
company: string;
}
// ... pattern implementation
`;
// compileAndRun compiles and instantiates it
const result = compileAndRun({
files: [{ name: "contractor.tsx", contents: forkedSource }],
main: "contractor.tsx",
input: { name: "John Smith", email: "john@example.com" } // existing data
});
// result.result is the running piece - replaces the original record
```
### Each Forked Pattern:
1. **Is a complete, coherent UX** - not fragmented pieces
2. **Exports `NAME` and `[UI]`** - can be rendered standalone
3. **Satisfies the base interface** - can be used in containers
4. **Lives in user's space** - they own it, can modify it
---
## Container Pattern Protocol
Containers manage items, discover patterns, and support nested lists:
```typescript
// packages/patterns/container-protocol.ts
import type { Stream, Writable, VNode } from "commontools";
/**
* What containers expect from their items
*/
export interface ContainerItem {
item: T; // The actual item (matches minimal interface)
name: string; // Display name
ui?: VNode; // Optional inline UI
sameAs?: T; // Optional: link to same entity in another context
}
/**
* What containers expose
*/
export interface ContainerProtocol {
// Direct items managed by this container
items: Writable[]>;
// Nested containers (lists of lists)
subLists: Writable[]>;
// All items including from subLists (computed)
allItems: ContainerItem[];
// Handlers
addItem: Stream<{ item: T }>;
removeItem: Stream<{ item: T }>;
addSubList: Stream<{ list: ContainerProtocol }>;
removeSubList: Stream<{ list: ContainerProtocol }>;
// For @-mentions
mentionable: unknown[];
// Pattern discovery: find patterns that satisfy interface T
// (used for the "Add" dropdown - just lists existing patterns, no forking)
availablePatterns: PatternRef[];
}
/**
* Reference to a pattern that can be instantiated
*/
export interface PatternRef {
name: string;
schema: unknown; // The pattern's type
create: () => ContainerItem;
}
/**
* Record upgrade protocol - for individual records, not containers
* This is how fork-on-demand works at the record level.
*/
export interface RecordUpgradeProtocol {
// Current record data
currentData: T;
currentType: PatternRef;
// Available upgrades (patterns that extend current type)
availableUpgrades: UpgradeOption[];
// Upgrade the record to a new type (uses compileAndRun internally)
upgradeRecord: Stream<{ newType: PatternRef | string }>; // string = custom fork request
}
export interface UpgradeOption {
targetType: PatternRef;
requiredFields: string[]; // Fields that need to be added
description: string; // e.g., "Add birthday to make this a FamilyMember"
}
```
---
## Example: Contacts Container (with Lists of Lists)
```typescript
///
import { pattern, NAME, UI, Writable, action, computed } from "commontools";
import type { PersonLike, ContainerItem, ContainerProtocol } from "commontools";
interface Input {
title?: string;
items?: Writable[]>;
subLists?: Writable[]>;
}
interface Output {
[NAME]: string;
[UI]: VNode;
items: ContainerItem[];
subLists: ContainerProtocol[];
allItems: ContainerItem[]; // items + all subList items
addItem: Stream<{ item: PersonLike }>;
addSubList: Stream<{ list: ContainerProtocol }>;
mentionable: unknown[];
}
export default pattern(({ title, items, subLists }) => {
const data = items ?? Writable.of[]>([]);
const lists = subLists ?? Writable.of[]>([]);
const addItem = action(({ item }: { item: PersonLike }) => {
data.push({ item, name: item.name });
});
const addSubList = action(({ list }: { list: ContainerProtocol }) => {
lists.push(list);
});
// Combine direct items + items from all sub-lists
const allItems = computed(() => {
const direct = data.get();
const fromSubLists = lists.get().flatMap(sub => sub.allItems);
return [...direct, ...fromSubLists];
});
return {
[NAME]: computed(() => title ?? "Contacts"),
[UI]: (
{/* Direct items */}
{data.map(entry => (
{entry.name}
))}
{/* Sub-lists (e.g., AutoImportedGoogleContacts) */}
{lists.map(subList => (
{subList[NAME]}
{subList.allItems.map(entry => (
{entry.name}
))}
))}
),
items: data,
subLists: lists,
allItems,
addItem,
addSubList,
mentionable: allItems,
};
});
```
---
## Example: Forking Contact to Create FamilyMember
This shows how a user might fork the base Contact pattern to create a FamilyMember variant.
### Base: contact.tsx (provided)
```typescript
///
import { pattern, NAME, UI, Writable, Default, computed } from "commontools";
import type { PersonLike } from "commontools";
export interface Contact extends PersonLike {
name: string;
email?: string;
phone?: string;
}
export default pattern<{ contact: Writable> }, { contact: Contact }>(({ contact }) => {
return {
[NAME]: computed(() => contact.name || "Contact"),
[UI]: (
),
contact,
};
});
```
### Forked: family-member.tsx (user creates when needed)
User says: "I need to track family members with birthdays and dietary restrictions."
LLM forks contact.tsx and adds the requested fields:
```typescript
///
import { pattern, NAME, UI, Writable, Default, computed } from "commontools";
import type { PersonLike } from "commontools";
// Forked from Contact, added: relationship, birthday, dietary, gifts
export interface FamilyMember extends PersonLike {
name: string;
relationship: string; // Added: "spouse", "child", "parent", etc.
birthday?: string; // Added: ISO date
dietaryRestrictions?: string[]; // Added: for meal planning
giftPreferences?: string[]; // Added: for gift giving
}
export default pattern<{ member: Writable> }, { member: FamilyMember }>(({ member }) => {
return {
[NAME]: computed(() => member.name || "Family Member"),
[UI]: (
),
member,
};
});
```
**Key point:** FamilyMember wasn't pre-designed. It was created just-in-time when the user needed it.
---
## Lists of Lists in Practice
### Example: Contacts with Google Import Sub-List
```typescript
///
import { pattern, NAME, UI, Writable, computed } from "commontools";
import type { PersonLike, ContainerProtocol } from "commontools";
// A sub-list that imports from Google Contacts
interface GoogleContactsImport extends ContainerProtocol {
syncStatus: 'synced' | 'syncing' | 'error';
lastSync: string;
}
// Main Contacts container can include this as a sub-list
const contacts = ContactsContainer({
items: [
{ item: { name: "Alice" }, name: "Alice" },
{ item: { name: "Bob" }, name: "Bob" },
],
subLists: [
googleContactsImport, // Includes all Google contacts as a sub-list
workContactsImport, // Could have multiple sub-lists
],
});
// contacts.allItems now includes Alice, Bob, AND all items from sub-lists
```
### Why Lists of Lists?
1. **Keep provenance**: You know Carol came from Google import, not manually added
2. **Bulk operations**: Remove entire import source at once
3. **Avoid duplication**: Don't copy 500 Google contacts into main list
4. **Structure preservation**: Main list + organized sub-groups
5. **Easy cleanup**: If Google sync breaks, just remove that sub-list
### When to Use Sub-Lists vs Projection
| Use Sub-Lists | Use Projections |
|---------------|-----------------|
| Same interface type | Different interface types |
| Imported contacts → Contacts | Notebook checkboxes → Tasks |
| Work contacts → Contacts | Birthdays → Calendar events |
| Structure matters | Shape transformation needed |
---
## Tony's Needs - Addressed
The architect clarified Tony's actual pattern needs:
| Need | Solution |
|------|----------|
| Task list with tasks | Container pattern for `TaskLike` with subLists |
| Calendar with events | Container pattern for `EventLike`/`CalendarItem` |
| Tasks/events can be different shapes | Fork base patterns when needed, add fields |
| Create new items | `addItem` handler on container |
| Create new types | Container helps discover/fork patterns |
| Import contacts from Google | Add GoogleContacts as subList, don't flatten |
| Mentionable items | `mentionable` = all items from items + subLists |
**Implementation pattern:**
1. Define TypeScript type for minimal interface: `{ done: boolean, title: string }`
2. Provide base patterns: `task.tsx`, `contact.tsx`, `event.tsx`
3. Container helps users fork base patterns to create new types
4. Container supports subLists for imports and grouping
5. Simple `sameAs` for occasional duplicates
---
## Handling Duplicates: Simple `sameAs`
### The Scenario
You have:
- `contractor`: `{ name: "John Smith", hourlyRate: 150, company: "Acme" }`
- `friend`: `{ name: "John Smith", birthday: "1985-03-15" }`
These might be the same person. What do you do?
### Simple Solution: `sameAs` Field
```typescript
// When you realize they're the same person, just link them
contractor.sameAs = friend;
// Or bidirectional
contractor.sameAs = friend;
friend.sameAs = contractor;
```
That's it. No complex reconciliation infrastructure.
### Why This Is Enough
1. **Duplicates are rare**: Most people in your contacts are distinct
2. **When they happen, they're obvious**: User notices "wait, I have two John Smiths"
3. **Simple fix**: Link them with sameAs, move on
4. **No merged views needed**: Just navigate from one to the other
### What NOT to Build
Don't build:
- Complex identity resolution algorithms
- Confidence scoring on matches
- Automatic deduplication systems
- Merged view generation
- Conflict resolution UX
These are over-engineering for an edge case. If someone really needs sophisticated deduplication, they can build a specialized tool for it.
### When Duplicates Arise
| Situation | Solution |
|-----------|----------|
| Imported contacts overlap with manual | User links them with sameAs |
| Same person in work and personal lists | Lives in both, linked with sameAs |
| Typo creates duplicate | Delete the duplicate |
| Actually two different people | Leave them separate (no sameAs) |
---
## Critical Files to Modify
| File | Change |
|------|--------|
| `packages/api/types/interfaces.ts` | New: Minimal interface types |
| `packages/patterns/container-protocol.ts` | Revise for minimal interfaces |
| `packages/patterns/variants/` | New: Variant patterns directory |
| `packages/patterns/containers/` | New: Container patterns directory |
---
## Implementation Phases
### Phase 1: Core Infrastructure
- [ ] Define minimal interface types (`TaskLike`, `PersonLike`, `EventLike`)
- [ ] Export from "commontools" entrypoint
- [ ] Revise container-protocol.ts with subLists support
### Phase 2: Base Patterns + First Container
- [ ] `contact.tsx` - Base PersonLike pattern (minimal: name, email?, phone?)
- [ ] `task.tsx` - Base TaskLike pattern (minimal: title, done)
- [ ] `event.tsx` - Base EventLike pattern (minimal: title, date)
- [ ] `contacts-container.tsx` - Container with subLists support
### Phase 3: Record-Level Upgrade Infrastructure
- [ ] Pattern discovery: find patterns satisfying an interface (for container "Add" menu)
- [ ] Upgrade discovery: find patterns that extend current record's type
- [ ] `RecordUpgradeProtocol` implementation for individual records
- [ ] LLM-assisted fork: generate new pattern source with requested fields
- [ ] Integration with `compileAndRun`: compile forked pattern dynamically
- [ ] Record replacement: replace record in-place with upgraded type (preserve data)
- [ ] Schema validation: verify fork still satisfies base interface
### Phase 4: Lists of Lists
- [ ] `addSubList` handler on containers
- [ ] Combined `allItems` computed property
- [ ] Example: GoogleContactsImport as sub-list of Contacts
### Phase 5: Simple Duplication Handling
- [ ] Add optional `sameAs` field to ContainerItem
- [ ] UI to link two items as same entity
- [ ] Navigation between linked items
### Phase 6: UX for Container + Record Upgrade
- [ ] Container "Add" menu shows available patterns (just selection, no forking)
- [ ] Record view shows "Upgrade" menu with available type upgrades
- [ ] "Fork and customize..." option in record upgrade menu triggers LLM flow
- [ ] compileAndRun integration for dynamic pattern compilation
- [ ] Record replacement UI (preserves data, changes type)
- [ ] Browse patterns others have created (optional)
---
## Verification Plan
### Container Protocol
1. **Test addItem**: Add contact to container, verify it appears
2. **Test addSubList**: Add GoogleContacts import, verify items appear in allItems
3. **Test mentionable**: Verify all items (direct + from subLists) appear in mentionable
### Record-Level Upgrade
4. **Test pattern discovery**: Container finds patterns satisfying PersonLike for "Add" menu
5. **Test upgrade discovery**: Record shows available type upgrades (Contact → FamilyMember)
6. **Test fork + compileAndRun**: Fork contact.tsx, add birthday field, compile dynamically
7. **Test record replacement**: Upgrade Alice from Contact to FamilyMember, verify data preserved
8. **Test schema validation**: Upgraded pattern still satisfies PersonLike interface
### Lists of Lists
9. **Test nested structure**: Contacts with 2 subLists, verify allItems combines correctly
10. **Test provenance**: Items from subLists show their source
11. **Test bulk removal**: Remove subList, verify its items disappear from allItems
### Simple Duplicates
12. **Test sameAs**: Link two contacts, verify navigation between them works
### Integration
13. **Manual test**: Create Contacts, add direct contacts, add GoogleContacts subList
14. **Manual test**: View Alice (Contact), use Upgrade menu to make her a Contractor
15. **Verify**: After upgrade, Alice appears as Contractor in container, original data preserved
---
## Questions for Architect (Berni)
### Addressed by This Revision (v3)
1. ~~Over-engineering reconciliation~~ → Simple `sameAs` field
2. ~~Pre-designed variant catalogs~~ → Fork-on-demand
3. ~~Complex identity infrastructure~~ → Treat duplicates as edge case
4. ~~Flat container model~~ → Lists of lists (subLists)
### Still Open
1. **Interface location**: Should minimal interfaces live in `packages/api/types/` or elsewhere?
2. **Pattern discovery mechanism**: How does a container find all patterns that satisfy its interface?
- Query by interface type?
- Registry of patterns with their interfaces?
- Static analysis of pattern schemas?
3. **Record upgrade discovery**: How does a record know what types it can upgrade to?
- Find patterns where current type is a subset of target type?
- Registry of "extends" relationships between patterns?
- LLM analysis of schema compatibility?
4. **compileAndRun integration for upgrades**: How exactly does the upgrade flow work?
- LLM generates forked pattern source
- `compileAndRun` compiles it dynamically
- How is the record "replaced in-place" in the container?
- Does the new pattern get saved to user's space for reuse?
5. **SubList identity**: How do we track where a sub-list came from?
- Reference to original container?
- Import source metadata?
- Live sync vs snapshot?
6. **Schema validation**: How do we verify a fork still satisfies the base interface?
- TypeScript compiler check?
- Runtime validation?
- Warning vs error on violation?
---
## Appendix: What Changed
### v1 → v2 (Annotation → Reconciliation)
| v1 Design | v2 Design | Rationale |
|-----------|-----------|-----------|
| `[ANNOTATIONS]` array | Identity linking | Not extensibility, it's reconciliation |
| Annotation system | Records + reconciliation | Simpler mental model |
| Single "person" + annotations | N variant patterns | Coherent UX per variant |
### v2 → v3 (Pre-Designed → Fork-on-Demand)
| v2 Design | v3 Design | Rationale |
|-----------|-----------|-----------|
| Pre-designed N variants | Fork-on-demand | Create types when you need them |
| Complex reconciliation | Simple `sameAs` field | Duplicates are edge case |
| Flat container items | Lists of lists (subLists) | Contacts includes GoogleContacts |
| Catalog of variants | Container discovers patterns | Container is the entry point |
| Identity linking infrastructure | Simple field reference | Don't over-engineer edge cases |
### v3 → v4 (Container Fork → Record-Level Upgrade)
| v3 Design | v4 Design | Rationale |
|-----------|-----------|-----------|
| Fork from container "Add" menu | Fork from individual record "Upgrade" menu | Fork happens on records, not containers |
| Container has forkPattern handler | Container just lists patterns | Simpler container responsibility |
| Unclear when fork happens | Fork when viewing a record and needing more fields | Clearer mental model |
| No record replacement model | Record upgraded in-place, data preserved | Upgrade = replace with richer type |
| N/A | compileAndRun enables dynamic compilation | Leverage existing builtin for fork flow |
### Key Insight (v4)
> "Fork-on-demand happens on **individual records**, not from the container's add dropdown. When viewing John Smith (Contact), you can upgrade him to a Contractor by adding hourlyRate. The LLM forks the pattern, compileAndRun compiles it, and the record is replaced in-place."
**Container responsibility:** List available patterns in "Add" menu (no forking)
**Record responsibility:** Show upgrade options, enable fork-and-upgrade via compileAndRun
---
## Appendix: Research Sources
- **Architect feedback (v1)**: Discord messages (2026-01-27, morning)
- **Architect feedback (v2)**: Live conversation (2026-01-27, afternoon) - fork-and-pull, reconciliation
- **Architect feedback (v4)**: Clarification that fork happens on records, not container add menu
- **Container protocol**: `packages/patterns/container-protocol.ts`
- **Existing patterns**: `packages/patterns/contacts/`, `packages/patterns/todo-list/`
- **Template registry**: `packages/patterns/record/template-registry.ts` - existing fork model
- **Module registry**: `packages/patterns/record/registry.ts` - composable modules
- **compileAndRun builtin**: `packages/runner/src/builtins/compile-and-run.ts` - dynamic pattern compilation