@chronicle.app/schema
v0.5.0
Published
Minimal Chronicle extraction vocabulary and generated TypeScript/Zod schemas
Readme
@chronicle.app/schema
A deliberately small extraction vocabulary with generated TypeScript interfaces
and Zod validators. The canonical file is chronicle.ttl; edit it and run
npm run schema:generate from the repository root to regenerate the TypeScript/Zod
schemas. npm run schema:check verifies that the committed output matches the
ontology without rewriting it. Write each term's rdfs:comment by the rules in
WRITING.md.
Documentation site
The schema is documented at https://schema.chronicle.app, built from
chronicle.ttl and the example records in examples.ttl. The
site lives in apps/schema-site, which also describes
the examples format.
Other vocabularies
Chronicle's vocabulary stands on its own. It borrows schema.org's model and
names, and chronicle.ttl points each term to the closest schema.org term as a
hint for mapping data between them: skos:exactMatch for the same meaning,
skos:closeMatch for a similar one, and skos:broadMatch when schema.org's
term is broader. A reasoner draws nothing from SKOS mappings, so the hints
don't claim the vocabularies are compatible. They aren't: values, scope, and
structure differ in places. No rdfs:subClassOf, rdfs:subPropertyOf,
owl:equivalentClass, or owl:equivalentProperty points to another
vocabulary, and the schema site's build fails on one.
alignments/ holds one file for each vocabulary that Chronicle's
terms relate to, schemaorg.ttl and activitystreams.ttl. Each names the
release its terms were checked against, and holds:
doc:note: how a Chronicle term differs from the term it maps to, written by the rules in WRITING.md.doc:unlike: a term with the same name as a Chronicle term but a different meaning, such as schema.org'sProject.- The mappings to any vocabulary other than schema.org. They can change without a vocabulary release.
<name>.terms.json lists the classes and properties of the release. To move to
a newer one, run node scripts/snapshot-vocabulary.js schemaorg <release> (or
activitystreams), update doc:release, and fix what the schema site's build
reports. The site shows each term's relations, has a page for each vocabulary,
and fails when a mapping names a term the release does not have, or when a
Chronicle term shares a name with a term it neither maps to nor marks unlike.
Names are also compared without the endings in doc:nameEnding, so
LikeAction is compared with Activity Streams' Like. To relate the
vocabulary to another one, add its alignment, its .terms.json, and a
comparison in the site's vocabularies/.
Vocabulary
Base: an identity-bearing node; carriessourceId.Entity: a thing described by a source; addsnameandurl.Action: an occurrence; adds a single entity-valuedobject.
Plugins extend these roots only where their outputs require it: Things adds task lifecycle actions and collections/tags; shell adds commands, people, and machine realms; iMessage adds messages and attachment media; Claude Code adds threads, software agents, and model instruments; Safari adds page views; Arc Timeline adds visits, trips, places, and journeys; WhatsApp adds group channels, members, and quoted replies. Shared properties cover source identity, event time, agents, membership, authors, and recipients.
Text, URL, DateTime, and Number are datatypes. A
DateTime is a Date or an EDTF string at the source's precision, such as 1987 or
XXXX-03-12; isDateTime checks a string. A property takes a list of values
unless its SHACL shape sets sh:maxCount 1. Records carry @type and at least one of @key (a nonempty list of
identity fields or computed key entries) or @id (an existing identity). A
record can also carry @assertedAt, the instant its source observed it, and
@asserts, the predicates it states completely ('*' for all of them).
Identity validation checks that declaration, not the existence or hash of the
referenced fields. Source-specific extraction owns the values; never invent IDs
to make a record pass validation.
import { ActionSchema, EntitySchema } from '@chronicle.app/schema';
// Synthetic example data.
const entity = EntitySchema.parse({
'@type': 'Entity',
'@key': ['sourceId'],
sourceId: 'example-record-1',
name: 'Example record',
url: 'https://example.com/record/1',
});
const action = ActionSchema.parse({
'@type': 'Action',
'@key': ['sourceId'],
sourceId: 'example-action-1',
object: entity,
});Each type has a TypeSchema validator and a TypeAndChildrenSchema validator
that also accepts its descendants. BaseAndChildrenSchema accepts all declared
record types. Validation checks nested identities, property types, URL syntax,
and cardinality. Unknown object fields are stripped by Zod; undeclared record
types are rejected. Import interfaces such as Entity and Action for static
typing; use validators at runtime to enforce identity requirements.
Writing the ontology
chronicle.ttl says what terms mean in OWL, and how records use them in SHACL:
- A class is an
owl:Class.:Baseis the disjoint union of:Entityand:Action, so nothing is both. - A property is an
owl:ObjectPropertywhen it links to records and anowl:DatatypePropertywhen it holds values.:sameAstakes either, so it is anrdf:Property. - A property's
rdfs:domainandrdfs:rangename one term, or several in oneowl:unionOf. Don't add a secondrdfs:domainorrdfs:range: OWL reads two as both at once, so the generator rejects it. - A datatype is an
rdfs:Datatypedefined over XSD. Each accepts the plain strings that Chronicle JSON becomes in JSON-LD as well as typed values, such as anxsd:dateTimefor an instant. - A property can name its inverse with
:inverseName, such ashasParton:isPartOf. A record can give the relation under either name, so a whole can list its parts, and only the forward edge is stored. The generatedINVERSE_PROPERTIESmaps each inverse name to its forward property. - A number property can say what form its values take with
:metric. Agaugeis a level or a rate, such as a heart rate, and combines as a mean, minimum, or maximum. Asumis an amount, such as a number of steps, and adds up. A metric's:unitis the one unit all its values are in, as a UCUM code such as/min. The generatedMETRIC_PROPERTIESgives each metric's form and unit. - A SHACL property shape after a property holds its record rules.
sh:maxCount 1makes it take one value. A shape withsh:targetClassandsh:minCount 1makes it required on that class.
Generation and compatibility
The generator turns the ontology into TypeScript types and Zod validators. It
reads terms through scripts/terms.js, which the schema site also uses. It
handles inheritance, including a class with two parents, domains and ranges,
and SHACL counts, and it fails for cyclic inheritance, undeclared referenced
classes, a property with two domains or ranges, or a metric that isn't a
gauge or a sum of numbers in one unit. The package has no
persistence metadata, derived effects, or runtime filesystem parser.
Generated runtime code depends only on Zod; the TTL is also included in the package.
Schema versions
The npm package follows Chronicle’s shared software version in package.json.
The ontology has an independent version declared with owl:versionInfo in
chronicle.ttl, exported as the generated SCHEMA_VERSION constant. Software
releases can advance without changing the vocabulary version.
chronicle.ttl is the only editable ontology; historical versions are preserved
by immutable software Git tags named v<package-version> and versioned npm packages.
Release notes map software versions to vocabulary versions.
The package exports @chronicle.app/schema/chronicle.ttl; the former
@chronicle.app/schema/schema.ttl export remains an alias for compatibility.
Use patch releases for description corrections that preserve meaning and minor
releases for anything else. Until 1.0, a minor release can rename or remove
terms, change what they mean, or add constraints, so data that validates against
one minor version may not validate against the next. From 1.0, those changes need
a major release. Term identifiers such as https://schema.chronicle.app/Task are
unversioned, so don't reuse one for a different concept. Give that concept a new
term.
Snapshot paths use the vocabulary version, not the npm package version.
The publication convention is
https://schema.chronicle.app/releases/<version>/chronicle.ttl, with matching
documentation under the same release path. Published snapshots must never be
overwritten or removed by later deployments. The unversioned /chronicle.ttl
and term pages serve main, so main holds only vocabulary that is ready to
publish: work on new terms in a branch until it is ready. Any change to
chronicle.ttl after a release must raise owl:versionInfo. While the
vocabulary is in flux, npm run schema:check doesn't check this, so run
npm run version:check before a release to compare against the latest release
tag. See Preparing a release for the
release procedure.
The vocabulary is intentionally small. Add terms only when a plugin needs them; regenerate and add focused tests alongside each addition.
Node.js 22.13+. MIT covers the ontology, generator, generated code, and docs; see LICENSE.
