Repository navigation
Note Page WQL Alignment
tags:
- domain-model
- audit
- wql
- note
- page date: 2026-09-27 status: aligned-v23
[!info] Document Purpose This document audits the data attributes, ownership boundaries, and query interactions of Note and Page, specifically how WQL (
find:,rows:,agg:) resolves and composes them. It highlights existing conflations, documents current runtime behavior, and provides tagged decision sections for alignment.
There is a fundamental semantic collision in the codebase between two definitions of "Page":
-
Storage Entity (Page /
pagetable):- A lightweight placement and routing anchor (
id,date?,slug?,title?,createdAt). - Has no body, no markdown, no segments, and no tags.
- Multiple workout Notes associate with a Page via the
page_notesjunction table (e.g. stacking multiple notes on a single journal date/journal/2026-09-26or named collection/p/:slug).
- A lightweight placement and routing anchor (
-
WQL Target & Document Role (
find:page/Note.type === 'page'):- A document-style note (guides, syntax canvases, dashboards, landing content).
- This is stored in the
notestable, not thepagetable. - In fact, WQL's
find:pageexecutor completely bypasses thepagetable and queries thenotestable filtering for document types andsourceIdprefixes.
erDiagram
Page ||--o{ PageNote : "pageId"
Note ||--o{ PageNote : "noteId"
Note ||--|{ BlockRevision : "noteId, blockId, version (saved actionable states)"
Note ||--o{ BlockIndexRow : "noteId (parsed blocks)"
BlockIndexRow ||--o{ BlockEffort : "blockContentId (exercise links)"
Effort ||--o{ BlockEffort : "effortSlug"
Note ||--o{ Session : "noteId (executions)"
Note ||--o{ EventRecord : "noteId (telemetry)"
Note ||--o{ Attachment : "noteId (blobs)"
Note }o--o{ Tag : "via note_tags"
Tag }o--o| TagType : "type (domain, format, equipment, quality, intent)"
- Storage Owner: Canonical unit of authored content and exercise logic.
- Fields Owned:
| Field | Type | Storage Role / Meaning | Status / Notes |
|---|---|---|---|
id |
string (UUID) |
Canonical entity ID. Primary key across all authoring and telemetry links. | Implemented |
title |
string |
Display name of the note or workout. | Implemented |
type? |
NoteKind |
Content taxonomy descriptor: 'journal' | 'template' | 'playground' | 'collection' | 'dashboard' | 'page' | (string & {}). |
Implemented (Consolidated in V11) |
sourceId? |
string |
Origin provenance. Parent note UUID or template/collection source ID. | Implemented |
catalog? |
string |
Seed directory identifier for bundled content. | Implemented |
date? |
number |
Unix timestamp (ms) representing the domain date of the record. Populated from frontmatter during seed/save. | Implemented |
createdAt |
number |
Unix timestamp (ms) when the note was created. | Implemented |
seedOrigin? |
'seed' | 'user' |
Ownership marker for seed synchronization. | Implemented |
tags? |
string[] |
Optional denormalized tag array projection. Canonical persistence lives in normalized note_tags. |
Implemented |
-
Former Fields (Retired in V11):
-
slug: Removed fromNote. Route slugs live exclusively onPage.slug. -
pageId: Removed fromNote. Placements live in thepage_notesjunction table. -
segmentIds,templateId,updatedAt: removed fromNotestorage rows (rawContent restored as the single authored source by the sparse cutover).
-
-
Content Relation:
- Raw markdown lives once on
Note.rawContent; saved actionable block states live in BlockRevision (block_revisionstable). (Superseded the retired per-segment rows.) - Derived blocks live in BlockIndexRow (
block_indextable).
- Raw markdown lives once on
- Storage Owner: Placement, calendar dates, and named collection anchors.
- Fields Owned:
| Field | Type | Storage Role / Meaning | Status |
|---|---|---|---|
id |
string (UUID) |
Primary key. | Implemented |
date? |
string (YYYY-MM-DD) |
Unique index by-date. Identifies calendar/journal day. |
Implemented |
slug? |
string |
Unique index by-slug. Identifies named custom routes (/c/:slug, /p/:slug). |
Implemented |
title? |
string |
Optional display label for the page container. | Implemented |
createdAt |
number |
Unix timestamp (ms). | Implemented |
-
What Page Does NOT Own:
- No markdown or segments: A page has no body text of its own.
-
No tags: Tagging occurs exclusively via
note_tagsonNote.id. -
No telemetry: Sessions and results reference
Note.iddirectly.
- Storage Owner: Many-to-many relationship between Page and Note.
-
Fields:
-
id: string (UUID) -
pageId: string (FK topage.id) -
noteId: string (FK tonote.id) -
position?: number (ordering within the page) -
createdAt: number
-
-
Indexes:
by-page,by-note,by-page-note(unique compound).
In V10, pageId was copied onto subordinate entities (segments, sessions, attachments, events).
Under the current domain model, this denormalization is an architectural vestige that breaks cardinality:
-
Authored content and telemetry belong to
noteId, notpageId. - A single note can belong to multiple pages (e.g. a date page and a custom collection page) via
page_notes. A single denormalizedpageIdon segments or events cannot represent this and causes dangling or arbitrary references. - Queries no longer use child
by-pageindexes (retrieval is driven bynoteId, and pages resolve notes viapage_notes). -
Alignment Rule: Subordinate entities (
Session,Attachment,EventRecord) must treatpageIdas deprecated legacy context only. Identity, retrieval, telemetry aggregation, and deletion cascade exclusively vianoteId.
WQL provides three query families:
-
find:<target>{filters} [last <window>](Content Discovery) -
rows:<plane>{filters}(Execution Statement Records) -
<agg>:<metric>{filters} [by {dim}] [where find:...](Analytics & Joins)
In packages/wql/src/QueryService.ts (runFind):
// How QueryService currently discriminates page vs note:
const isPageTarget = parsed.target === 'page';
const hasTypeFilter = parsed.filters.some(f => f.key === 'type' || f.key === 'page');
const hasPageSource = parsed.filters.some(f => f.key === 'source' && f.values.some(v => v.value === 'page' || v.value === 'pages'));
const isPage = (n: Note) =>
n.type !== 'note' && (
n.sourceId?.startsWith('page:') ||
n.sourceId?.startsWith('guides:') ||
['collection', 'syntax', 'behavior', 'analytics', 'dashboard', 'home', 'page'].includes(n.type ?? '')
);
if (isPageTarget || hasPageSource) {
notes = notes.filter(isPage);
} else if (parsed.target === 'note' && !hasTypeFilter) {
notes = notes.filter(n => !isPage(n));
}-
find:note:- Loads all records from
noteStore+staticNoteStore. - Filters out records matching
isPage(n). - Result: Only returns workout/executable notes (unless
type:is explicitly specified).
- Loads all records from
-
find:page:- Resolves placement pages (collections, dates, custom routes) from the
pagetable by joining candidate notes viapage_notes. - Filtered by note tags (
find:page{domain:crossfit}) or exercise containment (find:page{effort:clean-and-jerk}).
- Resolves placement pages (collections, dates, custom routes) from the
-
find:block:- Queries
block_index. Filtered by note tags vianoteIdor direct exercise containment viablock_efforts(find:block{effort:pull-up}).
- Queries
-
source:<scope>:-
source:journal: Notes where!sourceId || sourceId === 'journal'(excluding playground IDs). -
source:collections: Notes wheresourceId.startsWith('collection:') || sourceId.startsWith('page:collection:'). -
source:page/source:pages: Notes wheresourceId === 'page' || sourceId.startsWith('page:') || sourceId.startsWith('guides:'). -
source:playground: Notes wheresourceId === 'playground' || type === 'playground'.
-
-
type:<kind>: MatchesNote.type(e.g.type:wod,type:collection). -
tags:<label>: IntersectsnoteIdvianote_tags/Tagindex. -
catalog:<id>: Matches static content directory slug (e.g.crossfit-girls). -
Time Windows (
last 2w,from ... to ...):- Evaluated against
Note.createdAt. -
Gap: Calendar journal pages store their canonical date in
Page.date(YYYY-MM-DD), but WQL time-window queries evaluate againstNote.createdAt(Unix ms). Seeded workouts or backdated entries can lead to date mismatches.
- Evaluated against
-
rows:all{note:<noteId>}/rows:segment{block:<blockId>}:- Scans
events(EventRecord) filtering directly bynoteIdorblockContentId. - Note:
rows:supports filtering bynoteandblock, but has no directpage:<pageId>scope key inWQL_ROWS_SCOPE_KEYS.
- Scans
-
agg:metric{} where find:note{...}:- Executes
runFindfor thefind:notepredicate. - Extracts the resulting list of matching
noteIdvalues. - Filters
EventRecordtelemetry whereevent.noteId IN (matchingNotes). - Computes aggregation over the filtered event stream.
- Executes
Use these sections to mark decisions and track consensus.
- Option B (Separate Entity Target):
find:pagequeries thepagetable to discover date/custom page containers viapage_notesjunction; document notes usefind:note{template:canvas}orfind:note{source:guides}.-
Tradeoff: Clean relational model where
find:pagetargets actualPagerows andfind:notetargets authoredNotedocuments.
-
Tradeoff: Clean relational model where
- Contract Rule:
page_notesis strictly placement and grouping (which calendar date or custom page anchors this note). - Contract Rule: A note appearing in a collection, tag list, or stream view is composition via query, and must NEVER modify
page_notesor subordinate records. - Contract Rule: Multi-note pages (like
JournalDatePage) compose independentNoterecords; they do not merge them into a single synthetic note. - Contract Rule: Child records (
segments,sessions,events,attachments) belong tonoteId. SubordinatepageIdfields are deprecated legacy fields and never used as canonical join keys.
- Current State: Fact telemetry queries evaluate against observation civil dates (
civilDatesCoveredByRange,metricDate, andtimestamp). Note discovery (find:note) evaluates againstNote.createdAt/Note.date.
- List views and query cards expose both:
-
noteId: Links to the universal editor (/notes/:noteId). -
pageId: Links to the containing view/page context (/journal/:dateor/p/:slug).
-
- Store schemas:
apps/playground/src/types/storage.ts - IndexedDB initialization:
apps/playground/src/services/storage/IndexedDBStorage.ts - WQL engine & query execution:
packages/wql/src/QueryService.ts - WQL vocabulary & targets:
packages/wql/src/vocabulary.ts - Domain model specifications:
docs/domain-model/Note.mddocs/domain-model/Page.mddocs/domain-model/collection.mddocs/domain-model/BlockRevision.md