Rules and patterns for authoring AD4M data models in this package.
src/
├── WeNode.ts ← base class for all models
├── constants.ts ← shared URIs and constants
├── entities/ ← first-class application objects
├── blocks/ ← composable content nodes
└── utils/ ← helper functions (transforms, normalisation)
Entities are first-class application objects with independent identity and lifecycle. They exist before and outside any document that might reference them. They are never written as direct children of a CollectionBlock in the block editor.
Examples: Space, AgentProfile, Signal, ChatSession, ChatMessage
Rules:
- No
versionproperty — they are not collaboratively edited in a CRDT sense - If they need to appear inside a document composition, they are referenced by pointer via
EmbedBlock(not embedded directly) - Typically managed by stores (
AdamStore,SpaceStore, etc.)
Blocks are composable content nodes that can be created fresh and dropped directly into the block editor as a unit of authored content. They may also exist and be used standalone outside any composition — but their defining characteristic is that authoring them inline in a document makes sense.
Examples: TextBlock, ImageBlock, LocationBlock, EventBlock, TaskBlock, AudioBlock
Rules:
- Always have a
version: numberproperty for CRDT conflict resolution in collaborative editing - Can appear as direct children of
CollectionBlock - Can also be used standalone (e.g.
LocationBlockreferenced viaSpace.locationsHasMany,TaskBlockin a task board view) — this does not make them entities
"Can a user meaningfully create this fresh, inline, while authoring a document?"
- Yes → it's a block (
blocks/) - No → it's an entity (
entities/)
Space is the canonical borderline example: a space could theoretically be created from a document, but doing so has infrastructure consequences (registering SDNA models, creating a perspective, optionally publishing a neighbourhood) that make inline authoring impractical. It is therefore an entity.
EmbedBlock is a special block whose purpose is to embed a reference to an entity (or another block) by its stable identifier. The renderer resolves the reference lazily from the appropriate store.
// Embedding a Space in a document
EmbedBlock {
target: '<space-uuid>',
targetType: 'space',
displayMode: 'card' | 'inline',
}
// Embedding a user in a document
EmbedBlock {
target: '<agent-did>',
targetType: 'agent',
displayMode: 'card' | 'inline',
}Valid targetType values: 'space' | 'agent' | 'block'
All models — both entities and blocks — extend WeNode, which in turn extends Ad4mModel.
WeNode provides:
comments: string[]— HasMany relation for comment IDssignals: string[]— HasMany relation for signal IDs
These are always present on every model instance but are only surfaced in the UI where contextually appropriate (e.g. a LocationBlock pin on a map might not show a comments thread, but the data is there if needed in future).
| Type | Suffix | Example |
|---|---|---|
| Block | *Block |
LocationBlock, TaskBlock |
| Entity | none | Space, AgentProfile |
| Utility function | camelCase | resizeImage, normalizeSignal |
All @Property predicates use the we:// namespace. Use snake_case for multi-word predicates:
@Property({ through: 'we://name' })
@Property({ through: 'we://start_date' })Prefer generic, reusable predicates. When two models share the same semantic concept (e.g. name, description, url), use the same predicate — this enables cross-model graph pattern queries. For example, querying ?node we://name ?name returns names from LocationBlock, Theme, ChatSession, TagBlock, and any future model without knowing the type in advance.
Every model must have a @Flag as its first property. The @Flag decorator writes a constant triple that uniquely identifies the model type — essential for filtering mixed graph results.
@Flag({ through: 'we://flag', value: 'we://location_block' })
flag: string = '';Rules:
- Always use
we://flagas thethroughpredicate — neverwe://type(that is a content property on some models) - The
valuefollows the patternwe://<snake_case_model_name> - The TypeScript property must be named
flagto avoid clashing with contenttypeproperties - Always the first property in the class, before any
@Propertyor@HasMany
- Decide: entity or block? Apply the authoring question above.
- Place in the correct folder (
entities/orblocks/). - Extend
WeNode(notAd4mModeldirectly, unless you have a specific reason). - Add a
version: numberproperty if and only if it's a block. - Export from the folder's
index.tsand fromsrc/index.ts. - Register the model in any perspective that needs it (e.g. in
AdamStore.createSpaceorinitSystemPerspectives).