People-owned and producer-owned data
Every document in the store has exactly one kind of writer. Which paths belong to people, which to producers, and why that rule replaces locking.
What it is
Every path in the store is written either by producers (routine runs) or by people (through the Desk), never both. Views and agent columns are the one shared case: a producer may create them, and from then on only people change them.
Why it exists
Page writes in the artifact database carry no version check; they are last-writer-wins. Only the run side uses if_version. If a person and a run could both write one document, a run could silently undo a person’s decision, or a person’s save could vanish under a run’s write. Giving each document one kind of writer means they never race, with no locks.
It is also the main safety rule. Settings, decisions and merges are how people steer a routine. If a run could write them, text on a fetched page could steer the routine instead.
How it works
desk_contract.pyholdsHUMAN_PREFIXES:settings/,userstate/,decisions/,merges/,data/users/.guard()refuses any planned batch with a write under them.CREATE_ONLY_PREFIXES:views/,columns/. A producer may create a document there only if it is absent; any edit or delete is refused.PRIVATE_PREFIXES:userstate/,data/users/. The planner never reads them.- On the page side,
actions.jsis the only file that writes, and only toPEOPLE_COLLECTIONS(settings,userstate,decisions,merges,views,columns) plusdata/users/<viewer>/.
Reference
| Path | Writer | Notes |
|---|---|---|
meta/workspace, meta/definition |
Seed step | Written once by seed, or compile for the definition |
entities/<id> |
Producers | Each producer replaces only its own bySource slice |
runs/<runId> |
Producers | |
inbox/<runId>--<n> |
Producers | |
pending/<id> |
Producers | Gates; create-only per run id |
state/learning |
Producers | Written by learn |
settings/<producerId>, settings/workspace |
People | Seed writes defaults once, only when absent |
decisions/<id> |
People | Keep, drop, accept, reject, approve |
merges/<entityId> |
People | Hand merges |
userstate/<entityId> |
People | Stars, tags, notes |
views/<id>, columns/<id> |
Agent creates as proposed; people own every later edit | |
data/users/<viewer>/state/... |
That viewer | Read, done, ack; private even from the owner |
settingsHistory/<id> |
People | Since 0.5.0; see below |
Since 0.5.0:
settingsHistory/is a new people collection (every Settings save writes{producer, before, after, at, by}), and per-viewersnoozeandpinstate underdata/users/<viewer>/state/. New people collections are added to bothactions.jsandHUMAN_PREFIXES.
Per-viewer state
data/users/<id>/ is private to that viewer. The page gets <id> from user.id() after declaring the user capability. Documents sit at even segment counts, so per-viewer paths carry a state segment: data/users/<viewer>/state/read/<inboxId>. If user.id() returns null, per-viewer features are off for that view; nothing falls back to a shared path or to localStorage.
Worked example
On Hiring Signals, a person opens the dropped “Graduate Data Analyst” role and chooses Keep. The Desk writes decisions/role--https-jobs-example-au-1002 {decision: "keep", at, by}. On the next run the write contract reads that decision and writes the role as status: "active", dropReason: null, with the sighting note kept by person. The run never writes the decision, and the person never writes the entity. The learning step counts the keep as evidence that title_keywords missed something.
Common pitfalls
- Storing a person’s edit on the entity. It will be overwritten by the next run. Store it as a decision or in
userstate/. - A routine that “remembers” by writing settings. Refused. Memory lives in
state/or on records. - Reading per-viewer state in a run. The planner never reads it, by design. A run cannot know who read what.