dont-data-model Specification
Purpose
TBD - created by archiving change add-dont-data-model-specs. Update Purpose after archive.
Requirements
Requirement: Entity structure
All stored objects SHALL be entities. Each entity SHALL have an id (ULID, prefixed per kind: claim:, term:, event:, evidence:, project:), a kind attribute (claim, term, hypothesis, evidence, project, …), a set of attribute assertions, and a history of events. Class membership SHALL be recognised by predicate match, not stored as a fact — “Is this entity an X?” is a rule evaluation, not a lookup. Attributes SHALL be first-class and globally defined; they are not owned by classes. The event:, evidence:, and project: prefixes are internal storage identifiers; only claim: and term: are externally-addressable as EntityId values in CLI verbs and input schemas (see dont-payload-types). Parsers MUST use a default branch for unknown entity_kind values; adding a new entity kind requires a minor-version envelope bump.
Scenario: entity has prefixed ULID identifier
- WHEN a new entity is created
- THEN it receives a ULID prefixed by its kind (e.g.
claim:01HX05A9K8VP)
Scenario: entity kind is an attribute not a class declaration
- WHEN a consumer queries “Is entity X a RicciTensor?”
- THEN the system evaluates a recognition predicate, not a stored class-membership fact
Scenario: attributes are globally scoped
- WHEN an attribute
statementis defined - THEN it is available to any entity, not bound to a single kind
Scenario: unknown entity kind uses default branch
- WHEN a parser encounters an entity with an
entity_kindvalue not in its known set - THEN it handles the entity through a default branch rather than failing
Scenario: project entity is reserved for project-wide audit
- WHEN the system needs to record a project-wide event such as
mode-changed - THEN it uses a reserved internal
project:entity rather than attaching the event to a claim or term
Requirement: Datom-based event-sourced storage
The system SHALL store all epistemic core facts (claims, terms, statuses, events) as datoms — immutable atomic facts in the shape (entity, attr, value, tx, assert_bit) — where tx is a monotonically increasing transaction number and assert_bit distinguishes assertion from retraction. In-place mutations are permitted for project infrastructure (e.g. config.toml) and cache artifacts, but the epistemic core MUST remain strictly append-only. Every CLI invocation SHALL be its own transaction; there is no batching across invocations. Within a transaction, event ordering SHALL be ULID-sorted. Derived orchestration commands (like assume or overlook) MUST NOT bypass core invariants; if they need to reach a final state, they MUST emit a sequence of valid events (e.g. concluded then dismissed) rather than jumping directly to a verified state.
Scenario: fact is stored as a datom
- WHEN a claim is concluded
- THEN the system writes datoms with entity, attribute, value, transaction number, and assertion bit
Scenario: each CLI invocation is one transaction
- WHEN two
dontcommands run sequentially - THEN each produces a distinct transaction with a higher transaction number than the previous
Scenario: events within a transaction are ULID-ordered
- WHEN a single transaction writes multiple events
- THEN the events are ordered by their ULID within the transaction
Scenario: attribute update produces assertion and retraction datoms
- WHEN a claim’s status changes from
unverifiedtodoubted - THEN a retraction datom (
assert_bit=0) for the old status and an assertion datom (assert_bit=1) for the new status are written in the same transaction
Requirement: Snapshot isolation and concurrency
The system SHALL use snapshot isolation: concurrent readers see a consistent snapshot and writers serialise. When parallel dont calls produce a write conflict, the system SHALL retry with exponential backoff (up to 3 attempts, 50 ms base). After the third retry exhausts, the call SHALL terminate with a non-zero exit status and emit an error envelope with code: "db-locked", unmet_clauses[] naming the transaction that held the lock, and remediation[] suggesting dont doctor and re-running the command (see dont-errors).
Scenario: concurrent readers see consistent state
- WHEN two read-only
dontcommands run concurrently - THEN each sees a consistent snapshot and neither blocks the other
Scenario: write conflict retries with backoff
- WHEN a write conflicts with another in-flight transaction
- THEN the system retries up to 3 times with exponential backoff from 50 ms base
Scenario: write conflict succeeds on retry
- WHEN a write conflicts with another transaction but the second retry succeeds
- THEN the command exits 0 and the transaction commits normally
Scenario: exhausted retries produce db-locked error
- WHEN all 3 retry attempts fail
- THEN the command exits non-zero with
code: "db-locked",unmet_clauses[]naming the blocking transaction, andremediation[]suggestingdont doctorand re-running the command
Requirement: Signal handling
The system SHALL handle SIGINT and SIGTERM by cleanly rolling back any in-flight transaction: no partial event is committed, and the process exits with code 130 (SIGINT) or 143 (SIGTERM). Signals received during the read-only preamble (argument parsing, config load, pending-spawn expiry sweep) SHALL cause a clean exit with no store mutation. SIGPIPE (closed downstream reader) SHALL be treated as normal termination with exit code 0. Signals received during network I/O inside dont verify-evidence or dont import SHALL cancel the in-flight request; any rows written for URIs already resolved before the signal SHALL remain committed. SIGKILL cannot be intercepted; the append-only log guarantees the store is never corrupted — at worst a transaction is lost, never half-applied.
Scenario: SIGINT during transaction rolls back cleanly
- WHEN SIGINT is received during a write transaction
- THEN the transaction is discarded, no partial event is committed, and the process exits 130
Scenario: SIGPIPE is normal termination
- WHEN SIGPIPE is received (e.g. from
head -n 1in a pipeline) - THEN the process exits with code 0
Scenario: SIGTERM during transaction rolls back cleanly
- WHEN SIGTERM is received during a write transaction
- THEN the transaction is discarded, no partial event is committed, and the process exits 143
Scenario: signal during read-only preamble exits cleanly
- WHEN SIGINT is received during argument parsing or config load
- THEN the process exits cleanly with no store mutation
Scenario: signal during verify-evidence preserves completed checks
- WHEN SIGINT is received during
dont verify-evidenceafter 3 of 5 URIs have been checked - THEN the 3 completed
evidence_checkedrows remain committed and the process exits 130
Scenario: SIGKILL does not corrupt store
- WHEN SIGKILL terminates the process mid-transaction
- THEN the append-only log guarantees no half-applied state; at worst the in-flight transaction is lost
Requirement: Time-travel query support
The system SHALL support reconstructing state at any past transaction by querying at a specific transaction number. Full bitemporality (transaction-time + valid-time) SHALL be available in the storage substrate for future features but the v0.3 CLI surface SHALL use transaction-time only.
Scenario: query at a past transaction
- WHEN a consumer queries state at transaction 42
- THEN the system returns the state as it was after transaction 42 committed
Scenario: valid-time is substrate-available but not CLI-exposed
- WHEN a consumer uses the v0.3 CLI
- THEN no valid-time query surface is available; every fact is stamped with wall-clock
atonly
Requirement: Core relations
The system SHALL maintain these logical relations derived from datoms:
entity { id, entity_kind, created_at, created_by }attribute { entity_id, name, value, tx }event { id, entity_id, event_kind, at, author, reason?, evidence_uri?, spawn_request_id? }evidence { id, entity_id, source_uri, kind, supports, quote? }depends_on { entity_id, dep_id }
Both evidence and depends_on SHALL key on entity_id (not claim_id) because dismiss targets both claims and terms and stale-cascade traverses term-to-term dependencies. depends_on rows for terms SHALL be synthesised from kind_of/related_to attributes at rule-evaluation time, not written directly by a verb; the relation’s shape admits both explicit and synthesised rows.
Scenario: evidence keyed on entity not claim
- WHEN evidence is attached to a term via
dismiss - THEN the
evidencerelation stores it withentity_idpointing to the term
Scenario: depends_on supports term-to-term edges
- WHEN a term has a
kind_ofreference to another term - THEN a
depends_onedge exists between them for stale-cascade traversal
Requirement: Kind disambiguation
The system SHALL namespace “kind” to avoid overloading: entity_kind for what an entity is (claim, term, event, evidence, project), event_kind for what happened (see canonical event kind list), envelope_kind for the envelope’s payload-type discriminator (see dont-envelope), and rule_kind for the applicable-rules discriminator (gate, flag; see dont-payload-types).
Scenario: entity_kind distinguishes stored object types
- WHEN a claim entity is queried
- THEN its
entity_kindis"claim"
Scenario: event_kind distinguishes event types
- WHEN a trust event is recorded
- THEN its
event_kindis"trusted"
Requirement: Canonical event kind list
The v0.3 system SHALL emit exactly these 12 event kinds: created, concluded, defined, trusted, dismissed, locked, ignored, spawn-requested, spawn-resolved, spawn-timeout, evidence-checked, mode-changed. Derived assessments such as stale SHALL NOT emit event kinds because they are recomputed trace results rather than persisted lifecycle changes. All event kinds SHALL be lower-kebab-case per the naming convention in dont-envelope. Additions SHALL require a minor-version envelope bump and MUST be documented in the changelog. Parsers MUST treat unknown event kinds as ignorable (forward compatibility) — unknown events SHALL appear in history[] output but SHALL NOT cause parser failure.
Scenario: complete event kind set
- WHEN the system processes events
- THEN it recognises exactly the 12 canonical event kinds listed for v0.3
Scenario: unknown event kind is ignorable
- WHEN a parser encounters an event kind not in the v0.3 set
- THEN it treats the event as ignorable rather than failing
Scenario: new event kind requires envelope version bump
- WHEN a new event kind is introduced
- THEN it requires a minor-version envelope bump and changelog documentation
Requirement: Claim-specific attributes
Claims SHALL carry these attributes: statement (string), status (lattice value per dont-status-lifecycle), confidence (float | null; null when no LLM-authored value was provided), provenance (structured object), atoms[] (repeated attribute rows, not JSON blobs), hypotheses[] (repeated attribute rows; each carries {idx, text, assessment} where assessment = {supporting: string[], refuting: string[]}), and refs[] (repeated attribute rows). atoms[], hypotheses[], and refs[] SHALL be stored as repeated attribute rows so that rules can reason over individual items. The lockable rule (see future rules spec) requires at least 3 assessed hypotheses to gate lock.
Scenario: atoms stored as individual rows
- WHEN a claim is concluded with 3 atoms
- THEN each atom is stored as a separate attribute row, not as a JSON blob
Scenario: confidence is uncalibrated
- WHEN a claim has
confidence: 0.78 - THEN consumers treat this as the LLM’s stated number, not a calibrated probability
Requirement: Term-specific attributes
Terms SHALL carry these attributes: curie (string, the term’s compact URI), definition (string, prose definition), label (string, optional; the SK11 type-text — a singular indefinite noun phrase suitable for use in olog diagrams), kind_of[] (array of CURIEs referencing parent terms), related_to[] (array of CURIEs referencing related terms), status (persisted lattice value per dont-status-lifecycle), and provenance (structured object). kind_of[] and related_to[] create edges traversed by stale-cascade derived-assessment computation and dangling-definition rules.
Seed entries (populated during dont init) that omit label in the seed YAML suppress the verb-level label-shape validators during seed installation only. Suppression is limited to the dont init seed pass; subsequent dont define calls on the same CURIE are subject to normal validator behaviour. The seed is pre-locked and is not round-tripped through define, so no term-shape-* refusal fires during seed installation regardless of whether label is present.
Scenario: term carries curie and definition
- WHEN a term is defined with
dont define proj:RicciTensor --doc "..." - THEN the stored term has
curie: "proj:RicciTensor"anddefinitionmatching the provided text
Scenario: term carries label when supplied
- WHEN a term is defined with
dont define proj:RicciTensor --label "a Ricci tensor" --doc "..." - THEN the stored term has
label: "a Ricci tensor"alongsidecurieanddefinition
Scenario: label is absent when not supplied
- WHEN a term is defined without
--label - THEN the stored term has no
labelattribute; the field is optional
Scenario: seed entry without label suppresses shape checks
- WHEN
dont initinstalls seed terms from YAML entries that omitlabel - THEN no
term-shape-*refusal fires for those entries during installation
Scenario: kind_of creates traversable edges
- WHEN a term has
kind_of: ["dont:Term"] - THEN the
stale-cascaderule can traverse from the parent term to this term
Requirement: Atom model
An atom SHALL be a sub-statement carrying {idx, text, status, evidence[]} where status uses a three-value sub-lattice (unverified, doubted, verified) — atoms cannot become locked or ignored, and stale appears only as a derived assessment on entity views, not as atom status. Atom-level evidence[] is stored but MAY be projected differently in view payloads (see dont-payload-types ClaimView). An atom SHALL transition to verified when a dismiss call names it via --atom <idx>. The --atom flag SHALL be repeatable on a single dismiss call so that one body of evidence can verify multiple atoms without issuing N separate commands.
Scenario: atom has its own status
- WHEN a claim with 3 atoms has atom 0 dismissed but atoms 1 and 2 undismissed
- THEN atom 0 has status
verifiedwhile atoms 1 and 2 remainunverified
Scenario: single dismiss verifies multiple atoms
- WHEN
dont dismiss --atom 0 --atom 1 --evidence <uri>is run - THEN both atom 0 and atom 1 transition to
verifiedwith the provided evidence
Requirement: Atom-completion gate
A claim SHALL reach verified in exactly one of two ways: (1) the claim has no declared atoms and a dismiss targets the claim with sufficient evidence, or (2) the claim has declared atoms and every declared atom has reached verified — the whole claim’s verified transition is then automatic on the last atom’s transition. A whole-claim dismiss (no --atom flags) on a claim that has declared atoms with any atom still unverified or doubted SHALL be refused with error code atoms-incomplete. This atom-completion gate is an absolute data-model invariant and MUST NOT be disabled or relaxed by project configurations or methodology rules. A trust call MAY target a single atom via --atom <idx>; doubting any atom SHALL cascade the parent claim to doubted.
Scenario: auto-promotion on last atom verified
- WHEN the last unverified atom of a claim is dismissed
- THEN the whole claim automatically transitions to
verifiedwithout an explicit whole-claim dismiss
Scenario: whole-claim dismiss refused when atoms incomplete
- WHEN
dont dismiss claim:X --evidence <uri>is run without--atomflags and atom 1 is stillunverified - THEN the command is refused with
code: "atoms-incomplete"
Scenario: doubting an atom cascades to parent claim
- WHEN
dont trust claim:X --atom 0 --reason "..."is run - THEN atom 0 transitions to
doubtedand the parent claim also transitions todoubted
Requirement: Import relations
The system SHALL maintain import relations separate from core entities: imported_term { curie, label, definition, xrefs, source, imported_at }, reference { uri, title, authors, year, source, imported_at }, and prefix { prefix, uri_base, canonical, imported_at }. Imports SHALL populate reference material; they are not entities in the core store and SHALL NOT be targetable by doubt/dismiss verbs. Auto-generation of local coined terms from imports is forbidden; imported terms remain in the imported graph until explicitly adopted via a define event. imported_term and term SHALL be queried together when checking CURIE resolution.
Scenario: imported term is not targetable by trust
- WHEN a consumer tries to
trustan imported term - THEN the command is refused because imported terms are not core entities
Scenario: CURIE resolution checks both tables
- WHEN a claim references CURIE
GO:0008150 - THEN resolution searches both the
termtable and theimported_termtable
Requirement: CURIE collision semantics
When the same CURIE exists in both term (coined) and imported_term (imported), the coined term SHALL shadow the imported one: the project’s definition is authoritative for rule evaluation and dismiss decisions. Re-importing a source SHALL NOT overwrite a coined term sharing its CURIE; the import operation SHALL record a warning. To replace a coined term with an import, the workflow is: trust <term-id> (doubt the coined term), re-import, the coined term remains doubted for audit while fresh claims resolve through imported_term.
Scenario: coined term shadows imported term
- WHEN both
termandimported_termhave CURIEproj:X - THEN rule evaluation and dismiss decisions use the coined term’s definition
Scenario: re-import does not overwrite coined term
- WHEN
dont import ols GOis re-run and a coined term shares a CURIE with an imported one - THEN the coined term is unchanged and the import logs a warning
Requirement: Five MVP primitives
The system SHALL recognise exactly five schema-level primitives and SHALL require written justification for adding a sixth:
attribute—{ ident, value_type, cardinality, predicate?, doc }wherevalue_typeis one ofstring,int,float,bool,ref,tuple,enum,exprandpredicateis an optional Datalog fragment or reference to an external verifier toolderived_class—{ ident, defining_attributes, extra_predicates, doc }— a named recognition query; not inheritanceenum—{ ident, values[], doc }prefix—{ prefix, uri_base, canonical }. While authoritative prefix definitions may reside in project infrastructure (config.toml), the data model MUST log the effective prefix mappings used by any event to ensure historical queries resolve correctly even if infrastructure changes.rule—{ name, body, severity: warn|strict, doc }
Explicitly not primitives: inheritance, mixins, slot_usage, identifier prefixes as a separate concept from prefix, class-owns-slot relationships.
Scenario: exactly five primitives
- WHEN the system’s schema is queried
- THEN it recognises exactly five primitive kinds: attribute, derived_class, enum, prefix, rule
Scenario: adding a sixth primitive requires justification and version bump
- WHEN a contributor proposes a sixth primitive
- THEN the pull request must demonstrate that no combination of existing primitives expresses the capability being added, and the addition requires a minor-version bump and changelog entry
Scenario: derived_class is recognition not inheritance
- WHEN a derived class query matches an entity
- THEN the entity is recognised as a member; multiple derived classes may apply simultaneously
Scenario: inheritance is not a primitive
- WHEN a consumer expects class inheritance
- THEN the system does not provide it; membership is by predicate match only
Requirement: Author identity format
Authors SHALL be identified by a string <actor-kind>:<id> where actor-kind is one of human, llm, tool, or ci, and id is an opaque stable identifier within that kind. Anonymous events are strictly forbidden; background processes, rule engines, and automated workflows MUST use a defined actor-kind (e.g., tool:rule-engine). Parsing SHALL split on the first : only — the id portion may itself contain colons. Empty id is invalid; actor_kind outside the four-value set is invalid. The actor_kind set is closed by design; extending it requires a minor-version bump and a parser upgrade gate. The convention is shared with beads and wai for cross-tool audit correlation.
Scenario: author string with colons in id
- WHEN author is
tool:github-actions:prod - THEN parsing yields
actor_kind = "tool"andid = "github-actions:prod"
Scenario: empty id is rejected
- WHEN author is
human: - THEN validation rejects it as invalid
Scenario: all four valid actor kinds are accepted
- WHEN author strings
human:alice,llm:claude-3,tool:ci-bot, andci:github-actionsare validated - THEN all four are accepted
Scenario: unknown actor kind is rejected
- WHEN author is
bot:gpt-4 - THEN validation rejects it because
botis not in the allowed set{human, llm, tool, ci}
Requirement: Confidence semantics
The confidence field on claims SHALL be stored as authored by the LLM, uncalibrated. The system SHALL NOT attempt to Platt-scale or post-process confidences in v0.3. Consumers treating confidences as probabilities SHALL read them as the author’s stated number, not as a calibrated estimate.
Scenario: confidence stored without transformation
- WHEN an LLM concludes a claim with confidence 0.78
- THEN the stored confidence is exactly 0.78, not a calibrated or scaled value
Scenario: out-of-range confidence is rejected
- WHEN a claim is concluded with
confidence: 1.3(outside 0.0-1.0) - THEN the command is refused with
code: "schema-mismatch"
Requirement: Provenance tracking
Every entity SHALL carry a provenance object recording at minimum author (AuthorString) and origin (string). Claims MAY additionally carry model, session_id, and context_hash in provenance. Every status transition SHALL record author, timestamp, and reason or evidence.
Scenario: claim provenance includes model
- WHEN a claim is concluded by
llm:claude-opus-4.7 - THEN provenance includes
author,origin: "llm", andmodel: "claude-opus-4.7"
Scenario: every transition records author and timestamp
- WHEN a claim is trusted
- THEN the event records
author,at(timestamp), andreason