idiolect
idiolect translates a record from one community's schema into another's while keeping the translation, its evidence, and its social standing inspectable. It runs on AT Protocol and uses Panproto for schema comparison and lens execution.
The project names its unit of local variation the idiolect. A dialect records a community convention, while a language is the substrate on which those local choices meet. None of the three requires a central schema registry.
Start from your task
Beginner: get a result in about five minutes
Install the checked-out CLI and resolve a record, then validate it against its Lexicon. These first two steps produce a concrete resolution and validation result before introducing lens laws, network publication, or the project's social records.
Project integration: connect an existing system
Choose the guide that matches the work in front of you: generate Rust and TypeScript types, publish a lens, index the firehose, or run the query API. Each guide links to the exact crate, CLI, or wire reference needed during implementation.
Advanced and formal: extend the model
Begin with lens semantics and laws or the vocabulary knowledge graph, then move to the observer protocol, Lexicon evolution policy, and crate extension points. The reading-path page gives a longer route through each level.
How the book is organized
The book keeps the four Diátaxis functions separate:
- The tutorial teaches through one runnable sequence.
- The guides give procedures for particular tasks.
- The concepts explain the mechanisms and their limits.
- The reference records exact APIs, fields, and commands.
Use the glossary for short definitions. The paths above cross the four sections, but they do not turn a tutorial into reference material or a conceptual explanation into a procedure.
Architecture
flowchart TB
subgraph sources["Source of truth"]
LEX["lexicons/dev/idiolect/*.json"]
SPEC["*-spec/ (orchestrator, observer, verify, cli)"]
end
subgraph codegen["Codegen"]
CG["idiolect-codegen<br/>emit · check · check-compat"]
end
subgraph emitted["Emitted surfaces"]
RECS["idiolect-records (Rust)"]
NPM["@idiolect-dev/schema (TS)"]
end
subgraph runtime["Runtime"]
PDS[("ATProto PDS<br/>+ firehose")]
IDX["idiolect-indexer"]
ORC["idiolect-orchestrator"]
OBS["idiolect-observer"]
VER["idiolect-verify"]
MIG["idiolect-migrate"]
LENS["idiolect-lens"]
end
LEX --> CG
SPEC --> CG
CG --> RECS
CG --> NPM
PDS -->|commits| IDX
IDX --> ORC
IDX --> OBS
OBS -->|observation records| PDS
LENS -->|reads/writes records| PDS
LENS --> MIG
LENS --> VER
Lexicons under lexicons/dev/idiolect/ are the source of truth for the record
family. Code generation derives Rust types and TypeScript validators from those
files. The orchestrator queries, observer methods, and verifier runners each add
a declarative JSON specification; code generation emits their dispatch and wire
integration. This separation is the declarative boundary (DB): record and
method taxonomies live in data, while runtime code implements their behavior.
The DB lets reviewers distinguish generated contracts from handwritten runtime
semantics.
Stability
idiolect is pre-1.0. Releases in the 0.x series may include
arbitrary breaking changes between minor versions. Pin to an exact
version if you depend on this project, and read the
changelog
before bumping. See Stability and versioning.
Choose a reading path
The book retains four Diátaxis sections because each serves a different kind of work: the tutorial teaches through a sequence, the guides give procedures, the concepts explain the model, and the reference pages record exact contracts. The paths below cross those sections without merging their functions.
Beginner: resolve and validate a record
Start with Install the CLI and fetch a lens. It produces a concrete result with the checked-out CLI, then points to Validate a typed record. Continue through the tutorial only when you want to apply and verify a lens or publish a recommendation.
Intermediate: integrate a project
Begin with the task closest to your project:
- Run codegen to derive Rust and TypeScript surfaces from a Lexicon family.
- Author a community vocabulary to add governed, machine-readable terms.
- Publish and resolve a lens to put a translation on the network.
- Index a firehose to consume repository events.
- Run the orchestrator HTTP API to query indexed records.
Use the corresponding crate, Lexicon, CLI, or HTTP API page when implementation work requires an exact signature or wire contract.
Advanced: extend or analyze the system
Read Lens semantics and laws before changing lens execution or verification, and read The vocabulary knowledge graph before extending vocabulary inference. The Observer protocol and Lexicon evolution policy connect those formal objects to runtime behavior. Then use the crate reference to find extension traits and the stability policy to decide which contracts downstream code may rely on.
Tutorial
This tutorial follows one published Panproto lens from discovery to recommendation. The route is linear, and the first chapter produces a live, read-only result in about five minutes:
- Install the CLI and fetch a lens from the idiolect project account.
- Validate a typed record through the generated NSID dispatcher.
- Apply the lens to a small JSON record with Panproto 0.71.0.
- Verify the round trip over a three-record corpus.
- Publish a recommendation and its community record to your personal data server (PDS).
Each chapter starts from the repository state left by the previous one. The first four chapters do not require an ATProto account; Chapter 5 does, because it writes public records. Use the Guides for task-specific procedures and the Concepts for the underlying model.
Prerequisites
You need Git, Rust 1.95 or later, Cargo, and network access. Clone the idiolect repository in Chapter 1 and run the remaining commands from its root. If you plan to complete Chapter 5, you also need an ATProto account and an app password for that account.
Install the CLI and fetch a lens
This chapter gets you from a fresh checkout to a live idiolect record in about five minutes. You need Git, Rust 1.95 or later, and a network connection. You do not need an ATProto account.
Install from the checkout
Clone the repository and install the command-line interface (CLI):
git clone https://github.com/idiolect-dev/idiolect
cd idiolect
cargo install --locked --path crates/idiolect-cli
The first build may take a few minutes. Confirm that the installed binary comes from this checkout:
idiolect version
idiolect 0.12.1
Resolve the project account
A decentralized identifier (DID) names an account. Its DID document identifies the account's personal data server (PDS). Resolve the idiolect project DID:
idiolect resolve did:plc:wdl4nnvxxdy4mc5vddxlm6f3
The JSON response should identify idiolect.dev and include a pds_url.
Resolution is a separate step because records move with an account when its PDS
changes.
Fetch the tutorial lens
An AT-URI identifies one record by DID, collection, and record key. Fetch the published tutorial lens:
idiolect fetch \
at://did:plc:wdl4nnvxxdy4mc5vddxlm6f3/dev.panproto.schema.lens/tutorial-rename-sort-string-to-text
The result is a real dev.panproto.schema.lens record. Its sourceSchema and
targetSchema fields identify the schemas it connects, while blob contains
the Panproto lens definition. Keep the checkout: the next chapter validates a
typed record with the same libraries the CLI uses.
Validate a typed record
The record fetched in Chapter 1 came from the network. We now use a bundled fixture so that validation has a stable input and a reproducible result.
An ATProto lexicon defines
a record's fields and constraints. idiolect-records generates a Rust type for
each dev.idiolect.* lexicon and dispatches incoming JSON by its
namespaced identifier (NSID).
Run the valid case
From the repository root, run the tutorial validator:
cargo run --quiet \
--manifest-path scripts/publish-tutorial-lens/Cargo.toml \
--bin validate-tutorial-record
validated dev.idiolect.dialect: ud-en-2026
The executable serializes the bundled Dialect fixture to JSON and sends it
through the runtime dispatcher:
let value = serde_json::to_value(examples::dialect())?;
let record = decode_record(&Dialect::nsid(), value)?;
Dialect::nsid() selects the generated type. Deserialization then checks the
required fields and the field formats represented by that type. The returned
AnyRecord::Dialect value is safe to pass to code that expects a typed dialect.
See a rejection
The same executable can remove the required createdAt field before decoding:
cargo run --quiet \
--manifest-path scripts/publish-tutorial-lens/Cargo.toml \
--bin validate-tutorial-record -- --invalid
rejected invalid dev.idiolect.dialect record: record deserialization failed: missing field `createdAt`
Validation occurs here: malformed JSON is rejected before application logic receives a record. Chapter 3 applies the live lens from Chapter 1 to a small source record.
Apply the lens
A lens translates a
record between two schemas. Its forward operation, get, returns the target
record and a complement that
retains any source information needed by the reverse operation, put.
This chapter applies the lens fetched in Chapter 1 to the following source record:
{
"text": "hello, world"
}
Confirm the Panproto version
The runnable package pins the Panproto crates used here to 0.71.0:
panproto-lens = { git = "https://github.com/panproto/panproto.git", tag = "v0.72.0" }
panproto-schema = { git = "https://github.com/panproto/panproto.git", tag = "v0.72.0" }
The idiolect workspace uses the same Panproto version.
Run get
From the repository root, run the supplied client:
cargo run --quiet \
--manifest-path scripts/publish-tutorial-lens/Cargo.toml \
--bin apply-tutorial-lens
target_record = {
"text": "hello, world"
}
The executable creates one HTTP client, gives it to the lens and schema
resolvers, and calls apply_lens:
let out = apply_lens(
&resolver,
&loader,
&protocol,
ApplyLensInput {
lens_uri,
source_record,
source_root_vertex: None,
},
)
.await?;
The JSON value is unchanged because this tutorial lens renames a schema sort
from string to text; it does not rename the record's text field. The
unchanged value records a successful run: the runtime resolved the lens, loaded
both schemas, instantiated Panproto 0.71.0, and produced a target-schema value.
The returned out.complement belongs with this application of the lens.
Passing it to apply_lens_put would reconstruct the source record. Chapter 4
checks that reconstruction over several inputs.
Verify the round trip
A verification is a
typed claim about a lens property. Here we test whether put(get(source))
returns source for three records: ordinary text, an empty string, and Unicode
text.
Run the verifier
From the repository root:
cargo run --quiet \
--manifest-path scripts/publish-tutorial-lens/Cargo.toml \
--bin verify-tutorial-lens
result = Holds
kind = RoundtripTest
tool = idiolect-verify/roundtrip-test 0.12.1
The executable gives the corpus to RoundtripTestRunner and runs it against
the published lens:
let runner = RoundtripTestRunner::new(
resolver,
loader,
Protocol::default(),
corpus,
);
let verification = runner.run(&target).await?;
For each source record, the runner calls apply_lens and then
apply_lens_put. Holds means that all three records round-tripped exactly.
It supports a claim about this corpus; it is not a proof over every possible
record.
Distinguish a finding from a failure
A counterexample produces a typed verification with result = Falsified.
That finding is a successful verifier run and can be published. Transport,
schema-loading, and malformed-input problems instead return VerifyError,
because the runner could not evaluate the property.
The verification value is already shaped like a
dev.idiolect.verification record. Chapter 5 publishes a recommendation that
identifies the same lens and the source schema under which it applies.
Publish a recommendation
A recommendation
records that a community endorses a lens path under stated conditions. The
tutorial publisher creates a small community record for your account, then
publishes a recommendation whose conditionSourceIs points at the source
schema used in Chapters 3 and 4.
This chapter writes two public records to your PDS. The earlier chapters were read-only.
Create an app password
Use an ATProto account intended for this exercise. Create an app password in your account settings, then set the PDS URL, handle, and password in your shell. The example below uses Bluesky's hosted PDS; replace the URL if your account uses another server.
export PDS_URL='https://bsky.social'
export ATPROTO_HANDLE='your-handle.bsky.social'
export ATPROTO_PASSWORD='xxxx-xxxx-xxxx-xxxx'
Avoid placing a real password in a shared shell history. An app password can be revoked without changing the account's main password.
Publish both records
From the repository root:
cargo run --quiet \
--manifest-path scripts/publish-tutorial-lens/Cargo.toml \
--bin publish-tutorial-recommendation
unset ATPROTO_PASSWORD
The PDS assigns fresh record keys, so the command prints different AT-URIs for each run:
community at://did:plc:.../dev.idiolect.community/...
recommendation at://did:plc:.../dev.idiolect.recommendation/...
The recommendation contains three load-bearing references: (i) the community record just published, (ii) the tutorial's source schema as its applicability condition, and (iii) the tutorial lens as its one-step lens path. The generated Rust types validate those fields before the publisher sends the record.
Read the result back
Copy the printed recommendation AT-URI into the CLI:
idiolect fetch at://did:plc:.../dev.idiolect.recommendation/...
The returned JSON should contain issuingCommunity, conditions, lensPath,
and occurredAt. This output confirms that the recommendation was published.
Continue with Publish a lens when you need to author
the schemas and lens rather than reuse the tutorial records.
Guides
The guides start from an integration task and end with an operational result. If you have not yet resolved and translated a record, begin with the tutorial.
Project-integration path
For a first application, take these steps in order:
- Index a firehose into your own handler and choose a cursor store.
- Run the orchestrator HTTP API over the indexed catalog.
- Configure authenticated sessions before adding a record-publishing path.
This path connects the read side, query side, and authenticated write side without changing the function of any individual guide.
Task index
| Guide | When to reach for it |
|---|---|
| Index a firehose | You want to stream commits from a PDS firehose into your own indexer. |
| Run the orchestrator HTTP API | You want a read-only query surface over cataloged records. |
| Run the observer daemon | You want to fold encounter-family records into observation records. |
| Author a verification runner | You want to add a new property kind to the verifier. |
| Publish and resolve a lens | You have a panproto lens and want it on the network. |
| Migrate records across a revision | A schema you depend on changed, and you want to lift records across the change. |
| Configure OAuth sessions | You want a session store the publishing path can use. |
| Run codegen | You edited a lexicon or a spec and need the generated tree refreshed. |
| Author a community vocabulary | You want to extend an open enum or publish a typed knowledge graph. |
| Bundle records into a dialect | You want to ship a coherent set of idiolects as one canonical bundle. |
For extension traits, feature flags, wire shapes, and endpoint parameters, use the advanced reference path.
Index a firehose
idiolect-indexer composes
three boundaries around an AT Protocol
firehose:
EventStream: yieldsRawEvents from a PDS firehose. Shipped impls:JetstreamEventStream(Jetstream websocket feed) andTappedEventStream(the at-proto-native firehose viatapped).RecordHandler<F: RecordFamily = IdiolectFamily>: handles one decodedIndexerEvent<F>. The family parameter narrows the handler to the records the indexer should not skip. Everything outside the family is dropped before decode.CursorStore: persists the last-acknowledged sequence number per subscription so a restart resumes where the previous run left off.
drive_indexer composes the three. drive_idiolect_indexer is
the convenience alias when the family is IdiolectFamily.
Minimum viable indexer
use idiolect_indexer::{ drive_idiolect_indexer, FilesystemCursorStore, IndexerConfig, IndexerEvent, JetstreamEventStream, RecordHandler, }; use idiolect_records::IdiolectFamily; struct PrintHandler; impl RecordHandler<IdiolectFamily> for PrintHandler { async fn handle( &self, event: &IndexerEvent<IdiolectFamily>, ) -> Result<(), idiolect_indexer::IndexerError> { println!("{} {} {:?}", event.did, event.collection, event.action); Ok(()) } } #[tokio::main] async fn main() -> anyhow::Result<()> { let mut stream = JetstreamEventStream::connect( "wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=dev.idiolect.*", ).await?; let cursors = FilesystemCursorStore::open("./cursor.json")?; let handler = PrintHandler; let config = IndexerConfig::default(); drive_idiolect_indexer(&mut stream, &handler, &cursors, &config).await?; Ok(()) }
Add features:
cargo add idiolect-indexer \
--features firehose-jetstream,cursor-filesystem,reconnecting
cargo add anyhow tracing-subscriber
cargo add tokio --features macros,rt-multi-thread
reconnecting wraps the inner stream in an exponential-backoff
loop. cursor-sqlite swaps the filesystem cursor store for a
SQLite-backed one. resilience adds retry and circuit-breaker
handler wrappers.
Family-typed dispatch
The IdiolectFamily parameter is a typed predicate over NSIDs.
A commit whose collection is not in the family drops before
decode, so an upstream PDS adding a record type ahead of your
codegen run does not halt the loop. To handle two families
(idiolect plus a downstream community's lexicons), compose:
use idiolect_records::{IdiolectFamily, OrFamily};
struct MyFamily;
// `MyFamily` impls `RecordFamily`.
let handler: MyHandler<OrFamily<IdiolectFamily, MyFamily>> = ...;
OrFamily<F1, F2> recognizes every NSID either side claims. Its
AnyRecord is OrAny, a tagged union over the two halves.
detect_or_family_overlap audits a probe set at boot so a
configuration mistake does not silently shadow the right-side
family.
Cursor semantics
drive_indexer calls
CursorStore::commit(subscription_id, seq) after the handler
returns Ok. Make handler work idempotent to obtain at-least-once
processing across restarts. Exactly-once processing requires a custom
driver that commits application state and the cursor in one storage
transaction; drive_indexer cannot make those writes atomic.
Errors propagate as IndexerError. The variants distinguish
transport failures (Stream), decode failures (Decode),
family-contract bugs (FamilyContract, fired only when
contains returns true but decode returns None),
handler-defined errors (Handler), missing-body events
(MissingBody), and cursor-store failures (Cursor).
Observability
Every shipped surface logs through tracing. Wire a
subscriber:
tracing_subscriber::fmt()
.with_env_filter("idiolect_indexer=info")
.init();
You will see one log line per accepted commit, one per skipped commit (debug level), and one per cursor commit. The orchestrator exposes a Prometheus surface. See Run the orchestrator HTTP API.
Run the orchestrator HTTP API
idiolect-orchestrator
serves a read-only HTTP API over a record
catalog.
CatalogHandler fills the catalog from the indexer; the HTTP server
queries that same state.
What it serves
GET /healthz,GET /readyz— liveness and readiness.GET /metrics— Prometheus exposition.GET /v1/stats— record counts per kind.- One pair of REST and XRPC endpoints per declarative query in
orchestrator-spec/queries.json. The 0.12.1 surface includes: bounties (open, want-lens, by-requester), adapters (by framework, by invocation protocol, with verification), recommendations (starting from a source schema), verifications (by lens, by kind), communities (by member, by name), dialects (for community), beliefs (about a record, by holder), and vocabularies (with world, by name).
The full surface is in the HTTP query API reference.
Run it
The shipped daemon binary lives behind the daemon feature:
cargo install --path crates/idiolect-orchestrator \
--features daemon
The daemon feature pulls in catalog-sqlite, query-http, the
indexer's tapped firehose, and the SQLite cursor store. Configure and
run it with environment variables:
IDIOLECT_TAP_URL=http://localhost:2480 \
IDIOLECT_ORCHESTRATOR_DB=./catalog.sqlite \
IDIOLECT_ORCHESTRATOR_CURSORS=./cursors.sqlite \
IDIOLECT_HTTP_ADDR=127.0.0.1:8787 \
idiolect-orchestrator
Set IDIOLECT_TAP_ADMIN_PASSWORD if the tap requires it and
IDIOLECT_SUBSCRIPTION_ID when several subscriptions share a cursor
database. The daemon does not parse --catalog or --bind flags.
Query it
curl -s http://localhost:8787/v1/stats | jq
curl -s 'http://localhost:8787/v1/bounties/open' | jq
curl -s 'http://localhost:8787/v1/adapters?framework=hasura' | jq
curl -s 'http://localhost:8787/v1/verifications?lens_uri=at://...' | jq
curl -s 'http://localhost:8787/v1/verifications/sufficient?lens_uri=at://...&kinds=roundtrip-test&hold=true' | jq
The generated CLI exposes the spec entries that declare a cli
mapping:
idiolect orchestrator bounties
idiolect orchestrator adapters --framework hasura
idiolect orchestrator verifications --lens_uri at://...
Other HTTP queries have no CLI subcommand. The dispatcher in
crates/idiolect-cli/src/generated.rs is generated from the
same spec as the HTTP routes.
Add a query
Queries live in orchestrator-spec/queries.json (a single JSON
document with a top-level queries array). To add one:
- Add a new entry to the array. Each entry declares the query's name, description, parameters, predicate (a panproto-expr expression), and the record kind it iterates over.
- Run
cargo run -p idiolect-codegen. - The generated tree picks up the HTTP route, XRPC alias,
query-string parser, and response shape. Add a
climapping if the query also needs a CLI subcommand.
The hand-written part is the panproto-expr predicate inside the spec entry. The generated tree handles routing, parameter parsing, and response encoding.
Observability
The orchestrator exposes /metrics in Prometheus exposition format
and emits structured tracing logs. The metric names and label sets
are defined in crates/idiolect-orchestrator/src/http.rs.
Deployment
A pre-built container image ships at
ghcr.io/idiolect-dev/orchestrator:<version> per release. The
image is signed with sigstore keyless. Verification policy is
in
docs/ci-cd.md.
Run the observer daemon
The observer folds records from a
firehose
through an ObservationMethod and emits a
dev.idiolect.observation at each configured flush. Encounters remain
the event log; observations carry the computed summary.
The crate is
idiolect-observer.
Its daemon binary lives behind the daemon feature.
What it produces
A dev.idiolect.observation record carries:
- the observer's DID,
- a structured
methoddescriptor (name, version, optional parameters and code reference), - a
scopedescribing which records the aggregation covers, - the method's
outputpayload (shape is method-defined), - the observation's publication timestamp and visibility.
The deliberation-tally method currently places its result in
observation.output; dev.idiolect.deliberationOutcome is a separate
record kind.
Choose the publication boundary
Observations are content-addressed and signed, like any other ATProto record. A consumer reading an observation can:
- verify the signer DID,
- ask the indexer for the underlying encounters and re-fold them independently,
- treat the observation as a soft assertion of fact, not a single source of truth.
This record boundary lets several observers publish different folds without erasing the underlying events.
Run the daemon
cargo install --path crates/idiolect-observer --features daemon
The binary is configured through environment variables, not command
flags. Set IDIOLECT_OBSERVER_DID; optionally set
IDIOLECT_TAP_URL, IDIOLECT_TAP_ADMIN_PASSWORD,
IDIOLECT_OBSERVER_CURSORS, IDIOLECT_FLUSH_EVENTS, and
IDIOLECT_PDS_URL. It wires:
- A firehose stream (tapped, via the indexer's
firehose-tappedfeature, transitively pulled in bydaemon). - A SQLite cursor store.
- A
CorrectionRateMethodinsideObserverHandler<M, P>. - A flush schedule that triggers observation publication.
If IDIOLECT_PDS_URL is unset, the reference daemon uses an in-memory
publisher and persists no observation records. Its PDS branch does not
yet supply authentication; use a wrapper binary with an authenticated
PdsWriter for production publication.
Bundled methods
The spec at observer-spec/methods.json declares nine bundled
methods. Each lives in crates/idiolect-observer/src/methods/.
| Method | Folds |
|---|---|
correction-rate | Per-lens correction counts grouped by reason. |
encounter-throughput | Encounter traffic by kind and downstream result. |
verification-coverage | Per-lens verification counts by kind, result, and distinct verifiers. |
lens-adoption | Per-lens encounter count and distinct invokers. |
action-distribution | Encounter counts grouped by use.action, optionally rolled up through a vocab. |
purpose-distribution | Encounter counts grouped by use.purpose. |
basis-distribution | Record counts grouped by basis variant, bucketed by record kind. |
attribution-chains | dev.idiolect.belief counts by holder and subject. |
deliberation-tally | Per-statement per-stance deliberationVote counts (see the note below). |
The current spec declares all nine methods in record form; they consume
&IndexerEvent<IdiolectFamily>. The library also supports instance-form
methods over panproto WInstance through InstanceMethodAdapter.
default_methods() returns boxed instances of every
record-form method; instance-form methods need a caller-supplied
schema resolver and are constructed individually.
Add a method
Edit observer-spec/methods.json, add the method's entry, run
cargo run -p idiolect-codegen. The generated descriptor table
picks up the new method. Implement ObservationMethod (or
InstanceMethod) in
crates/idiolect-observer/src/methods/<module>.rs and add it
to the default_methods() constructor.
Operational notes
- Observers should run with their own DID, distinct from the DIDs whose encounters they observe.
- Multiple observers may publish observations of the same scope. Consumers can require quorum among of trusted observers before treating an observation as authoritative.
deliberation-tally output
The shipped deliberation-tally method emits its
per-statement per-stance vote counts inside an
observation.output blob, not as a typed
dev.idiolect.deliberationOutcome record. The data shape is
the same; the publication surface differs. Publishing a typed outcome
requires separate application code.
Author a verification runner
A verification
runner takes a lens and typed inputs, then returns a Verification
record with result set to Holds, Falsified, or Inconclusive.
The result record is publishable as a
dev.idiolect.verification; downstream consumers reading the
record can decide whether to trust it.
The runner trait:
pub trait VerificationRunner: Send + Sync {
fn kind(&self) -> VerificationKind;
fn tool(&self) -> Tool;
async fn run(&self, target: &VerificationTarget) -> VerifyResult<Verification>;
}
A falsified property returns
Ok(Verification { result: Falsified, ... }), not an error.
A falsified result is a finding the runner is meant to report,
not an error. VerifyError is reserved for input-shape,
transport, or irrecoverable-state failures.
Shipped runners
Four kinds ship in crates/idiolect-verify/src/:
| Kind | Runner |
|---|---|
roundtrip-test | RoundtripTestRunner |
property-test | PropertyTestRunner |
static-check | StaticCheckRunner |
coercion-law | CoercionLawRunner |
The lexicon's verification.kind field is open-enum and lists
additional kinds (formal-proof, conformance-test,
convergence-preserving). Those kinds are recognized but not
shipped as runners. Communities that need them author their own.
Add a runner kind
Adding a kind requires two source edits and regeneration:
- Add an entry to
verify-spec/runners.jsondeclaring the kind and its description. Runcargo run -p idiolect-codegen. The generated kind taxonomy (crates/idiolect-verify/src/generated.rs) picks up the new kind. - Implement
VerificationRunnerfor a struct in a new module undercrates/idiolect-verify/src/. Re-export it fromlib.rs.
The runner's kind() returns the new VerificationKind variant. Its
run method performs the check and returns a Verification. Use
idiolect_verify::runner::build_verification to package the result
with its structured property field.
Test
Every shipped runner has integration tests that exercise the
holds / falsified / inconclusive cases against fixtures. New
runners follow the same pattern: a fixture with a known result,
a call to runner.run(...), an assertion on the returned
Verification.
Publish a verification record
Once you have a Verification, publish it via
idiolect_lens::RecordPublisher:
use idiolect_lens::RecordPublisher;
let publisher = RecordPublisher::new(writer, my_did);
let resp = publisher.create(&verification).await?;
println!("published: {}", resp.uri);
The publisher serializes the record, inserts $type, and forwards it
to the configured PdsWriter. For OAuth-bound writes, combine
SigningPdsWriter with a DpopProver from the pds-reqwest and
dpop-p256 features.
CLI surface
The idiolect verify <kind> subcommand wraps each shipped
runner against a live PDS via PdsResolver + PdsSchemaLoader:
idiolect verify roundtrip-test --lens AT_URI [--corpus PATH] [--pds-url URL] [--verifier-did DID]
idiolect verify property-test --lens AT_URI --corpus PATH [--budget N] [--pds-url URL] [--verifier-did DID]
idiolect verify static-check --lens AT_URI [--pds-url URL] [--verifier-did DID]
idiolect verify coercion-law --lens AT_URI --vcs-url URL --standard STD [--version V] [--violation-threshold N] [--verifier-did DID]
Corpus files may be JSON arrays or JSON Lines. The
property-test generator cycles through the corpus by index,
and --budget controls case count. The CLI prints the typed
Verification record as JSON and exits non-zero on Falsified
or Inconclusive. Publishing the result is a separate step:
pipe to idiolect publish verification --record -.
Publish and resolve a lens
A lens on the network depends on three records:
- A
dev.panproto.schema.lensrecord on a PDS, carrying the protolens (or protolens chain) blob and pointers to the source and target schemas. - Its source schema, as a
dev.panproto.schema.schemarecord. - Its target schema, as a
dev.panproto.schema.schemarecord. AgetSchemaXRPC response from a panproto VCS may supply either schema instead.
Consumers resolve the lens record by at-uri. The runtime instantiates against the two schemas.
Build the lens
The shortest path is to derive it from a schema diff:
schema lens generate --protocol atproto old.json new.json --save chain.json
schema is the panproto CLI. chain.json is a protolens chain in
panproto's serialized format. See the
panproto book for what the chain
looks like and how to inspect it.
Stage it
use idiolect_records::{AtUri, Datetime, PanprotoLens, PanprotoLensRoundTripClass};
let chain: serde_json::Value = serde_json::from_slice(&std::fs::read("chain.json")?)?;
let lens = PanprotoLens {
blob: Some(chain),
created_at: Datetime::parse("2026-04-19T00:00:00.000Z").unwrap(),
laws_verified: Some(true),
object_hash: format!("sha256:{}", sha256_hex(&blob_bytes)),
round_trip_class: Some(PanprotoLensRoundTripClass::Iso),
source_schema: AtUri::parse(
"at://did:plc:tutorial.dev/dev.panproto.schema.schema/v1",
)?,
target_schema: AtUri::parse(
"at://did:plc:tutorial.dev/dev.panproto.schema.schema/v2",
)?,
};
Three fields warrant care:
object_hashis a content-addressed identifier for the chain bytes. TheVerifyingResolverwill refuse to hand the lens to the runtime unless the hash matches the canonical bytes.round_trip_classuses the wire valuesiso,retraction,projection, andopaque. Consumers may use the class to select an appropriate review path.laws_verifiedis a soft assertion that the chain passed panproto's coercion-law and existence checks. A true value here is meaningless without a correspondingdev.idiolect.verificationrecord from a publisher you trust. Treat it as a pre-publish smoke signal.
Publish
Construct a SigningPdsWriter from a reqwest PDS client plus a
DPoP prover, wrap it in a RecordPublisher, and call create:
use idiolect_lens::{
P256DpopProver, RecordPublisher, ReqwestPdsClient, SigningPdsWriter,
};
let client = ReqwestPdsClient::with_service_url(&session.pds_url);
let prover = P256DpopProver::from_pkcs8_pem(&pkcs8_pem)?;
let writer = SigningPdsWriter::new(
client,
session.access_jwt.clone(),
prover,
session.dpop_nonce.clone(),
);
let publisher = RecordPublisher::new(writer, session.did.clone());
let resp = publisher.create(&lens).await?;
pkcs8_pem is converted from the session's
dpop_private_key_jwk via an external JWK-to-PKCS8 helper.
Driving the OAuth dance and persisting the session is the
caller's job. See Configure OAuth sessions. A PDS may
validate the record's lexicon shape, but it does not run idiolect's
resolver or content-hash checks. Validate the chain and hash before
publication; consumers should resolve through VerifyingResolver.
Resolve it
The complement of publishing is resolving. Given an at-uri, the
Resolver trait
hands back a PanprotoLens record:
use idiolect_lens::{
PdsResolver, ReqwestPdsClient, VerifyingResolver, CachingResolver, Resolver,
};
use std::sync::Arc;
use std::time::Duration;
let client = ReqwestPdsClient::with_service_url("https://bsky.social");
let inner: Arc<dyn Resolver> = Arc::new(PdsResolver::new(client));
let verifying = Arc::new(VerifyingResolver::sha256(inner));
let resolver = CachingResolver::new(verifying, Duration::from_secs(300));
let lens = resolver.resolve(&lens_uri).await?;
VerifyingResolver re-hashes the bytes the inner resolver returned
and rejects the record on mismatch. CachingResolver keeps the
result in a TTL'd cache so repeated apply_lens calls do not
re-fetch.
Resolver is object-safe, and its futures are Send. Thus, an async
service may hold Arc<dyn Resolver> and call apply_lens from a
spawnable request future.
Make it discoverable
The 0.12.1 orchestrator catalogs dev.idiolect.* records, not
dev.panproto.schema.lens records. Consumers fetch a known lens URI
directly. Publish idiolect records that point to the lens to make that
URI discoverable:
- Publish a
dev.idiolect.recommendationfrom a community DID endorsing the lens path under stated conditions. - Publish
dev.idiolect.verificationrecords covering the properties consumers care about. - Register the lens in a
dev.idiolect.dialect'spreferredLensesso dialect-aware consumers find it without a separate query.
Migrate records across a revision
Use idiolect-migrate
when a schema
revision no longer accepts records written against its predecessor.
The crate is a thin typed façade over panproto-check (for diff
classification) and idiolect-lens (for record translation). It
is primarily a library, and also ships an optional
idiolect-migrate binary behind the cli feature for streaming
batch migration (see Batch migration).
The runtime path:
flowchart LR
OLD[record at v1] --> APPLY[apply_lens forward]
APPLY -->|new shape| NEW[record at v2]
APPLY -->|complement| C[complement bytes]
NEW --> EDIT[edit at v2]
EDIT --> PUTBACK[apply_lens_put]
C --> PUTBACK
PUTBACK -->|reconstructed| OLD2[record at v1]
If the lens is an isomorphism the round-trip is byte-equal. If it is a projection, the complement carries the dropped data and the reverse direction reconstructs the original.
Classify the diff
Before generating a lens, classify what changed:
use idiolect_migrate::classify;
use panproto_schema::Protocol;
let protocol = Protocol::default();
let report = classify(&schema_v1, &schema_v2, &protocol);
classify returns a CompatReport (re-exported from
panproto-check) that distinguishes compatible from breaking
changes. Compatible diffs need no migration: records valid under
v1 remain valid under v2.
Auto-derive the lens
For breaking diffs that are covered by shipped recipes:
let plan = plan_auto(
&schema_v1,
&schema_v2,
&protocol,
source_schema_hash,
target_schema_hash,
)?;
plan_auto returns a MigrationPlan carrying the two caller-supplied
schema hashes, a protolens_chain, and an alignment_quality score.
It returns NoChange or OnlyNonBreaking when a migration plan is
unnecessary.
Panproto 0.71.0 computes this alignment with an exact valued-CSP optimizer. The score orders alternatives for the same source schema; do not treat a fixed number as a confidence threshold across unrelated schema pairs.
For breaking diffs that resist automation,
plan_auto returns Err(PlannerError::NotAutoDerivable)
listing the offending changes. The caller writes the lens by
hand.
Migrate one record
use idiolect_migrate::migrate_record;
let migrated_body = migrate_record(
&resolver,
&schema_loader,
&protocol,
lens_uri,
source_record_body,
).await?;
migrate_record resolves lens_uri, loads its source and target
schemas, and wraps idiolect_lens::apply_lens. It returns the target
record body as serde_json::Value.
Verify before cutting over
Migration without verification is a guess. Run the round-trip runner against a corpus before treating the migrated tree as authoritative:
use idiolect_verify::{RoundtripTestRunner, VerificationRunner, VerificationTarget};
let runner = RoundtripTestRunner::new(resolver, schema_loader, protocol, corpus);
let target = VerificationTarget { /* lens, verifier, occurred_at, tool_override */ };
let verification = runner.run(&target).await?;
A Verification { result: Holds, .. } establishes only that the
sampled corpus round-tripped. Record the corpus boundary, review any
projection complement, and publish the result through
idiolect_lens::RecordPublisher if other consumers need it.
Persist the lens record
If the migration is one-shot, the steps above are enough. If you
expect downstream consumers to migrate later, publish the lens
plan's body as a dev.panproto.schema.lens record and link it
from the new schema's preferredLenses list. See
Publish and resolve a lens.
Hand-authored chains
Some migrations are not auto-derivable
(NotAutoDerivable). The release-gate policy in
Lexicon evolution policy
covers that case. The authoring loop is:
- Hand-author the chain in panproto's protolens DSL.
- Run
schema lens inspectto classify it. - Run
schema theory check-coercion-lawsagainst anyCoerceTypestep. - Run the round-trip runner against a corpus snapshot.
- Publish the chain plus a verification record signed by a reviewer.
The checklist records the authored chain, law checks, corpus run, and reviewer-signed verification, but the current repository does not enforce every gate automatically.
Batch migration
The crate ships an idiolect-migrate binary (behind the cli
feature) that walks a directory of JSON records and writes a
migrated directory:
cargo install --path crates/idiolect-migrate --features cli
idiolect-migrate \
--lens at://did:plc:.../dev.panproto.schema.lens/example \
--in ./records-v1/ \
--out ./records-v2/ \
[--pds-url URL]
Records stream one at a time, so memory use does not grow with the input directory. Failed migrations go to stderr; the process exits 1 if any file fails and 0 otherwise.
Configure OAuth sessions
idiolect-oauth stores
OAuth
sessions after an application completes the authorization flow. The
crate supplies OAuthTokenStore, three stores, and refresh timing; an
OAuth client supplies the network exchange.
When you need it
Anything that publishes records (encounter, recommendation, verification, observation, lens, dialect, vocab, ...) needs an authenticated PDS session. Reading records does not.
Pick a store
| Store | Feature | Use when |
|---|---|---|
InMemoryOAuthTokenStore | (always) | Tests and fixtures. |
FilesystemOAuthTokenStore | store-filesystem | A single operator process running on one host. Sessions live under a directory; one file per DID. |
SqliteOAuthTokenStore | store-sqlite | Multi-process or multi-tenant deployments. Concurrent reads, fsync per write. |
All three implement OAuthTokenStore. Its native async methods make
the trait non-object-safe, so callers remain generic over
S: OAuthTokenStore rather than using Arc<dyn OAuthTokenStore>.
idiolect-oauth is publish = false; depend via git.
Filesystem store
idiolect-oauth = { git = "https://github.com/idiolect-dev/idiolect", tag = "v0.12.1", features = ["store-filesystem"] }
use idiolect_oauth::{FilesystemOAuthTokenStore, OAuthTokenStore};
std::fs::create_dir_all("./sessions/")?;
let store = FilesystemOAuthTokenStore::new("./sessions/")?;
// Write a session (returned by the OAuth dance, not by this crate):
store.save(&session).await?;
// Read it back later:
let recovered = store.load(&session.did).await?; // Option<OAuthSession>
The directory contains one JSON file per session keyed by DID.
SQLite store
idiolect-oauth = { git = "https://github.com/idiolect-dev/idiolect", tag = "v0.12.1", features = ["store-sqlite"] }
use idiolect_oauth::{SqliteOAuthTokenStore, OAuthTokenStore};
let store = SqliteOAuthTokenStore::open("sessions.sqlite")?;
Drive the OAuth dance
The authorization client returns an authenticated session that you
store through OAuthTokenStore::save. The OAuthSession shape is
documented in the crate's source: it carries the DID, PDS URL,
access JWT, refresh JWT, DPoP private key (JWK-serialized),
DPoP nonce, and expiry timestamps as public fields.
For session-staleness decisions, either call refresh_if_needed
(documented below) or read OAuthSession::is_expired and
OAuthSession::needs_refresh(now, threshold) in your own refresh
path. Either way, the application supplies the Refresher that
drives the refresh endpoint.
DPoP
The session's
DPoP
keypair binds the access token.
The signer (the P256DpopProver in
idiolect-lens under
the dpop-p256 feature) consumes the keypair from the session
and signs every PDS write through SigningPdsWriter.
Persisting the DPoP key with the session is the store's job.
Both shipped persistent stores
(FilesystemOAuthTokenStore, SqliteOAuthTokenStore) do; if
you write a custom store, do the same.
idiolect oauth login (transitional)
The idiolect CLI ships an oauth login subcommand that
exchanges a handle + app password for an access JWT via
com.atproto.server.createSession and persists the resulting
session as a JSON file under
$IDIOLECT_SESSION_DIR (default
~/.config/idiolect/sessions/):
idiolect oauth login --handle yourhandle.bsky.social --pds-url https://bsky.social
# password from --app-password or ATPROTO_APP_PASSWORD / ATPROTO_PASSWORD env
idiolect oauth list
idiolect oauth logout --did did:plc:...
This path uses app passwords in legacy Bearer mode.
This CLI path is separate from OAuthSession: its JSON file contains
the DID, handle, PDS URL, access JWT, and refresh JWT, but no DPoP key.
Use the library store for a DPoP-bound OAuth deployment.
refresh_if_needed
idiolect_oauth::refresh_if_needed(&store, &refresher, did)
loads a session, decides whether to refresh based on the
current wall clock plus a 60-second buffer, drives the caller-
supplied Refresher::refresh if so, persists the result, and
returns the live session. Callers who want to drive the
decision themselves read OAuthSession::needs_refresh and
OAuthSession::is_expired directly.
use idiolect_oauth::{refresh_if_needed, Refresher, RefreshError, OAuthSession};
struct MyRefresher { /* http client, auth-server URL, ... */ }
impl Refresher for MyRefresher {
async fn refresh(&self, session: &OAuthSession) -> Result<OAuthSession, RefreshError> {
// POST refresh_token to the auth-server's token_endpoint.
// Return a fresh OAuthSession with new access_jwt / expires_at.
todo!()
}
}
let fresh = refresh_if_needed(&store, &MyRefresher { /* ... */ }, "did:plc:...").await?;
The trait is narrow on purpose: the refresh HTTP call lives in
whatever OAuth client the application uses (atrium-oauth, a
hand-rolled reqwest call, an in-memory fake for tests).
idiolect-oauth owns the storage and timing decision around it.
Run codegen
Codegen treats the lexicons
under lexicons/dev/idiolect/ as its source of truth. Regenerate
derived Rust types, TypeScript validators, family modules, HTTP
routes, and CLI dispatch after changing that source.
The crate is
idiolect-codegen.
It runs as a workspace binary (cargo run -p idiolect-codegen).
Invocation
cargo run -p idiolect-codegen # write the generated tree
cargo run -p idiolect-codegen -- --check # verify no drift
The default mode writes the generated tree. The --check flag emits
in memory, compares bytes against the working tree, and exits nonzero
on drift. Both modes parse emitted Rust and TypeScript through
panproto 0.71.0's tree-sitter gate before accepting the output.
What gets emitted
| Output | Source | Where |
|---|---|---|
| Per-record Rust types | lexicons/dev/idiolect/*.json and the vendored lexicons/dev/panproto/* | crates/idiolect-records/src/generated/ |
idiolect-records family module | the shipped lexicons | crates/idiolect-records/src/generated/family.rs |
| Per-record fixtures (Rust) | lexicons/dev/idiolect/examples/*.json | crates/idiolect-records/src/generated/examples.rs |
| TypeScript validators + types | same lexicons | packages/schema/src/generated/ |
| Orchestrator HTTP routes | orchestrator-spec/queries.json | crates/idiolect-orchestrator/src/generated/ |
| Orchestrator CLI dispatcher | orchestrator-spec/queries.json | crates/idiolect-cli/src/generated.rs |
| Observer method descriptors | observer-spec/methods.json | crates/idiolect-observer/src/generated.rs |
| Verifier kind taxonomy | verify-spec/runners.json | crates/idiolect-verify/src/generated.rs |
The three spec files are single JSON documents (not directories). Each declares an array of entries that codegen reads.
Check for generated drift
--check compares generated output with the checked-in tree. A failure usually
means that a lexicon or
spec changed without regeneration, or that a generated file was
edited by hand. Run the default mode, inspect the resulting diff, and
then run --check again.
Adding a new lexicon
- Drop a JSON document under
lexicons/dev/idiolect/. - Run
cargo run -p idiolect-codegen. - Optional: add a fixture under
lexicons/dev/idiolect/examples/<name>.jsonso theexamplesmodule emits a typed accessor. - Re-run the workspace tests to confirm the family round-trips.
Codegen rejects malformed lexicons, including invalid NSIDs, before it writes output. The subsequent emit gate rejects malformed generated source.
Adding a new spec entry
The orchestrator query, observer method, and verifier runner specs each carry a different shape. The pattern is the same:
- Add a JSON entry to the spec file.
- Run
cargo run -p idiolect-codegen. - Implement the hand-written half (the panproto-expr predicate
for an orchestrator query, the
ObservationMethodimpl for an observer method, theVerificationRunnerimpl for a verifier runner).
The dispatcher routes by the declared kind; the generated tree owns the parser and dispatch table.
Library API
For consumers outside the workspace, the emitter is callable as a library:
use idiolect_codegen::emit::{emit_rust, emit_typescript};
use idiolect_codegen::emit::family::{FamilyConfig, idiolect_family};
emit_rust(docs, examples, family) and
emit_typescript(docs, examples, family) take preloaded
LexiconDoc and Example slices plus &FamilyConfig. Each returns
anyhow::Result<Vec<EmittedFile>>; loading lexicons from disk remains
the caller's responsibility.
FamilyConfig::new(marker_name, id, nsid_prefix) constructs a
config from any string-like inputs. The shipped default for
dev.idiolect.* is idiolect_family().
The crate is publish = false and not on crates.io. Downstream
consumers depend on it via a path or git reference rather than
a registry version.
Author a community vocabulary
Open-enum
slugs may point to a dev.idiolect.vocab record. The record stores a
typed graph whose relation nodes may declare
OWL 2 property characteristics and whose
concept nodes may carry SKOS Core fields.
This guide covers the authoring side: writing the JSON, declaring relation properties, and publishing the record.
Minimum vocabulary
Every vocabulary needs a name, a description, and at least one node:
{
"$type": "dev.idiolect.vocab",
"name": "vote-stances",
"description": "Default deliberation vote stances.",
"world": "open",
"nodes": [
{ "id": "agree", "kind": "concept", "label": "Agree" },
{ "id": "disagree", "kind": "concept", "label": "Disagree" },
{ "id": "pass", "kind": "concept", "label": "Pass" }
],
"edges": [],
"occurredAt": "2026-05-01T00:00:00.000Z"
}
world controls whether unknown values are accepted as
extensions:
open— unknown values are first-class extensions.closed-with-default— unknown values fall back to a designated default.hierarchy-closed— unknown values are rejected.
Per-relation overrides live on the relation node's metadata.
Add typed relations
A relation is itself a node, with its algebraic properties declared as metadata:
{
"id": "polar_opposite_of",
"kind": "relation",
"label": "Polar opposite of",
"metadata": {
"symmetric": true,
"transitive": false,
"reflexive": false,
"irreflexive": true
}
}
The shipped fields cover these OWL 2 property characteristics: symmetric,
asymmetric, transitive, reflexive, irreflexive,
functional, inverseFunctional, plus inverseOf and a per-
relation world override. VocabGraph::validate walks asserted edges
and reports violations. RecordPublisher does not call this method,
so run it before publication.
Add edges
Now express the relations on top of the nodes:
{
"edges": [
{ "source": "agree", "target": "disagree", "relationSlug": "polar_opposite_of" },
{ "source": "disagree", "target": "agree", "relationSlug": "polar_opposite_of" }
]
}
If the relation is symmetric the runtime walks both directions, so you can author either direction (or both, redundantly).
Annotate with SKOS Core
Every concept node accepts SKOS-style annotations:
{
"id": "agree",
"kind": "concept",
"label": "Agree",
"alternateLabels": ["yes", "+1"],
"hiddenLabels": ["agreed", "agrees"],
"scopeNote": "Use when the voter affirms the statement as written.",
"example": "I agree with the proposal as drafted.",
"notation": "1",
"externalIds": [
{ "system": "wikidata", "identifier": "Q4116214", "matchType": "exact" }
]
}
The full annotation set is label, alternateLabels,
hiddenLabels, description (definition), scopeNote, example,
historyNote, editorialNote, changeNote, notation, and
externalIds. The matchType values on externalIds carry SKOS
semantics (exact, close, broader, narrower, related).
A kind: "collection" plus member_of edges expresses a SKOS
Collection.
Validate
use idiolect_records::{Vocab, vocab::VocabGraph};
let bytes = std::fs::read("vocab.json")?;
let vocab: Vocab = serde_json::from_slice(&bytes)?;
let violations = VocabGraph::from_vocab(&vocab).validate();
if !violations.is_empty() {
anyhow::bail!("vocabulary violations: {violations:#?}");
}
Deserialization checks the generated record shape. validate() then
returns Vec<VocabViolation>, with one entry for each graph-property
failure. Panproto's schema check compares schema migrations; it does
not validate a dev.idiolect.vocab record.
Publish
Same path as any other record. Construct a writer, wrap it in
RecordPublisher, and call create:
use idiolect_lens::{
P256DpopProver, RecordPublisher, ReqwestPdsClient, SigningPdsWriter,
};
use idiolect_records::Vocab;
let vocab: Vocab = serde_json::from_slice(&std::fs::read("vote-stances.json")?)?;
let client = ReqwestPdsClient::with_service_url(&session.pds_url);
let prover = P256DpopProver::from_pkcs8_pem(&pkcs8_pem)?;
let writer = SigningPdsWriter::new(
client,
session.access_jwt.clone(),
prover,
session.dpop_nonce.clone(),
);
let publisher = RecordPublisher::new(writer, session.did.clone());
let resp = publisher.create(&vocab).await?;
pkcs8_pem is converted from the session's
dpop_private_key_jwk via an external JWK-to-PKCS8 helper. Request
PDS-side lexicon validation when the deployment supports it, but keep
the local typed and graph checks: PDS validation does not run
VocabGraph::validate. See Configure OAuth sessions.
Use the published vocab
Once the vocabulary is on the network, an open-enum field may point to
it through the sibling *Vocab field. Rust consumers see one of two
enum forms:
- A generated known variant when the slug appears in the lexicon's
knownValueslist. Other(String)for every other wire slug, whether or not a referenced vocabulary declares it.
The is_subsumed_by, satisfies, and translate_to helpers query a
loaded VocabGraph or VocabRegistry; deserializing the enum does not
fetch the referenced record. See
The vocabulary knowledge graph for
the semantics.
Bundle records into a dialect
A dev.idiolect.dialect record publishes a community's selected
schemas and lenses
as one versioned bundle. A consumer fetches the record and follows
only the references its local policy accepts.
The shape is documented in the lexicon reference. The fields most consumers care about:
idiolects— schemas the community treats as canonical.preferredLenses— translations the community prefers.deprecations— entries that were once part of the dialect with replacement pointers.versionandpreviousVersion— the dialect's revision chain.
Author the bundle
The shortest path is to construct the typed record directly:
use idiolect_records::{AtUri, Datetime, Dialect};
let dialect = Dialect {
owning_community: AtUri::parse(
"at://did:plc:tutorial.dev/dev.idiolect.community/canonical",
)?,
name: "tutorial canonical".into(),
description: Some("...".into()),
idiolects: Some(vec![/* SchemaRef values */]),
preferred_lenses: Some(vec![/* LensRef values */]),
deprecations: None,
version: Some("1.0.0".into()),
previous_version: None,
created_at: Datetime::parse("2026-04-19T00:00:00.000Z")?,
};
Construct the record, then publish it via
idiolect_lens::RecordPublisher::create.
What it does for consumers
Consumers reading a dialect get three independent signals:
- A canonical NSID list. A consumer that has resolved a dialect knows which schemas the community treats as canonical and can filter incoming records accordingly.
- A lens preference list.
preferredLensesrecords which translations the community recommends; it does not override a consumer's trust policy. - An audit trail of deprecations. A consumer that sees a
Deprecationentry can keep reading the deprecated NSID for some grace period and route toreplacementafterwards.
Use a dialect at runtime
There is no shipped DialectClient. Fetch the record through a typed
record client and inspect its fields directly. The
idiolect_lens::Resolver trait resolves lens records only; it does
not resolve dev.idiolect.dialect records.
Multiple dialects
Two communities may publish disjoint, overlapping, or contradictory dialects. The protocol assigns no global priority, so consumers pick a resolution policy in application code:
- first-match — pick the first dialect listed in the consumer's config.
- quorum — accept a translation when of trusted dialects endorse the same lens path.
- merge — union the entries; on collision, fall back to a configured tie-breaker.
The runtime ships no trait or implementation for any of these: dialect resolution is a consumer decision, and the right shape varies by deployment.
Concepts
The Concepts chapters explain the model behind idiolect. They begin with a concrete coordination failure, introduce the records that make translation knowledge public, and then develop the formal and governance consequences. Procedures stay in the Guides; field-level contracts stay in Reference.
A first pass
Read these chapters in order if the project is new to you:
- Why idiolect exists names the private-converter problem and follows one translation through the record family.
- What you need first supplies the small amount of ATProto and panproto background used elsewhere.
- Idiolect, dialect, language separates the linguistic analogy from the concrete runtime artifacts.
- The
dev.idiolect.*lexicon family maps those artifacts onto the sixteen record kinds shipped by the repository.
Runtime model
The next four chapters explain how the runtime interprets those records:
- Records in signed, content-addressed repositories distinguishes record identity, record content, and repository proof.
- Open enums and vocabularies explains how unfamiliar slugs survive decoding and acquire community-specific meanings.
- The vocabulary knowledge graph develops the graph queries behind that interpretation.
- Observer protocol explains how event folds become independently published observation records.
Formal and governance paths
Lens semantics and laws is the formal center of the book. It introduces complements, round-trip laws, optic classification, and symmetric span construction against panproto 0.71.0. Deliberation then separates a community's decision process from its settled beliefs and recommendations. Lexicon evolution policy closes the section by comparing the intended migration gate with the enforcement that the current checkout actually provides.
Why idiolect exists
idiolect addresses the private-converter problem (PCP): independently written schema converters tend to remain inside the applications that need them, so later consumers cannot inspect, reuse, or evaluate the translation knowledge they contain.
Two event schemas
Suppose two communities build event-planning applications on ATProto. One
publishes garden.seedling.event, with title, startsAt, and a free-text
where field. The other publishes club.lantern.gathering, with name,
beginsAt, and a structured venue object. Each schema fits its application.
A calendar aggregator that consumes both collections now has to answer three questions: which fields correspond, what happens to information that exists on only one side, and whose answer should be trusted? ATProto supplies publication, identity, and repository proofs. It does not supply the correspondence between these two Lexicons.
Why local fixes accumulate poorly
The aggregator can hard-code two adapters. That solves its immediate problem, but another consumer must repeat the work, and neither consumer has a common object on which to publish tests or corrections. Supporting one schema only avoids translation by excluding data. Asking both communities to adopt a third schema may be appropriate in some settings, though it shifts the disagreement to standard selection.
These responses differ operationally, but they leave the PCP intact: knowledge of the relationship is either private or absent.
The publication move
idiolect makes a translation publishable. A panproto
lens
record names its source and target schemas and carries a schema-parameterized
translation body. A consumer can resolve that record, instantiate it under the
ATProto protocol, and apply it to a source value. If get drops where while
constructing venue, its complement retains the discarded state for put.
Publication creates a shared object, not automatic trust. The record family thus separates four kinds of claim:
- A
dev.idiolect.verificationreports the result of running a named check on a lens. - A
dev.idiolect.recommendationendorses a lens path under stated conditions and caveats. - A
dev.idiolect.encounterrecords an invocation; a correction or later observation can qualify that evidence. - A
dev.idiolect.dialectbundles the schema references that constitute a community's idiolect set, along with preferred lenses and deprecations.
This separation is the evidence split (ES). A lens body says how to translate; a verification says what a particular runner observed; a recommendation says who advises using the lens and when. Under the ES, no one record stands in for the others.
What the model can promise
The runtime can preserve unfamiliar open-enum slugs, reject values that its typed decoders cannot parse, apply a resolved lens, and publish the resulting evidence records. A corpus-backed verification may show that a round-trip law held for the tested corpus. It does not prove that the law holds for every possible record, and a signed repository commit proves authorship and integrity rather than semantic correctness.
idiolect consequently does not promise a global schema, universal convergence, or trustworthy publishers. It provides objects over which communities can make translation, evidence, and policy disagreements explicit. The idiolect/dialect/language frame names the three levels at which those disagreements arise.
From the example to the runtime
| Question from the example | Runtime object |
|---|---|
| How do the two event shapes correspond? | dev.panproto.schema.lens and lens semantics |
| Did a check find a counterexample? | dev.idiolect.verification |
| Who recommends this path, and under which conditions? | dev.idiolect.recommendation |
| What happened when a consumer used it? | dev.idiolect.encounter, correction, and observation |
| Which choices does a community currently prefer? | dev.idiolect.dialect |
| How can a community debate a choice before adopting it? | The deliberation records |
For the operational loop, continue with the tutorial. For the conceptual division of responsibility, continue with Idiolect, dialect, language.
What you need first
idiolect depends on ATProto for publication and on panproto for schema translation. Readers need three ATProto objects and one panproto object. The definitions below are sufficient for the rest of the Concepts chapters.
ATProto repositories and records
An ATProto account controls a public
repository.
A record
occupies a path of the form collection/rkey; an
AT-URI adds the
account DID:
at://did:plc:example/dev.idiolect.verification/3kexample
The repository is a content-addressed Merkle Search Tree whose leaves point to record CIDs. The repository commit is signed; an individual record is not a stand-alone signed document. A proof chain can connect a record CID to that signed commit. This distinction, specified in the official ATProto repository format, must be accounted for when evaluating provenance claims.
Lexicons and NSIDs
A Lexicon
is an ATProto schema document. Its
NSID
names a record collection or XRPC method; dev.idiolect.recommendation is one
such collection. The Lexicon specification
defines the record, object, reference, union, and scalar forms used throughout
this repository.
Independent publishers may define different NSIDs for similar data. Lexicon validation can determine whether a value has the declared shape, but it cannot determine that two independently named shapes describe the same thing. idiolect addresses that second problem.
PDSes and event streams
A personal data server (PDS) hosts an account's authoritative repository. Network consumers usually learn about record changes through a synchronization stream or a derived transport such as Jetstream or tap. The book uses event stream for the abstraction and firehose when referring to the network-wide ATProto stream specifically.
The current indexer accepts a generic EventStream. Concrete adapters cover
tap and Jetstream; thus a conceptual statement about an event fold does not
imply that every process connects directly to a PDS.
Panproto lenses
A lens
relates a source schema to a target schema. Its forward operation, get,
produces a target view and a
complement
containing source information that the view did not retain. Its backward
operation, put, combines a target view with that complement to reconstruct a
source value.
idiolect uses panproto 0.71.0 to instantiate and run these lenses. No category theory is assumed: Lens semantics and laws introduces the notation before using it, while the panproto book provides optional depth.
Idiolect, dialect, language
The project borrows three linguistic terms to distinguish individual practice, community policy, and shared infrastructure. The analogy is organizational; it does not claim that schema systems have every property of natural languages.
Three levels
An idiolect
is one publisher's practice: the record types it emits, the lenses it uses, and
the conventions it follows. There is no dev.idiolect.idiolect record. An
idiolect is inferred from records; a dialect's idiolects field lists the
schema references that constitute its idiolect set.
A dialect
is an explicit community bundle. A dev.idiolect.dialect record names its
owning community and may carry schema references in idiolects, along with
preferredLenses, deprecations, and version links. The dialect is policy
expressed as data; consuming it remains a local choice.
The language level (LL) is the shared ATProto substrate plus the schema and lens records published on it. The LL has no single runtime record and no requirement that all participants use one schema. Local catalogs, indexers, and orchestrators construct views over this level.
| Level | Concrete representation | Scope |
|---|---|---|
| Idiolect | A publisher's observed records and choices | One publisher or application |
| Dialect | dev.idiolect.dialect | A community's stated schema and lens policy |
| Language | ATProto repositories, event streams, Lexicons, and lenses | The federated substrate |
Plural canonicity
The model permits more than one community to call a different bundle canonical. We call this plural canonicity (PC). A consumer resolves PC locally by choosing which community records, recommendations, vocabularies, and observers it trusts. The orchestrator's catalog supports that selection; it does not make the selection universal.
PC has two consequences. First, a new schema can be published without a network-wide approval step. Second, a lens can connect established schemas without forcing either publisher to rename its collection. But the same freedom allows incompatible or malicious records, so policy cannot be derived from federation alone.
Failure modes
Three failure modes recur at different levels:
- Unlinked duplication. Two NSIDs describe similar data, but no lens or recommendation relates them. Consumers must either treat them separately or author the missing relationship.
- Vocabulary collision. Two communities reuse a slug with different
intended meanings. A
*Vocabreference can disambiguate the vocabulary, but an omitted reference may leave policy to a canonical default outside the wire record. - Dialect drift. A community updates a dialect's preferred lenses or deprecations. An AT-URI may then resolve to a new record CID while consumers holding an older strong reference continue to see the pinned revision.
A potential worry about PC is that it merely renames fragmentation. That worry is justified when publishers supply no lenses, evidence, or policy records. The model does not prevent that outcome. It makes the missing relationships visible and gives later publishers a common place to add them.
The actual guarantees
An NSID identifies an intended Lexicon shape, but repositories remain untrusted input; invalid records may appear on an event stream. A generated decoder can reject a record it cannot deserialize, though not every semantic constraint is enforced by deserialization alone. Likewise, a lens carries law claims, but those claims require checking on concrete instances or stronger external evidence.
Thus, the three levels distribute responsibility rather than truth. Publishers
choose an idiolect, communities state a dialect, and consumers decide how to
interpret the language-level record population. The
dev.idiolect.* family supplies the records used in that
exchange.
The dev.idiolect.* lexicon family
The repository ships sixteen record Lexicons and one shared-definitions
Lexicon, dev.idiolect.defs. Together they separate event traces, aggregate
claims, community policy, deliberation, and runtime integration.
flowchart TB
subgraph evidence["Events and evidence"]
ENC[encounter]
COR[correction]
OBS[observation]
VER[verification]
RET[retrospection]
end
subgraph policy["Claims and policy"]
BEL[belief]
REC[recommendation]
BOU[bounty]
DIA[dialect]
COM[community]
end
subgraph meaning["Meaning and integration"]
VOC[vocab]
ADA[adapter]
end
subgraph process["Deliberation process"]
DEL[deliberation]
DST[deliberationStatement]
DVO[deliberationVote]
DOU[deliberationOutcome]
end
ENC --> COR
ENC --> OBS
ENC --> RET
VER --> REC
COM --> DIA
DEL --> DST
DST --> DVO
DEL --> DOU
The arrows show common reference or fold relationships. They do not imply that publishing one record automatically creates another.
Events and evidence
dev.idiolect.encounterrecords one lens invocation, including the lens, source schema, structured use, encounter kind, and visibility.dev.idiolect.correctionattaches a path, reason, and corrected value to an encounter reference.dev.idiolect.observationcarries an observer DID, method descriptor, scope, version, and free-form aggregate output.dev.idiolect.verificationrecords a runner'sholds,falsified, orinconclusivejudgment about a structured lens property.dev.idiolect.retrospectionreports a delayed finding about one encounter, including detecting party, detection time, and optional confidence.
These records form the evidence chain (EC): an invocation may be corrected or reviewed later, while an observer can publish a method-specific aggregate over the stream it processed. The EC is plural because different parties may publish incompatible assessments.
Claims and community policy
dev.idiolect.beliefis a holder-attributed claim about a strongly referenced record. Its subject is required; holder, basis, annotations, and visibility are optional.dev.idiolect.recommendationpublishes a conditioned lens path from an issuing community, with optional preconditions, verification requirements, caveats, and supersession.dev.idiolect.bountyrequests a lens, adapter, or verification under stated constraints and eligibility rules.dev.idiolect.dialectbundles the schema references in a community's idiolect set, along with preferred lenses, deprecations, and version links.dev.idiolect.communitydescribes membership, hosting policy, core schemas and lenses, endorsements, and conventions.
The distinction between belief and recommendation is intentional. Belief is an attributed claim about a subject; recommendation advises a translation path under conditions.
Meaning and integration
dev.idiolect.vocab represents nodes, typed edges, relation metadata, and
human-facing annotations. It also retains the earlier actions/parents tree
shape, which VocabGraph normalizes into subsumed_by edges. See
The vocabulary knowledge graph.
dev.idiolect.adapter describes how a named framework version can be invoked
and what isolation policy it requires. It is a declaration, not an executable
plugin or proof that the framework is safe.
Deliberation process
The four deliberation records preserve a process before it becomes settled policy:
dev.idiolect.deliberationnames the community, topic, and optional status.dev.idiolect.deliberationStatementplaces a statement in that deliberation.dev.idiolect.deliberationVotepins a statement revision and records a stance.dev.idiolect.deliberationOutcomecarries an observer-computed tally and optional adopted statements.
Deliberation explains why the process records remain separate from belief and recommendation.
Composition at the indexer boundary
The Rust bindings collect these sixteen record types under IdiolectFamily.
Consumers can combine it with another generated family through
OrFamily<F1, F2>. That composition widens typed dispatch; it does not generate
lenses or assert that records from the two families are semantically
equivalent. Translation still requires an explicit lens.
The Lexicons reference gives the field-level contract for each record.
Records in signed, content-addressed repositories
ATProto separates three coordinates that are easy to conflate: a record's mutable address, the CID of its current content, and the signed commit that authenticates a repository state. idiolect relies on all three, but its current runtime does not verify all three on every read.
Address, content, and proof
A record
lives at a repository path (collection, rkey) owned by an account DID. An
AT-URI combines
those coordinates:
at://did:plc:example/dev.panproto.schema.lens/3kexample
The AT-URI is stable across updates to that path. A CID names one encoded record value. Updating the record preserves the AT-URI and changes the CID when its content changes.
The repository's Merkle Search Tree maps the path to that record CID. Its root appears in a signed repository commit. Consequently, provenance verification is a chain:
flowchart LR
URI[AT-URI path] --> MST[repository MST]
MST --> CID[record CID]
MST --> ROOT[tree root CID]
ROOT --> COMMIT[signed commit]
COMMIT --> DID[DID document key]
The official repository specification defines this proof structure. Saying that a record is "signed" is convenient shorthand, but the signature is on the commit, not embedded in each record.
Mutable and strong references
An AT-URI alone follows the current value at a path. A strong reference pairs that URI with a CID. idiolect uses strong-reference-shaped definitions where later mutation would change the subject of a claim, including beliefs, deliberation statements, votes, and outcomes. Other fields intentionally use an AT-URI or a custom reference with an optional CID when following updates may be intended.
This choice is semantic. A vote should continue to name the statement revision
on which it was cast; a dialect's previousVersion link, by contrast, names a
record path in a version chain.
What the current runtime verifies
The read path has two distinct verification layers:
PdsResolverand the PDS clients fetch record values. They do not currently validate an ATProto repository proof chain or commit signature.VerifyingResolver<R, H>canonicalizes a resolved lens record'sblob, hashes those JSON bytes, and compares the result with that same record'sobjectHash. The bundledSha256Hasheraccepts thesha256:prefix.
The second check detects disagreement between a lens blob and its declared application-level hash. It is not a substitute for repository signature verification: an untrusted response could alter both fields unless the caller also authenticates the record through ATProto's repository machinery.
The idiolect-identity crate resolves a DID document and its PDS service URL.
It does not connect a fetched record CID to a signed repository commit. We call
this missing connection the proof-boundary gap (PBG). Deployments that need
cryptographic provenance must close the PBG outside the currently shipped
resolver stack.
What idiolect adds above storage
ATProto supplies record addressing, repository content addressing, signed commits, account migration through DID service resolution, and the Lexicon schema language. idiolect adds typed Rust and TypeScript bindings, record-family dispatch, vocabulary graph queries, lens resolution, verification records, and community policy records.
These layers make different claims. A Lexicon describes an intended wire shape; a generated decoder decides whether it can construct a typed value; a lens relates two schema graphs; and a verification reports the result of a particular check. Keeping those claims separate prevents provenance, validation, and semantic correctness from collapsing into one ambiguous notion of "valid."
Records outside the public family
Not every runtime datum is a dev.idiolect.* record. Cursor stores, OAuth
sessions, in-memory catalogs, and cached vocabulary graphs are local state.
Some reuse generated schema machinery, but they do not become federated merely
because they serialize. The public record family begins where a publisher
commits a Lexicon-shaped record to an ATProto repository.
Lens semantics and laws
idiolect runs panproto 0.71.0's state-based asymmetric lenses. The basic idea is to retain whatever a target view cannot express, then use that retained state when translating backward. We call this retained state the complement.
State-based form
For a source space , view space , and complement space , the runtime shape is:
Given a source , get returns a view and complement . A caller may
modify the view and then call put with the modified view and the original
complement. In idiolect-lens, apply_lens and apply_lens_put expose these
two directions over JSON records after parsing them into panproto instances.
Consider the event schemas from Why idiolect exists. If the
target has a structured venue but cannot represent every character of the
source's free-text where, get may place the residual source data in .
The complement is thus record-specific state, not metadata that can be safely
reconstructed from the lens definition alone.
Round-trip laws
A well-behaved lens satisfies two obligations. GetPut says that reading an unmodified view and writing it back recovers the source:
Here put(get(s)) abbreviates destructuring the pair returned by get and
passing both components to put.
PutGet says that writing a view with a compatible complement and reading it again recovers that view:
The projection selects the view component. panproto's check_laws
checks GetPut on one concrete source, checks PutGet on its original view, and
also tries a mechanically modified view when one can be produced. Passing this
check is evidence about those instances; it is not a proof over all , ,
and .
For an isomorphism the complement is empty, and the two operations are inverses:
Optic classification
panproto classifies a theory transform structurally with OpticKind. Version
0.71.0 uses these five variants:
| Kind | Structural reading | Complement role |
|---|---|---|
Iso | Bijection | Empty |
Lens | Single-focus projection or extension | Retains dropped data or required defaults |
Prism | Variant injection | Retains a variant tag |
Affine | Composition of lens-like and prism-like behavior | Retains both forms of state |
Traversal | Multi-focus transform | Tracks focus positions |
classify_transform assigns this kind from transform structure. Elementary
transforms are intended to be lawful by construction, but classification does
not itself run the laws. check_optic_laws performs the instance-level checks
available for the classified kind.
Composition uses the optic lattice implemented by OpticKind::compose: Iso
is the identity, Traversal absorbs the other kinds, and composing Lens with
Prism yields Affine. Concrete lens composition is sequential and must align
the first lens's target schema with the second lens's source schema.
Coercion classes
Primitive value conversions have a separate CoercionClass. The class records
what relationship the forward and inverse functions claim:
| Class | Claim |
|---|---|
Iso | Both round trips are identities. |
Retraction | The inverse recovers every value in the forward image. |
Projection | The target is deterministically derived from source data, but no inverse recovers the source from that target alone. |
Opaque | No stronger structural relationship is claimed; the complement retains the original value. |
These classes compose differently from optic kinds. Iso is the identity,
Opaque absorbs, and composing a Retraction with a Projection collapses to
Opaque. panproto's sample-based coercion-law checker may falsify a declared
class, though a finite sample cannot establish a universal law.
Symmetric lenses as spans
panproto builds a symmetric lens from two asymmetric lenses with a common source schema :
To synchronize a left view into a right view, the runtime first uses the left
leg's put to reconstruct a middle instance, then applies the right leg's
get. This is a span through shared state, rather than a direct lens whose
source is .
idiolect-lens::apply_lens_symmetric resolves two lens records, requires equal
sourceSchema references, and constructs this span. Its JSON-level entry point
rebuilds the middle instance with put_without_complement. Thus, the incoming leg
must be isomorphic: a lossy leg that needs saved complement data is rejected.
Callers that hold such data can instead use panproto's complement-aware
SymmetricLens operations directly.
Verification records
The verification Lexicon recognizes seven open-enum kinds, but the current
idiolect-verify crate implements four runners:
RoundtripTestRunnerchecks forward-then-backward equality on a nonempty, caller-supplied corpus.PropertyTestRunnerperforms the same round trip on values from a caller-supplied generator and finite budget.StaticCheckRunnervalidates the source and target panproto schema graphs; it does not execute the lens.CoercionLawRunnerdelegates to a caller-supplied coercion-law client.
A result of holds records that the configured run found no counterexample.
The runner, corpus or generator, tool version, and publisher thus remain
part of the evidence. Author a verification runner covers
the operational interface.
Open enums and vocabularies
ATProto Lexicon distinguishes suggested string values from closed enumeration.
knownValues lists common values but does not restrict the string; enum
defines a closed set. The distinction is part of the official
Lexicon string specification.
idiolect builds its extension convention on knownValues. We call the
combination of an open slug and an optional vocabulary reference the
open-enum pair (OEP).
Wire shape
The adapter Lexicon contains an OEP for its invocation protocol:
{
"kind": {
"type": "string",
"knownValues": ["subprocess", "http", "wasm"]
},
"kindVocab": {
"type": "ref",
"ref": "dev.idiolect.defs#vocabRef"
}
}
The record's kind value may be subprocess or a value that did not exist when
the consumer generated its bindings. kindVocab, when present, identifies a
vocabulary
in which the slug can be interpreted.
Many idiolect Lexicons describe an omitted *Vocab field as selecting a
canonical project vocabulary. That default is a convention in the schema
description, not a URI inserted by deserialization. A consumer that needs graph
semantics must choose or configure the default record itself.
Generated bindings
The Rust generator turns the example into
AdapterInvocationProtocolKind::{Subprocess, Http, Wasm, Other(String)}.
Serialization preserves the wire slug, including the string inside Other.
The TypeScript generator emits the literal union
"subprocess" | "http" | "wasm" | string & {} so editors retain completion for
known values without rejecting extensions.
Rust open-enum types also expose three graph-facing operations:
is_subsumed_bytests thesubsumed_byrelation in oneVocabGraph.satisfiestests reachability under a caller-selected relation.translate_toasks aVocabRegistryfor anequivalent_totranslation between two registered vocabulary URIs.
None of these methods fetches a vocabulary record. Loading, validating, and caching those records remains the caller's responsibility.
Preservation before interpretation
The OEP separates two requirements. Preservation means that an old consumer
can decode and reserialize an unfamiliar slug without replacing it. Generated
Other(String) variants provide that behavior. Interpretation means that a
consumer knows how the slug relates to a requirement such as subprocess.
Interpretation requires a loaded graph and a relation query.
This separation avoids a common failure mode in federated systems: treating an
unknown value as invalid merely because local code has not seen it. It does not
require a consumer to accept the value for every purpose. A policy may preserve
fly-machine on the wire and still decline to execute it because the relevant
vocabulary is missing or untrusted.
Closed fields
The shipped Lexicons still use enum for meta-policy fields whose extension
would alter a parser or runtime contract. vocab.world and the per-relation
world override, for instance, are closed over open, closed-with-default,
and hierarchy-closed. Unions may also be explicitly closed under the Lexicon
rules.
Changing a field from enum to knownValues expands the accepted wire values,
but it can also change generated source types. Existing record values remain in
the larger set; downstream code still needs regeneration and review.
Identifier collisions
Distinct slugs can normalize to the same Rust variant name. The generator keeps
the first name and adds a numeric suffix to later collisions, deterministically
within the generated enum. It also avoids using Other as the fallback name
when Other is itself a declared slug, selecting another fallback variant
instead. These rules preserve every wire value, though authors should still
prefer slugs whose generated names remain readable.
The vocabulary knowledge graph develops the graph semantics that turn preserved strings into queryable relations.
The vocabulary knowledge graph
dev.idiolect.vocab gives open-enum slugs a graph-shaped interpretation. A
record may contain the earlier actions/parents tree, the newer
nodes/edges graph, or both. VocabGraph::from_vocab normalizes these forms
into one read-side representation.
From a tree to a relation graph
In the earlier form, each action has zero or more parents. Normalization turns
each action into a concept node and each parent pointer into a subsumed_by
edge. If docker-run names subprocess as a parent, the normalized graph
contains:
The graph form generalizes this arrangement by allowing multiple named relations over the same node set. A community can represent subsumption, equivalence, opposition, membership, or a domain-specific relation without adding a new field to every record that uses the vocabulary.
Nodes and edges
A node requires only a stable id. Optional fields provide a kind, label,
status, external identifiers, and human-facing annotations. The known node
kinds are concept, relation, instance, type, and collection; the field
itself uses knownValues, so other kind slugs remain valid strings.
An edge is a triple consisting of a source id, relation slug, and
target id. Relation-kind nodes may carry relationMetadata. The metadata uses
property-characteristic names familiar from the
OWL 2 structural specification, including
symmetric, asymmetric, transitive, reflexive, irreflexive,
functional, and inverseFunctional.
The resemblance is deliberately limited. A dev.idiolect.vocab record is not
an OWL ontology, and VocabGraph is not an OWL reasoner. We call its supported
behavior relation-aware reachability (RAR).
What RAR computes
walk_relation(source, relation, reflexive) follows reachable edges for the
named relation. It always follows paths to closure, regardless of the relation's
transitive metadata. If the relation is marked symmetric, the walk follows
both outgoing and incoming edges. If the caller sets reflexive, the result
also contains the source.
The convenience operations are defined in terms of that walk:
subsumed_by(source)usessubsumed_bywith reflexive reachability.is_subsumed_by(specific, general)tests membership in that closure.direct_targetsanddirect_sourcesreturn one-hop neighbors.equivalent_in(source, other)searchesequivalent_toreachability for a node id known to the other graph.topreturns a unique non-relation root undersubsumed_by, when one exists;top_withgives an explicit record field priority.
The current implementation stores inverseOf and per-relation world in the
generated record type but does not apply them in VocabGraph traversal.
Likewise, setting reflexive metadata does not force a walk to include its
source; the call's reflexive argument controls that behavior. Consumers should
not infer full OWL semantics from the serialized names.
Validation boundary
VocabGraph::validate checks the constraints that can be established directly
from the authored edges and metadata:
- functional relations have at most one target per source;
- inverse-functional relations have at most one source per target;
- irreflexive relations contain no self-loop;
- asymmetric relations contain no pair of reverse edges; and
- declarations do not combine
symmetricwithasymmetricorreflexivewithirreflexive.
The method reports VocabViolation values. It does not reject the record during
deserialization, add missing closure edges, or establish that a label's intended
meaning is correct. That division is the validation boundary (VB):
structural inconsistency is machine-checkable, while semantic trust remains
consumer policy. At the VB, a clean graph is not yet a trustworthy vocabulary.
Human-facing annotations
The node fields label, alternateLabels, hiddenLabels, description,
scopeNote, example, historyNote, editorialNote, changeNote, and
notation are modeled after the
SKOS reference. They provide
authoring and display metadata, but the record is not serialized as RDF and
the runtime does not enforce SKOS integrity conditions.
externalIds adds mappings to identifiers outside ATProto with a mapping type
such as exact, close, broader, narrower, or related. These mappings are
stored on normalized nodes only indirectly: the current NormalizedNode view
does not expose externalIds. Callers that need them must inspect the generated
Vocab record.
Multiple vocabularies
VocabRegistry caches normalized graphs by caller-supplied URI string. It can
test a relation in one registered graph or translate a slug between two graphs
with equivalent_in. Translation succeeds when the target graph already knows
the slug or when equivalent_to reachability produces a node id that the target
knows.
This operation is pairwise. It does not search an arbitrary network of vocabulary records, fetch missing records, or select which publisher is authoritative. A consumer must load the two records, validate them, and decide that their equivalence declarations are acceptable.
Revision and supersession
A vocabulary record may point to a predecessor with supersedes. ATProto still
allows a publisher to update a record at the same AT-URI, producing a new CID,
or to create a record at a new key. The Lexicon does not require one revision
strategy. Consumers that need an immutable vocabulary revision should retain a
strong reference or otherwise pin the CID rather than relying on the AT-URI
alone.
Open enums and vocabularies explains how generated slug types call into these graph operations. The vocabulary guide covers record authoring.
Deliberation
The deliberation records preserve an unsettled community process. Beliefs and recommendations preserve attributed positions after or outside that process. We call this separation the process/position split (PPS).
Four linked records
flowchart LR
DEL[deliberation] --> DST[deliberationStatement]
DST --> DVO[deliberationVote]
DEL --> DOU[deliberationOutcome]
DOU --> DST
dev.idiolect.deliberation names an owning community and topic. It may also
carry a description, authentication requirement, classification, status,
closure time, and outcome AT-URI. The known classifications are question,
proposal, grievance, and retrospective; the known statuses include open,
closed, tabled, adopted, and rejected. Both fields remain open strings.
dev.idiolect.deliberationStatement strongly references the deliberation and
stores one statement. Its optional classification distinguishes claims,
proposals, dissent, clarification, questions, and community extensions. The
optional anonymous flag describes presentation policy; it does not remove the
repository DID from ATProto provenance.
dev.idiolect.deliberationVote strongly references one statement revision. Its
stance defaults to the known vocabulary of agree, pass, and disagree, with
an optional stanceVocab, integer weight, and rationale. The Lexicon constrains
weight to the range 0 through 1000 but does not define how a community must
interpret that number.
dev.idiolect.deliberationOutcome strongly references the deliberation and
contains per-statement stance counts, an optional list of adopted statements, a
computation time, and optional tool metadata. Multiple observers may publish
different outcomes for the same deliberation.
What PPS permits
A dev.idiolect.belief says that a holder stands behind a claim about a record.
A dev.idiolect.recommendation advises a conditioned lens path. Neither record
contains the statements considered, the votes cast, or the aggregation method.
The PPS permits a consumer to choose its evidential depth. A lightweight client may display a recommendation alone. A client auditing the decision can follow the community, deliberation, statement, vote, and outcome references. These records supply provenance coordinates; they do not guarantee a fair process or a correct conclusion.
Tallying in the current observer
DeliberationTallyMethod implements one aggregation. It counts votes by strong
statement reference and stance slug, and it sums optional weights. When
configured with a VocabRegistry and canonical stance vocabulary, it translates
stances through equivalent_to before counting; a slug with no translation
remains in its original bucket.
The method's snapshot resembles
dev.idiolect.deliberationOutcome.statementTallies, but the standard observer
publisher wraps that snapshot in dev.idiolect.observation. The current method
does not publish a typed deliberationOutcome, select adopted statements, or
update the deliberation's outcome field. An application that wants those
records must add the policy and publication step.
Procedure remains external
The four Lexicons do not implement voter eligibility, quorum, vote delegation,
ranked choice, quadratic weighting, or clustering. A community may describe
some of these choices in its community conventions or a vocabulary, but the
runtime does not infer a decision rule from the presence of weight.
A potential worry is that an open stance vocabulary makes two tallies incomparable. Vocabulary translation can reduce that problem when communities publish accepted equivalences. It cannot determine that an asserted equivalence preserves the communities' intended meanings, and untranslated stances remain a live possibility. The observer's method descriptor and output must thus accompany any comparison.
Observer protocol
An observer
turns a stream of typed records into an aggregate claim. The aggregate is a
dev.idiolect.observation record when a persistent publisher is configured.
We call this path the published-fold path (PFP).
flowchart LR
STREAM[event stream] --> INDEX[indexer decode]
INDEX --> METHOD[observation method]
METHOD --> SNAPSHOT[snapshot output]
SNAPSHOT --> PUB[publisher]
PUB --> RECORD[observation record or local sink]
A fold is stateful
Let be the event space, a method's private state, and its snapshot space. An observation method supplies a transition and a partial snapshot function:
Starting from , the method processes events in stream order:
If snapshot returns a value after events, the runtime wraps that value
with the observer DID, method descriptor, declared scope, method version,
visibility, and timestamp. A method may return no snapshot when it has not seen
enough relevant data.
This formulation is more accurate than treating every method as a set function.
Some methods may be order-insensitive, but ObservationMethod does not require
commutativity or idempotence. Comparability thus depends on method name,
version, scope, input coverage, and method semantics.
Stream and flush boundaries
The observer driver accepts any idiolect_indexer::EventStream. The reference
daemon currently uses TappedEventStream, backed by a tap service; the indexer
also has a Jetstream adapter behind its feature flag. The driver filters for
IdiolectFamily, decodes creates and updates, forwards them to one configured
method, and commits the cursor for live events after the handler succeeds.
FlushSchedule is event-count based or manual. EveryEvents(n) asks the
handler to publish after each processed events and once more when the stream
closes normally. It does not create clock-aligned windows. A method can declare
a window in its observation scope, but the generic driver does not enforce or
derive that window.
What gets published
Three publisher implementations exist:
InMemoryPublisherretains typed observations for tests and local callers.LogPublisheremits structured tracing events without repository durability.PdsPublisheradds$type: "dev.idiolect.observation"and calls the configuredPdsWriterto create a record.
The reference daemon selects CorrectionRateMethod. It uses the in-memory
publisher unless IDIOLECT_PDS_URL is set. Its PDS client is not authenticated
by the reference binary, so a normal PDS that requires authenticated writes
needs a wrapper or further integration before records will persist.
Bundled methods
The declarative method registry and generated default_methods() contain nine
record-form aggregators:
| Method | Snapshot coordinate |
|---|---|
correction-rate | Correction counts by lens and reason |
encounter-throughput | Encounters by kind and downstream result |
verification-coverage | Verifications by lens, kind, result, and verifier |
lens-adoption | Encounter and invoker counts by lens |
action-distribution | Encounters by structured action |
purpose-distribution | Encounters by structured purpose |
basis-distribution | Records by basis variant and record kind |
attribution-chains | Beliefs by holder and subject |
deliberation-tally | Votes by statement and stance |
Though default_methods() constructs all nine, drive_observer is generic
over one method and the reference daemon instantiates only correction-rate.
Running multiple methods requires multiple handlers or an application-level
composite.
Evidence and replay
The PFP separates aggregate state from a central query endpoint. Different observers can publish snapshots under their own DIDs, and consumers can compare them. If the record is committed to an ATProto repository, the repository proof can authenticate who published that snapshot.
But the observation record does not enumerate every input event, commit a digest of the input set, or prove that the method was executed as described. Recomputation requires access to an equivalent event history and the method's actual semantics. Divergent snapshots may reflect missed events, different cursor positions, method versions, scopes, or dishonest publication.
This is the replay boundary (RB). The PFP makes a claim portable; it does not make the claim self-proving. Consumers may impose quorum or observer-reputation policies above the RB, but the observation Lexicon does not implement either policy.
The observer guide covers daemon configuration. The observation reference gives the record shape.
Lexicon evolution policy
A Lexicon revision can preserve validation compatibility while changing what generated programs can safely assume. idiolect's intended response is the evolution evidence chain (EEC): classify the schema change, derive or author a lens when migration is needed, verify its stated properties, publish it, and connect community policy to that record.
The current checkout implements parts of the EEC in Rust and describes the rest in a shell script and CI workflow. It does not yet enforce the entire chain.
Compatibility and migration are different
idiolect-migrate begins with panproto_check::diff and
panproto_check::classify. If a diff has no breaking changes, plan_auto
returns OnlyNonBreaking: old records remain readable under the new schema, so
the library does not manufacture a migration plan.
If breaking changes exist, plan_auto asks panproto 0.71.0's auto_generate
for a ProtolensChain. Its default Balanced configuration uses the exact
valued-CSP optimizer and requires a total morphism. Success returns a
MigrationPlan containing the source and target schema identifiers supplied by
the caller, the chain, and an alignment-quality score. This score ranks
alignments for one source schema; it is not a confidence measure with a stable
threshold across unrelated pairs. Failure returns the breaking changes that
need manual attention.
This yields three distinct outcomes:
- no structural change;
- a compatible change that needs no record migration; or
- a breaking change that needs an auto-derived or hand-authored lens.
Compatibility is thus a read-side property, while migration is an operation over existing records.
From a plan to a published lens
A MigrationPlan is not an ATProto record. The caller must serialize its
protolens chain into a dev.panproto.schema.lens blob, provide the schema
record references and object hash expected by the vendored Lexicon, publish the
record, and retain its AT-URI. idiolect-migrate::migrate_record can then apply
that published lens through the normal idiolect-lens runtime.
Verification remains a separate step. RoundtripTestRunner can check GetPut on
a nonempty corpus; PropertyTestRunner can generate a finite set of cases;
StaticCheckRunner validates the two schema graphs. These checks may falsify a
claim. A passing finite run does not establish the corresponding universal law.
Finally, a community may add the lens to a dialect's preferredLenses, add a
deprecation entry for the prior object, or publish a recommendation. None of
those policy records is created by plan_auto.
Optic kinds are not governance classes
panproto 0.71.0 classifies transforms as Iso, Lens, Prism, Affine, or
Traversal. Earlier versions of this chapter described a different five-way
set, Iso/Injection/Projection/Affine/General, and assigned automatic
merge policy to it. That set is not the current OpticKind API.
A project can still define review rules over current optic kinds, complement requirements, compatibility reports, alignment quality, and verification evidence. Such rules are idiolect governance policy; they should not be presented as classifications returned by panproto.
Current automation boundary
The repository contains scripts/lexicon-evolve.sh and
.github/workflows/lexicon-evolution.yml, but both still encode the earlier CLI
and classification contract. With panproto 0.71.0:
schema diffaccepts positionalOLD NEWpaths rather than--srcand--tgt, and it has no--jsonflag;schema lens generaterequires--protocoland needs--chainto emit a reusable chain;schema lens inspectreports the current optic kinds; andschema lens verifyaccepts a data file and optional schema, not the directory/--schema/--chaincombination in the script.
The workflow makes CLI installation non-fatal and skips its pipeline when the installation step does not report success. It also searches for the obsolete classification names. Thus, the checked-in automation is a design scaffold, not a reliable merge gate for 0.71.0. We call this discrepancy the enforcement gap (EG).
A defensible gate
Until the EG is closed, reviewers can apply the EEC as an explicit checklist:
- compare the old and new parsed schemas with
idiolect-migrate::classify; - require a reviewed
MigrationPlanor a hand-authored chain for each breaking change; - inspect the current
OpticKindand complement requirements; - run the applicable verification runners on versioned inputs;
- publish the lens and verification records through an authenticated writer;
- update dialect, deprecation, and recommendation records deliberately; and
- retain the artifacts that identify the two schema revisions and test corpus.
Two points follow:
- The EEC does not make every migration reversible.
- It makes the remaining assumptions and evidence inspectable.
This leaves two live questions: which additional evidence would justify a stronger reversibility claim, and which parts of the EEC should become enforced CI gates? The migration guide covers the current library path, while Lens semantics and laws explains the obligations being tested.
Reference
Use this section to look up exported APIs, record fields, commands, and HTTP contracts. Task procedures remain in the guides.
| Section | Contents |
|---|---|
| Crates | One page per workspace crate, with public types, traits, error variants, and feature flags. |
| Lexicons | One page per dev.idiolect.* lexicon, with field-by-field shape. |
| CLI | Every shipped idiolect subcommand, its flags, and its output. |
| HTTP query API | Every endpoint exposed by the orchestrator, request and response shape. |
| Stability and versioning | The pre-1.0 stability policy. |
The reference covers idiolect 0.12.1 with panproto 0.71.0. For older releases, use the release archive.
Extension and API path
For an advanced integration, follow the lookup path that matches the extension boundary:
| Extension boundary | Start here | Then inspect |
|---|---|---|
| Add a record family or emitter target | idiolect-codegen | RecordFamily and the emit functions |
| Add a stream, handler, or cursor backend | idiolect-indexer | Trait signatures, feature flags, and error variants |
| Add a lens resolver or schema loader | idiolect-lens | Resolver, loader, apply-input, and apply-output types |
| Add an observation or verification method | idiolect-observer or idiolect-verify | Generated taxonomies and implementation traits |
| Integrate over process boundaries | CLI or HTTP API | Exact flags, query parameters, response envelopes, and errors |
For wire-level extensions, begin with the lexicon index and follow each page's source link to the authoritative JSON.
Authority policy
For published Rust crates, rustdoc on docs.rs is authoritative. For
workspace-only crates, build rustdoc from the current checkout. The
JSON under lexicons/dev/idiolect/ defines record shape. If this book
disagrees with either source, use the source and file an issue.
Crates
The workspace ships eleven crates at version 0.12.1. The workspace version keeps their release numbers aligned.
| Crate | Purpose |
|---|---|
| idiolect-records | Generated record types for the dev.idiolect.* lexicons; Record trait; family modules. |
| idiolect-codegen | Lexicon-driven Rust + TypeScript emitter; generated-source check; breaking-change classifier. |
| idiolect-lens | Resolve PanprotoLens records; run apply_lens. |
| idiolect-identity | DID resolution (did:plc, did:web). |
| idiolect-indexer | Firehose consumer with pluggable stream / handler / cursor store. |
| idiolect-oauth | OAuthTokenStore trait and shipped impls. |
| idiolect-observer | Fold encounter-family records into observation records. |
| idiolect-orchestrator | Read-only HTTP query API over a record catalog. |
| idiolect-verify | Verification runners with declarative dispatch. |
| idiolect-migrate | Schema diff plus lens-based record migration. |
| idiolect-cli | Command-line tool wrapping the library crates. |
Cargo manifests live under crates/<name>/Cargo.toml. Three
crates — idiolect-records, idiolect-identity, and
idiolect-indexer — are published to crates.io under the same
name and to docs.rs at
https://docs.rs/<name>/latest/<name_underscored>/. The rest are
publish = false and are consumed via a git or path reference;
each crate page states which applies.
Reference boundary
These pages list the exported boundaries and feature flags. Use docs.rs for the three published crates. For workspace-only crates, build rustdoc from the checkout named at the top of the page.
idiolect-records
API reference: docs.rs/idiolect-records · Source:
crates/idiolect-records/· Crate: crates.io/idiolect-recordsThis page is an editorial overview. The per-symbol surface (every public type, trait, function, and feature flag) is the docs.rs link above. That is the authoritative reference.
The crate provides Serde record types that mirror the dev.idiolect.*
lexicons. The
contents of crates/idiolect-records/src/generated/ are written
by idiolect-codegen. Do not edit by
hand.
[dependencies]
idiolect-records = "0.12.1"
The crate has no transport dependencies; it contains data types and their validation and dispatch helpers.
Public types
Record trait
Every generated record type implements Record, with associated
constants and methods that let consumers be generic over the
family.
AnyRecord enum
The dispatch primitive returned by
decode_record(&nsid, value). One variant per shipped
dev.idiolect.* record kind (Adapter, Belief, Bounty,
Community, Correction, Deliberation,
DeliberationOutcome, DeliberationStatement,
DeliberationVote, Dialect, Encounter, Observation,
Recommendation, Retrospection, Verification, Vocab).
The vendored panproto record types (PanprotoLens,
PanprotoSchema, PanprotoTheory, PanprotoProtolens,
PanprotoProtolensChain, PanprotoComplement,
PanprotoLensAttestation, PanprotoProtocol, plus
PanprotoCommit, PanprotoRefUpdate, PanprotoRepo) are
re-exported at the crate root as their own structs. They are
not variants of AnyRecord (which is scoped to
IdiolectFamily's NSIDs).
Family
RecordFamily
is the trait every family implements. The crate
ships IdiolectFamily for dev.idiolect.* and the
OrFamily<F1, F2> composer that recognizes every NSID either
side claims. detect_or_family_overlap audits a probe set at
boot so a configuration mistake does not silently shadow the
right-side family.
Typed wrappers
| Type | Format |
|---|---|
AtUri | at-uri |
Did | did |
Nsid | nsid |
Datetime | RFC 3339 |
Uri | URL |
Cid | CID |
Language | BCP 47 |
Each wraps a string with a parser. The parser fires at
deserialize time. Display / as_str / Deref<Target=str> are
uniform.
Vocab graph helpers
VocabGraph is a normalized read-only view over a Vocab
record (graph form, lifted from the legacy tree where present).
VocabRegistry caches multiple graphs by AT-URI for
cross-vocabulary work. The shipped query verbs
(walk_relation on the graph; is_subsumed_by, satisfies,
translate on the registry)
plus the validate walker are documented on docs.rs and in
The vocabulary knowledge graph.
Examples module
idiolect_records::examples::* exports a fixture per record
kind. Each fixture is the deserialized result of the JSON
constant under lexicons/dev/idiolect/examples/<name>.json. The
shipped fixtures cover: adapter, belief, bounty,
community, correction, dialect, encounter,
observation, recommendation, retrospection, verification,
vocab, plus the vendored panproto records (panproto_lens,
panproto_schema, panproto_commit, ...). Use them in tests so
you do not have to hand-roll JSON.
The four deliberation lexicons do not currently ship example fixtures. Consumers building deliberation tests construct records directly via the typed structs.
Feature flags
None. The crate is feature-flag-free and has no transport dependencies.
Errors
The family-decode path returns DecodeError, re-exported as
idiolect_records::DecodeError. UnknownNsid(String) reports an
NSID outside the generated family; Serde(serde_json::Error) reports
a typed deserialization failure. The indexer's separate
IndexerError::FamilyContract variant detects disagreement between a
family's contains and decode methods.
idiolect-codegen
Source:
crates/idiolect-codegen/This crate is
publish = false: it is workspace-internal machinery, not a library you depend on. There is no docs.rs page. The authoritative reference is the source above.
idiolect-codegen is a Rust and TypeScript emitter driven by the project
lexicons. It reads
lexicons/dev/idiolect/*.json and the three spec files
(orchestrator-spec/queries.json, observer-spec/methods.json,
verify-spec/runners.json), then writes the generated modules under each
downstream crate.
The crate is shipped both as a library (callable from a
downstream emitter) and as a binary (cargo run -p idiolect-codegen).
Binary subcommands
cargo run -p idiolect-codegen invokes the binary. The accepted
subcommands are:
| Subcommand | Purpose |
|---|---|
generate (default) | Emit every generated tree. |
check | Verify that the working tree matches generated output; exit nonzero on drift. The legacy --check spelling is also accepted. |
example <nsid> | Print a bundled fixture. A record kind such as encounter may replace the full NSID. |
list | List lexicons, their kinds, and fixture availability. |
doctor | Check the workspace layout and fixtures. |
check-compat --baseline <path> | Classify changes against a baseline lexicon tree; exit 1 if a breaking change is found. |
help | Print command help. |
The check mode compares generated output with the checked-in tree. CI runs it on every PR.
Library API
The callable surface is in idiolect_codegen::emit:
use idiolect_codegen::emit::{emit_rust, emit_typescript};
use idiolect_codegen::emit::family::{FamilyConfig, idiolect_family};
use idiolect_codegen::lexicon::LexiconDoc;
use idiolect_codegen::Example;
emit_rust(docs, examples, family) and
emit_typescript(docs, examples, family) take pre-loaded
LexiconDoc and Example slices plus a &FamilyConfig, and
return anyhow::Result<Vec<EmittedFile>>. Loading lexicons from
disk is the caller's job. The workspace binary parses every
document through both its internal parser and panproto 0.71.0.
FamilyConfig carries three Cow<'static, str> fields: the
marker name, the family ID, and the NSID prefix. The shipped
default for dev.idiolect.* is the idiolect_family()
constructor.
What it emits
Per shipped lexicon (lexicons/dev/idiolect/<name>.json):
- A Rust module under
crates/idiolect-records/src/generated/dev/idiolect/<name>.rswith the typed record struct, every nesteddefstype, theRecordimpl, and the open-enum types with their helpers. - A TypeScript module under
packages/schema/src/generated/with the validator, the discriminator predicates, and the'a' | 'b' | (string & {})open-enum types.
Per spec file:
orchestrator-spec/queries.jsonproduces the orchestrator's HTTP routes (crates/idiolect-orchestrator/src/generated/) and the matching CLI dispatcher (crates/idiolect-cli/src/generated.rs).observer-spec/methods.jsonproduces the observer's method taxonomy (crates/idiolect-observer/src/generated.rs).verify-spec/runners.jsonproduces the verifier's runner taxonomy (crates/idiolect-verify/src/generated.rs).
Each spec file is a single JSON document with a top-level
queries / methods / runners array. Codegen produces the
dispatch tables and typed enums. The hand-written predicates
live alongside the generated tree.
Generated-drift check semantics
cargo run -p idiolect-codegen -- check runs the same emitter
as the default mode, then byte-compares each emitted file
against the working-tree counterpart. Any diff is a drift error,
with a per-file diff. Run cargo run -p idiolect-codegen to
fix.
Identifier policy
Three rules:
- NSIDs are ASCII, lowercase, dot-separated. The emitter rejects non-conforming input.
- PascalCase names are derived deterministically from a slug.
On collision (
foo-barandfoo_bar), the second occurrence gets a numeric suffix (FooBar2). - The emitter walks each record's path until each member's
prefix is unique within the colliding group. The alias is
the unique-prefix concatenation (e.g.
ChangelogEntry,ResourceEntry).
The collision report is printed at codegen time so authors can rename a slug when the generated name is awkward.
idiolect-lens
Source:
crates/idiolect-lens/This crate is
publish = falseand is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally withcargo doc -p idiolect-lens --open.
Resolve dev.panproto.schema.lens records
and run apply_lens. The crate connects idiolect's record runtime to
panproto 0.71.0's lens runtime.
Because the crate is publish = false, downstream consumers
depend on it via a git or path reference rather than a
registry version:
[dependencies]
idiolect-lens = { git = "https://github.com/idiolect-dev/idiolect", tag = "v0.12.1", features = ["pds-reqwest"] }
Public surface
Resolvers
Resolver is the trait every resolver implements. It is
object-safe (Arc<dyn Resolver>), and its resolve future is Send.
Shipped implementations:
| Type | Backing store |
|---|---|
InMemoryResolver | HashMap<AtUri, PanprotoLens>. For tests and fixtures. |
PdsResolver<C> | com.atproto.repo.getRecord via a pluggable PdsClient. |
PanprotoVcsResolver<C> | A panproto vcs store via a pluggable PanprotoVcsClient. |
CachingResolver<R> | TTL'd cache wrapping any R: Resolver. |
VerifyingResolver<R, H> | Re-hashes the bytes, refuses on mismatch. |
Schema loaders
SchemaLoader is also object-safe. Shipped implementations:
InMemorySchemaLoader, FilesystemSchemaLoader, and
PdsSchemaLoader (the loader the tutorials use, pairing with
PdsResolver).
Apply functions
The runtime shipped under idiolect_lens::runtime:
apply_lens/apply_lens_put— state-based forward / backward.apply_lens_get_edit/apply_lens_put_edit— edit-based variants for incremental translation.apply_lens_symmetricpairs two state-based lenses that share a middle schema. This view-only helper calls panproto 0.71.0'sput_without_complement, so the incoming span leg must be an isomorphism. A lossy incoming leg requires complement state from an earliergetand direct use of panproto's lower-levelSymmetricLensAPI.
Each takes a resolver, a schema loader, a Protocol, and a
typed input struct. Each returns a typed output struct. The
composed future is Send so callers can spawn it under
tokio::spawn or hold it inside an #[async_trait] impl.
PDS clients
PdsClient (read) and PdsWriter (write) are the boundary
traits over xrpc. Behind feature flags:
| Feature | Adds |
|---|---|
pds-reqwest | ReqwestPdsClient (read-only). The reqwest-backed write surface uses SigningPdsWriter plus a DpopProver (one of StaticDpopProver, NoOpDpopProver, or P256DpopProver with the dpop-p256 feature). |
pds-atrium | AtriumPdsClient. |
pds-resolve | fetcher_for_did, publisher_for_did; DID-to-PDS resolution helpers. Pulls in idiolect-identity. |
dpop-p256 | The P256DpopProver for OAuth-bound DPoP requests. |
Generic publisher
RecordPublisher<W: PdsWriter> is the typed publisher. Wrap any
PdsWriter with RecordPublisher::new(writer, repo_did) and
publish typed records via publisher.create::<R: Record>(&record),
publisher.put, and publisher.delete. The publisher serializes
the record, splices the $type field, and forwards to the
PdsWriter boundary.
Errors
LensError collapses backend-specific errors into a small set
of variants (NotFound, Transport, decode failures, translate
failures). Backend-specific errors collapse to one of these at
the resolver layer. Callers do not pattern-match on transport
types.
Composition pattern
The recommended runtime stack:
use std::sync::Arc;
use std::time::Duration;
use idiolect_lens::*;
let client = ReqwestPdsClient::with_service_url("https://bsky.social");
let inner: Arc<dyn Resolver> = Arc::new(PdsResolver::new(client));
let verifying = Arc::new(VerifyingResolver::sha256(inner));
let resolver = CachingResolver::new(verifying, Duration::from_secs(300));
let loader = FilesystemSchemaLoader::new("./schema-cache")?;
let out = apply_lens(&resolver, &loader, &Protocol::default(), input).await?;
The Arc<dyn Resolver> indirection lets a downstream application
inject a different resolver, such as an in-memory resolver for tests,
without changing the surrounding API.
idiolect-identity
API reference: docs.rs/idiolect-identity · Source:
crates/idiolect-identity/· Crate: crates.io/idiolect-identityThis page is an editorial overview. The per-symbol surface (every public type, trait, function, and feature flag) is the docs.rs link above. That is the authoritative reference.
The crate resolves a decentralized identifier
(DID) to a structured DidDocument
carrying the also-known-as set, service entries, verification
methods, and any additional fields the source document carries.
[dependencies]
idiolect-identity = { version = "0.12.1", features = ["resolver-reqwest"] }
Public surface
IdentityResolver is the trait every resolver implements. The
crate ships three implementations.
| Type | Feature | Backing |
|---|---|---|
InMemoryIdentityResolver | (always) | HashMap<Did, DidDocument>. Tests and fixtures. |
ReqwestIdentityResolver | resolver-reqwest | Reqwest-backed; resolves did:plc via plc.directory and did:web via .well-known/did.json. |
CachingIdentityResolver<R> | (always) | TTL'd cache wrapping any inner resolver. |
DidDocument carries the resolved data. The shipped accessors
include handle(), pds_url(), and the underlying
also_known_as field. See docs.rs for the full surface.
Errors
IdentityError is the single error type the crate exposes.
Variants distinguish transport failures, parse failures, and
unsupported DID methods.
Feature flags
| Feature | Adds |
|---|---|
resolver-reqwest | The ReqwestIdentityResolver implementation. |
Caching
The shipped CachingIdentityResolver wraps any inner resolver
with the Duration supplied to CachingIdentityResolver::new.
Cache hits skip the HTTP request entirely. Cache misses fall
through to the inner resolver. Errors are not cached.
idiolect-indexer
API reference: docs.rs/idiolect-indexer · Source:
crates/idiolect-indexer/· Crate: crates.io/idiolect-indexerThis page is an editorial overview. The per-symbol surface (every public type, trait, function, and feature flag) is the docs.rs link above. That is the authoritative reference.
The crate factors a firehose consumer into three trait surfaces. It owns the loop. You bring the stream, the handler, and the cursor store.
[dependencies]
idiolect-indexer = { version = "0.12.1", features = ["firehose-jetstream", "cursor-filesystem", "reconnecting"] }
Public surface
Trait surface
pub trait EventStream: Send + Sync {
async fn next_event(&mut self) -> Result<Option<RawEvent>, IndexerError>;
}
pub trait CursorStore: Send + Sync {
async fn load(&self, subscription_id: &str) -> Result<Option<u64>, IndexerError>;
async fn commit(&self, subscription_id: &str, seq: u64) -> Result<(), IndexerError>;
async fn list(&self) -> Result<Vec<(String, u64)>, IndexerError> { /* default */ }
}
pub trait RecordHandler<F: RecordFamily = IdiolectFamily>: Send + Sync {
async fn handle(&self, event: &IndexerEvent<F>) -> Result<(), IndexerError>;
}
IndexerEvent<F> carries the decoded event: seq, live, the
DID, repo revision, rkey, NSID, action (create / update /
delete), CID, and the typed record body (Option<F::AnyRecord>).
Composer
pub async fn drive_indexer<F, S, H, C>(
stream: &mut S,
handler: &H,
cursor_store: &C,
config: &IndexerConfig,
) -> Result<(), IndexerError>
where
F: RecordFamily,
S: EventStream,
H: RecordHandler<F>,
C: CursorStore;
drive_idiolect_indexer is the convenience alias when
F = IdiolectFamily.
Shipped impls
| Type | Feature | Purpose |
|---|---|---|
JetstreamEventStream | firehose-jetstream | Subscribes to a Jetstream websocket feed. |
TappedEventStream | firehose-tapped | Subscribes to the at-proto-native firehose via tapped. |
ReconnectingEventStream<C, F, S> | reconnecting | Recreates an S: EventStream with a caller-supplied async connection factory and exponential backoff. |
InMemoryCursorStore | (always) | HashMap-backed; for tests. |
FilesystemCursorStore | cursor-filesystem | One JSON file containing the cursor map for every subscription ID. |
SqliteCursorStore | cursor-sqlite | One row per stream. Pairs with handlers that also write SQLite. |
NoopRecordHandler | (always) | Counts events and drops them. Useful as a baseline. |
RetryingHandler / CircuitBreakerHandler | resilience | Wraps an inner handler with retry / circuit-breaker policies. |
Error surface
IndexerError flattens the failure modes from all three
boundaries. Variants:
| Variant | Trigger |
|---|---|
Stream(String) | Transport error from the event stream. |
Cursor(String) | Cursor store read or write failed. |
Decode(DecodeError) | A known NSID failed to decode into its typed record. |
Handler(String) | Handler returned a handler-defined error. |
MissingBody(String) | The firehose event had no record body or the body was malformed. |
FamilyContract(String) | contains accepted an NSID but decode returned None; this is a family-implementation bug. |
Feature flags
| Feature | Adds |
|---|---|
firehose-jetstream | Jetstream websocket client. |
firehose-tapped | Tapped at-proto-native firehose client. |
cursor-filesystem | Filesystem cursor store. |
cursor-sqlite | SQLite cursor store. |
reconnecting | Reconnect wrapper. |
resilience | Retry and circuit-breaker handler wrappers. |
Cursor commit semantics
drive_indexer commits the cursor only after the handler
returns Ok. A failing handler does not commit. The loop
returns the error to its caller. Handler retries require the
RetryingHandler wrapper or an application-level policy. The
shipped driver is thus at-least-once: an event may be handled
again if the cursor commit fails. A custom driver can coordinate
its data write and cursor update in one storage transaction when
both use the same backend.
idiolect-oauth
Source:
crates/idiolect-oauth/This crate is
publish = falseand is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally withcargo doc -p idiolect-oauth --open.
The crate provides AT Protocol OAuth session storage through its
token-store trait and shipped implementations. The OAuth dance itself lives
in atrium-oauth-client, and the DPoP signer lives in
idiolect-lens under the dpop-p256 feature.
Because the crate is publish = false, depend via git or path:
[dependencies]
idiolect-oauth = { git = "https://github.com/idiolect-dev/idiolect", tag = "v0.12.1", features = ["store-filesystem"] }
Public surface
OAuthTokenStore is the trait every store implements. Its
methods are save / load / delete keyed by DID, plus a
defaulted list_dids.
OAuthSession carries the access token, refresh token, expiry,
and DPoP key. The session has helpers (is_expired,
time_until_expiry, needs_refresh(now, threshold),
refresh_expired) for callers that want to drive their own
refresh policy.
For the common case, the crate also ships refresh_if_needed
and a Refresher trait: refresh_if_needed(store, refresher, did) loads the session, decides whether to refresh (wall clock
plus a DEFAULT_REFRESH_THRESHOLD_SECS buffer), drives the
caller-supplied Refresher if so, persists the result, and
returns the live session.
Shipped stores
| Store | Feature | Backing |
|---|---|---|
InMemoryOAuthTokenStore | (always) | HashMap-backed; for tests. |
FilesystemOAuthTokenStore | store-filesystem | One JSON file per session. |
SqliteOAuthTokenStore | store-sqlite | One row per session. |
All three implement OAuthTokenStore. The trait uses native async
methods and is not object-safe. Callers thus parameterize their
application over a store type or define an object-safe adapter.
Errors
StoreError covers store-side failures. SessionError covers
session-shape failures. Callers that want a flattened error type
build their own at the application boundary.
Feature flags
| Feature | Adds |
|---|---|
store-filesystem | The filesystem-backed session store. |
store-sqlite | The SQLite-backed session store. |
DPoP keys
The session carries a Demonstrating Proof of Possession (DPoP) private key as a JWK. Persistence is the store's responsibility; both shipped stores persist it alongside the session. A custom store must do the same. The OAuth RFC does not define this key; RFC 9449 defines DPoP key binding. Reusing the bound key is necessary for requests made with the same DPoP-bound token.
The signer behind the DPoP-bound HTTP layer is P256DpopProver
in idiolect-lens under the dpop-p256 feature. The lens
crate's SigningPdsWriter wraps a DpopProver so every PDS
write sends a DPoP-bound proof header.
idiolect-observer
Source:
crates/idiolect-observer/This crate is
publish = falseand is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally withcargo doc -p idiolect-observer --open.
The crate folds encounter-family records into observation
records. The declarative spec at observer-spec/methods.json drives the
surface, and code generation emits the method-descriptor table.
Because the crate is publish = false, depend via git or path:
[dependencies]
idiolect-observer = { git = "https://github.com/idiolect-dev/idiolect", tag = "v0.12.1", features = ["daemon"] }
Public surface
pub trait ObservationMethod: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str;
/* observe(...) plus snapshot accessors; see source */
}
pub trait ObservationPublisher: Send + Sync {
/* persist or transmit a finished observation record */
}
pub struct ObserverHandler<M, P> { /* RecordHandler<IdiolectFamily> impl wiring an ObservationMethod onto an ObservationPublisher */ }
pub async fn drive_observer<S, C, M, P>(
stream: &mut S,
cursor_store: &C,
handler: &ObserverHandler<M, P>,
schedule: FlushSchedule,
) -> ObserverResult<()>;
ObservationMethod is the stateful aggregator. It folds decoded
events into internal state and snapshots that state on flush.
ObserverHandler wires the method onto the indexer's
RecordHandler boundary. drive_observer runs the indexer loop
and triggers periodic flushes that publish observations through
the configured publisher.
Shipped methods
The spec at observer-spec/methods.json declares nine bundled
methods. The typed structs ship in crates/idiolect-observer/src/methods/:
| Spec name | Module | Folds |
|---|---|---|
correction-rate | correction_rate | Per-lens correction counts grouped by reason. |
encounter-throughput | encounter_throughput | Encounter traffic by kind and downstream result. |
verification-coverage | verification_coverage | Per-lens verification counts by kind, result, and distinct verifiers. |
lens-adoption | lens_adoption | Per-lens encounter count and distinct invokers. |
action-distribution | action_distribution | Encounter counts grouped by use.action (with optional vocab roll-up). |
purpose-distribution | purpose_distribution | Encounter counts grouped by use.purpose. |
basis-distribution | basis_distribution | Record counts grouped by basis variant, bucketed by record kind. |
attribution-chains | attribution_chains | Counts of dev.idiolect.belief records by holder and subject. |
deliberation-tally | deliberation_tally | Per-statement per-stance deliberationVote counts, packed into the observation's output. |
Methods come in two forms (declared in the spec):
Record-form methods consume&IndexerEvent<IdiolectFamily>directly and implementObservationMethod.Instance-form methods consume a panprotoWInstanceplus the NSID and implementInstanceMethod. They wrap intoObservationMethodviaInstanceMethodAdapter.
default_methods() returns boxed instances of every
record-form method.
Publisher
ObservationPublisher is the persistence boundary. Shipped
implementations:
| Type | Backing |
|---|---|
InMemoryPublisher | Vec<Observation>. For tests. |
PdsPublisher | Writes via an idiolect_lens::PdsWriter to the observer's PDS. |
LogPublisher | Emits each observation as a structured tracing event. |
Errors
ObserverError is a flattened error type. ObserverResult<T>
is its alias.
Feature flags
| Feature | Adds |
|---|---|
daemon | The idiolect-observer binary plus its CLI. Pulls in tracing-subscriber, anyhow, and the indexer's tapped firehose / sqlite cursor features. |
pds-atrium | Forwards to idiolect-lens/pds-atrium for the PDS publisher. |
Adding a method
Edit observer-spec/methods.json, add the method's entry, run
cargo run -p idiolect-codegen. The generated descriptor table
picks up the new method. You write the
ObservationMethod (or InstanceMethod) impl in
crates/idiolect-observer/src/methods/<module>.rs and add it to
the default_methods() constructor.
idiolect-orchestrator
Source:
crates/idiolect-orchestrator/This crate is
publish = falseand is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally withcargo doc -p idiolect-orchestrator --features daemon --open.
The crate exposes a read-only HTTP query API over a record
catalog. orchestrator-spec/queries.json drives
the surface, and code generation emits both the routes and the matching CLI
dispatcher.
Because the crate is publish = false, depend via git or path:
[dependencies]
idiolect-orchestrator = { git = "https://github.com/idiolect-dev/idiolect", tag = "v0.12.1", features = ["daemon", "catalog-sqlite", "query-http"] }
Public surface
The crate exposes:
Catalog— an in-memory struct holdingEntry<R>slots per record kind, with typed iterators (encounters(),bounties(),verifications(), ...).CatalogRef— a shareable handle around the catalog that the HTTP handlers and the indexer's record handler both hold.SqliteCatalogStore(undercatalog-sqlite) — persistent catalog backing.CatalogHandler— the indexer'sRecordHandler<IdiolectFamily>impl that upserts every accepted record into the catalog.AppStateplushttp_router()— axum router wiring underquery-http.- Theory-resolver and predicate-evaluator helpers used by generated query handlers.
HTTP endpoints
Every handler under the v1 prefix is generated from
orchestrator-spec/queries.json. The current shipped routes:
| Path | Returns |
|---|---|
GET /healthz, GET /readyz | Liveness + readiness. |
GET /metrics | Prometheus exposition. |
GET /v1/stats | Per-kind record counts. |
GET /v1/bounties/open | Cataloged bounties whose status is open / claimed / unset. |
GET /v1/bounties/want-lens?... | Bounties whose wants is a specific lens. |
GET /v1/bounties/by-requester?requester_did=... | Bounties by requester. |
GET /v1/adapters?framework=... | Adapters by framework. |
GET /v1/adapters/by-invocation-protocol?... | Adapters by invocation-protocol kind. |
GET /v1/adapters/with-verification?... | Adapters with at least one verification record. |
GET /v1/recommendations | Recommendations starting from a given source schema. |
GET /v1/verifications?lens_uri=... | Verifications for a specific lens. |
GET /v1/verifications/by-kind?... | Verifications by kind. |
GET /v1/verifications/sufficient?lens_uri=...&kinds=... | Whether each comma-separated verification kind has at least one holds record for the lens. |
GET /v1/communities?... | Communities for a member DID. |
GET /v1/communities/by-name?... | Communities by name. |
GET /v1/dialects/for-community?... | Dialects owned by a community. |
GET /v1/beliefs/about?... | Beliefs whose subject is a given record. |
GET /v1/beliefs/by-holder?... | Beliefs by holder DID. |
GET /v1/vocabularies/by-world?... | Vocabularies declared with a given world. |
GET /v1/vocabularies/by-name?... | Vocabularies by name. |
Each generated route is also mounted at its /xrpc/<query-nsid>
alias. The full path-and-flag table for each endpoint is generated. See
orchestrator-spec/queries.json
for the authoritative list.
Errors
OrchestratorError flattens catalog and HTTP errors.
OrchestratorResult<T> is its alias.
Feature flags
| Feature | Adds |
|---|---|
catalog-sqlite | SQLite-backed catalog store. |
query-http | HTTP server (axum-based). |
daemon | The idiolect-orchestrator binary, wiring the indexer plus catalog plus HTTP API with a tapped-backed firehose. |
Observability
/metrics exposes the readiness gauge, per-kind catalog gauges, and
the total catalog gauge in Prometheus text format. The daemon emits
structured lifecycle and failure logs through tracing. The exact
metric names are defined in crates/idiolect-orchestrator/src/http.rs.
idiolect-verify
Source:
crates/idiolect-verify/This crate is
publish = falseand is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally withcargo doc -p idiolect-verify --open.
The crate provides verification runners with
declarative dispatch. verify-spec/runners.json drives the surface, and code
generation emits the kind taxonomy.
Because the crate is publish = false, depend via git or path:
[dependencies]
idiolect-verify = { git = "https://github.com/idiolect-dev/idiolect", tag = "v0.12.1" }
Public surface
pub trait VerificationRunner: Send + Sync {
fn kind(&self) -> VerificationKind;
fn tool(&self) -> Tool;
async fn run(&self, target: &VerificationTarget) -> VerifyResult<Verification>;
}
A runner returns a Verification record with result set to
Holds, Falsified, or Inconclusive. Falsification is not
an error: a falsified verification is a first-class record.
VerifyError is reserved for input-shape, transport, or
irrecoverable-state failures.
The runner::build_verification helper packages a runner result into a
Verification record shaped for direct publication via
idiolect_lens::RecordPublisher::create.
Shipped runners
The spec at verify-spec/runners.json declares four bundled
kinds:
| Kind | Runner |
|---|---|
roundtrip-test | RoundtripTestRunner — runs put(get(a)) == a over a corpus. |
property-test | PropertyTestRunner — runs an arbitrary boolean predicate over a corpus. |
static-check | StaticCheckRunner — resolves the lens's source and target schemas and runs panproto's schema validator on both. |
coercion-law | CoercionLawRunner — runs panproto's sample-based coercion-law checker, optionally via a CoercionLawClient. |
The lexicon's verification.kind field is open-enum and lists
additional kinds (formal-proof, conformance-test,
convergence-preserving). Those kinds are recognized but not
shipped as runners. Communities that need them author their own
runner against the trait.
Errors
VerifyError covers input-shape, transport, and
irrecoverable-state failures. VerifyResult<T> is its alias.
Result records
A passing run produces a Verification record with
result: "holds". A falsifying run produces one with
result: "falsified" plus a counterexample (when the runner
captured one). The publisher path uses
idiolect_lens::RecordPublisher.
Adding a runner
- Add the kind's entry to
verify-spec/runners.json. - Run
cargo run -p idiolect-codegento refresh the generated kind taxonomy. - Implement the
VerificationRunnertrait against the new kind in a new module undercrates/idiolect-verify/src/. - Re-export it from
lib.rsand add to the runner registry wiring.
idiolect-migrate
Source:
crates/idiolect-migrate/This crate is
publish = falseand is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally withcargo doc -p idiolect-migrate --open.
The crate combines schema-diff classification with
lens-based record migration. It is a typed facade over panproto 0.71.0's
panproto-check crate and idiolect-lens.
Because the crate is publish = false, depend via git or path:
[dependencies]
idiolect-migrate = { git = "https://github.com/idiolect-dev/idiolect", tag = "v0.12.1" }
Public surface
The crate exposes:
classify(old, new, protocol)runs the panproto diff and returns aCompatReportdistinguishing compatible from breaking changes.plan_auto(old, new, protocol, source_schema_hash, target_schema_hash)asks panproto'sauto_generateto derive aMigrationPlancarrying source / target schema hashes plus a protolens chain the caller can publish. For breaking diffs that resist automatic derivation, it returnsErr(PlannerError::NotAutoDerivable)listing the offending changes.migrate_record(resolver, schema_loader, protocol, lens_uri, source_record)wrapsidiolect_lens::apply_lensfor one record.MigrationPlan— the typed plan struct.MigrateError,MigrateResult,PlannerError— the error types.- Re-exported
CompatReportandSchemaDifffrompanproto-checkfor convenience.
Migration shapes
| Diff | Behavior |
|---|---|
| Non-breaking (added optional, added vertex, added edge) | classify returns compatible = true; no plan is needed. |
| Auto-derivable breaking | plan_auto returns a MigrationPlan with a protolens-chain body and an alignment-quality score. The exact supported shapes follow panproto 0.71.0's auto_generate. |
| Non-auto breaking (removed required, changed required type, added required without default) | plan_auto returns NotAutoDerivable. The caller writes the lens by hand. |
The alignment-quality score ranks alternatives for one source schema. Panproto 0.71.0 does not give it a pair-independent confidence interpretation.
Dependency boundary
Two reasons:
- The migration-shaped API (classify-then-plan-then-migrate)
is a different shape than the runtime API
(
apply_lensplus resolvers). idiolect-migratedepends onpanproto-check, which is a heavier dependency than the lens runtime itself. Separating it avoids adding that dependency to applications that only apply existing lenses.
The idiolect-migrate binary
Behind the cli feature the crate also ships an
idiolect-migrate binary (src/bin/idiolect_migrate.rs) that
streams a directory of JSON records through a lens at-uri against
a live PDS. The library is the default; the binary is opt-in:
cargo install --path crates/idiolect-migrate --features cli
See Migrate records across a revision for the batch-migration flow.
Scope
The crate is a thin facade with no runtime state. The
runtime cost of a migration equals the cost of one apply_lens
per record.
idiolect-cli
Source:
crates/idiolect-cli/This crate is
publish = falseand is not on docs.rs. The CLI is intended to be installed and run, not depended on as a library.
The idiolect command-line tool wraps the library crates so operators and end
users do not need to write Rust for common operations.
cargo install --path crates/idiolect-cli
The CLI hardcodes the idiolect-lens features it needs
(pds-reqwest, pds-resolve). There are no CLI-level features
to set.
Subcommand surface
idiolect resolve <did>
idiolect fetch <at-uri>
idiolect orchestrator <subcommand>
idiolect encounter record [...]
idiolect oauth <login|list|logout> [...]
idiolect publish <kind> --record <path> [...]
idiolect verify <kind> [...]
idiolect version
idiolect help
The full reference is the CLI reference.
Parser boundary
The CLI uses a hand-rolled subcommand parser. Two reasons:
- The orchestrator subcommand surface is partly codegen-emitted
from
orchestrator-spec/queries.json. A clap surface would put two layers of declarative wiring on top of each other. - Compile time and binary size matter for a tool meant to be installed broadly.
Codegen surface
The CLI's orchestrator … dispatcher is emitted from
orchestrator-spec/queries.json into
crates/idiolect-cli/src/generated.rs. Adding a query (per the
orchestrator guide) regenerates
the dispatcher. The new subcommand becomes available
automatically.
The hand-written subcommands (resolve, fetch, encounter record,
oauth, publish, verify, version, and help) live in
main.rs and their sibling modules.
Output
All commands print pretty-printed JSON to stdout on success.
Errors go to stderr with error: <message>.
Configuration
| Setting | Default | Override |
|---|---|---|
| Orchestrator URL | http://localhost:8787 | --url flag on orchestrator subcommands |
| Log level | warn | RUST_LOG environment variable |
| Session directory | $HOME/.config/idiolect/sessions | IDIOLECT_SESSION_DIR environment variable |
| Login app password | none | --app-password, ATPROTO_APP_PASSWORD, or ATPROTO_PASSWORD |
The CLI does not read a config file.
Lexicons
This section covers the dev.idiolect.* lexicon family. Every record
kind that travels on the network has a lexicon document under
lexicons/dev/idiolect/.
| NSID | Page |
|---|---|
dev.idiolect.adapter | adapter |
dev.idiolect.belief | belief |
dev.idiolect.bounty | bounty |
dev.idiolect.community | community |
dev.idiolect.correction | correction |
dev.idiolect.defs | defs |
dev.idiolect.deliberation | deliberation |
dev.idiolect.deliberationStatement | deliberationStatement |
dev.idiolect.deliberationVote | deliberationVote |
dev.idiolect.deliberationOutcome | deliberationOutcome |
dev.idiolect.dialect | dialect |
dev.idiolect.encounter | encounter |
dev.idiolect.observation | observation |
dev.idiolect.recommendation | recommendation |
dev.idiolect.retrospection | retrospection |
dev.idiolect.verification | verification |
dev.idiolect.vocab | vocab |
Policy
The pages in this section are navigation summaries. The
authoritative shape for every lexicon is the JSON document under
lexicons/dev/idiolect/<name>.json; the generated Rust types on
docs.rs/idiolect-records
are derived from that JSON and are the authoritative typed
surface. When this book and either source disagree, the source
wins. Please file an issue.
dev.idiolect.adapter
A published record that declares how to invoke a framework wrapper and what isolation it requests. The shipped orchestrator catalogs these declarations; it does not execute adapters or enforce their isolation policies. A separate adapter host must interpret and enforce the contract.
Source:
lexicons/dev/idiolect/adapter.json· Rust:idiolect_records::Adapter· TS:@idiolect-dev/schema/adapter· Fixture:idiolect_records::examples::adapter
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
framework | string (at most 128 characters) | yes | Canonical framework name (e.g. hasura, prisma, coq). |
versionRange | string | yes | Semver range supported. |
invocationProtocol | object | yes | How the adapter is invoked. |
isolation | object | yes | Sandboxing requirements an adapter host is expected to enforce. |
author | did | yes | DID of the adapter author. |
verification | at-uri | no | Optional verification record demonstrating conformance. |
occurredAt | datetime | yes | Publication timestamp. |
invocationProtocol
| Subfield | Type | Required | Notes |
|---|---|---|---|
kind | open enum | yes | subprocess / http / wasm. |
kindVocab | vocabRef | no | Vocab the kind slug resolves against. |
entryPoint | string | no | Binary name (subprocess), URL (http), or WASM module reference. |
inputSchema | schemaRef | no | Schema of the adapter's input. |
outputSchema | schemaRef | no | Schema of the adapter's output. |
isolation
| Subfield | Type | Required | Notes |
|---|---|---|---|
kind | open enum | yes | none / process / container / vm / wasm-sandbox. |
kindVocab | vocabRef | no | Vocab the kind slug resolves against. |
networkPolicy | open enum | no | none / egress-denylist / egress-allowlist / full. |
networkPolicyVocab | vocabRef | no | Vocab the policy slug resolves against. |
filesystemPolicy | open enum | no | readonly / scratch / writable-subtree / full. |
filesystemPolicyVocab | vocabRef | no | Vocab the policy slug resolves against. |
resourceLimits | { maxMemoryBytes?, maxCpuSeconds?, maxWallSeconds? } | no | Requested ceilings for the adapter host. |
Field details
Contract semantics
The publisher asserts that a framework in versionRange supports
the declared invocation protocol under the requested isolation
policy. Cataloging the record does not verify that assertion.
Consumers may use linked verification records to evaluate specific
conformance claims before passing the record to an adapter host.
invocationProtocol.kind
The transport an adapter host uses:
| Slug | What it means |
|---|---|
subprocess | Fork entryPoint as a child process and exchange JSON over standard input and output. |
http | Send JSON to the URL at entryPoint. |
wasm | Instantiate the WebAssembly module at entryPoint and call the host-defined export. |
The slug is open-enum: a community publishing a vocab with an
additional kind (e.g. nats-rpc, grpc-stream) extends the
transport set without modifying the lexicon.
isolation.kind
The sandboxing posture requested from an adapter host:
| Slug | What it means |
|---|---|
none | Run in the orchestrator's own process. Only safe for fully-trusted code. |
process | Fork into a separate process; OS-level isolation. |
container | Run in a container (Docker, Podman, Firecracker microVM). |
vm | Run in a full VM. |
wasm-sandbox | Run in a WASM runtime with capability-based access. |
The lexicon does not define an ordering among these values or a minimum-isolation policy. A host that treats them as ordered must document that local policy.
Network and filesystem policies
Orthogonal axes layered on top of the kind:
networkPolicy | What it means |
|---|---|
none | No network access. |
egress-denylist | Network access except to listed denied hosts. |
egress-allowlist | Network access only to listed allowed hosts. |
full | Unrestricted. |
filesystemPolicy | What it means |
|---|---|
readonly | The adapter sees a read-only mount. |
scratch | The adapter writes to a scratch directory cleaned up after each invocation. |
writable-subtree | The adapter writes to a designated subtree. |
full | Unrestricted. |
These fields declare requested capabilities; enforcement depends on the adapter host and its isolation runtime. Consumers should not infer enforcement from the existence of the record.
resourceLimits
Requested ceilings for a host that supports resource accounting:
| Field | Unit |
|---|---|
maxMemoryBytes | RAM, in bytes. |
maxCpuSeconds | CPU time, in seconds. |
maxWallSeconds | Wall-clock time, in seconds. |
A consumer running an untrusted adapter sets all three.
verification
An optional pointer to a dev.idiolect.verification record
demonstrating conformance. A consumer that wants to trust an
adapter's claim about its inputSchema / outputSchema looks for
a conformance-test verification (see
verification).
Example
{
"$type": "dev.idiolect.adapter",
"framework": "hasura",
"versionRange": "^2.30",
"invocationProtocol": {
"kind": "http",
"entryPoint": "https://hasura.example/v1/graphql",
"inputSchema": { "uri": "at://did:plc:adapter-author/dev.panproto.schema.schema/hasura-input" },
"outputSchema": { "uri": "at://did:plc:adapter-author/dev.panproto.schema.schema/hasura-output" }
},
"isolation": {
"kind": "container",
"networkPolicy": "egress-allowlist",
"filesystemPolicy": "scratch",
"resourceLimits": {
"maxMemoryBytes": 1073741824,
"maxCpuSeconds": 30,
"maxWallSeconds": 60
}
},
"author": "did:plc:adapter-author",
"occurredAt": "2026-04-19T00:00:00.000Z"
}
Concept references
- Concepts: The dev.idiolect.* lexicon family
- Lexicons: verification · bounty (
wantAdapter)
dev.idiolect.belief
A signed doxastic claim that a referenced record is true or
applicable. Belief records are how third parties represent what
they think about another record without flattening provenance:
the subject strong reference
pins the exact referenced record, the
holder identifies the party whose attitude is represented, and
the basis records what grounds it.
Source:
lexicons/dev/idiolect/belief.json· Rust:idiolect_records::Belief· TS:@idiolect-dev/schema/belief· Fixture:idiolect_records::examples::belief
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
subject | strongRecordRef | yes | AT-URI + CID for the record the belief is about. |
holder | did | no | Party whose attitude is represented. Omit for first-party. |
basis | basis | no | Structured grounding (load-bearing when holder differs from the repo owner). |
annotations | string (at most 4,000 graphemes) | no | Narrative commentary. |
visibility | visibility | no | Visibility scope. |
occurredAt | datetime | yes | When the belief was published. |
Field details
subject is a strong reference
subject carries both the AT-URI and the CID of the record the
belief is about. Pinning by CID means a later revision of the
target record does not silently change what this belief asserts.
A consumer reading a belief about a recommendation can fetch the
exact recommendation revision the holder believed in, even if the
recommendation has been edited since.
holder versus the repo owner
The simplest case: the repo owner is the holder. The record
expresses the publisher's own belief, and holder is omitted.
The richer case: the repo owner is publishing a belief on behalf
of, or about, another party. A labeler that publishes
{ subject: <some encounter>, holder: did:plc:other-party }
is asserting that the other party believes the encounter is
applicable. The labeler's signature attests that the labeler
made the attribution. The holder field carries who the attitude
is attributed to.
basis carries the grounds
When the holder is not the repo owner, the consumer needs to know
on what grounds the attribution rests. The four basis variants:
| Variant | Use when |
|---|---|
basisSelfAsserted | The holder asserted directly with no external grounding claimed. The default when basis is omitted. |
basisCommunityPolicy | Grounded in a community's published policy. |
basisExternalSignal | Grounded in something outside ATProto (a license, an external standard, a statement on another network). |
basisDerivedFromRecord | Grounded in another ATProto record (with an inference rule identifying the derivation). |
See defs#basis for the field shapes.
Labeler use
A labeler workflow that wants to record "a third party endorses this lens for a particular use" uses three records:
- The lens record (in the lens author's PDS).
- The encounter or recommendation describing the use (in either the labeler's or the third party's PDS).
- A belief record (in the labeler's PDS) with
subjectpointing at the encounter / recommendation,holderset to the third party's DID, andbasiscarrying the structured grounds.
Consumers reading the belief see all three. The labeler's signature on the belief makes the attribution accountable.
Example
{
"$type": "dev.idiolect.belief",
"subject": {
"uri": "at://did:plc:other-party/dev.idiolect.recommendation/3l5",
"cid": "bafyreidfcm4u3vnuph5ltwdpssiz3a4xfbm2otjrdisftwnbfmnxd6lsxm"
},
"holder": "did:plc:other-party",
"basis": {
"$type": "dev.idiolect.defs#basisCommunityPolicy",
"community": "at://did:plc:community/dev.idiolect.community/canonical",
"policyUri": "https://community.example/policies/lens-endorsement-v1"
},
"annotations": "Labeler attests the holder accepted this recommendation.",
"visibility": "public-detailed",
"occurredAt": "2026-04-19T00:00:00.000Z"
}
Related records
flowchart LR
L[labeler] -->|publishes| B[belief]
B -->|subject| R[recommendation]
B -->|holder| H[holder DID]
B -->|basis| G[community policy]
R -->|signed by| C[community]
C -.recognized by.-> H
A consumer reading the belief sees: which record is being
endorsed (subject), who is doing the endorsing (holder), on
what grounds (basis), and who is attributing it (the repo
signer). Disagreements among labelers about a holder's beliefs
are themselves first-class records: a contradicting belief from
a different labeler is just another record, signed and visible.
Concept references
- Concepts: The dev.idiolect.* lexicon family
- Concepts: Records as content-addressed signed data
- Lexicons: defs (
#basis,#strongRecordRef) · recommendation
dev.idiolect.bounty
A declaration that a lens, verification, or adapter is wanted, with terms. idiolect does not intermediate fulfillment: payment, review, and acceptance happen on external rails referenced in the record.
Source:
lexicons/dev/idiolect/bounty.json· Rust:idiolect_records::Bounty· TS:@idiolect-dev/schema/bounty· Fixture:idiolect_records::examples::bounty
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
requester | did | yes | Who is requesting. |
wants | union | yes | Exactly one of wantLens / wantVerification / wantAdapter. |
constraints | array (at most 64 entries) | no | Structured constraints the deliverable must satisfy. |
reward | object | no | { summary?, externalRef? }. idiolect does not transact. |
eligibility | array (at most 128 entries) | no | Postfix eligibility tree. |
fulfillment | at-uri | no | Once fulfilled, points to the deliverable record. |
status | open enum | no | open / claimed / fulfilled / withdrawn. |
statusVocab | vocabRef | no | Vocab the status slug resolves against. |
basis | basis | no | Structured grounding. |
occurredAt | datetime | yes | Publication timestamp. |
The three want shapes
wantLens
Asks for a lens between two schemas.
| Subfield | Type | Required | Notes |
|---|---|---|---|
source | schemaRef | yes | Source schema. |
target | schemaRef | yes | Target schema. |
bidirectional | boolean | no | Whether the requested lens must be invertible. |
wantVerification
Asks for a verification of a lens.
| Subfield | Type | Required | Notes |
|---|---|---|---|
lens | lensRef | yes | The lens to verify. |
kind | open enum | yes | Verification kind: roundtrip-test / property-test / formal-proof / conformance-test / static-check / convergence-preserving. (Note: the lexicon's known-values list here omits coercion-law; that kind is reachable through the open-enum extension via kindVocab.) |
kindVocab | vocabRef | no | Vocab the kind slug resolves against. |
wantAdapter
Asks for an adapter for a framework.
| Subfield | Type | Required | Notes |
|---|---|---|---|
framework | string (at most 128 characters) | yes | Framework name. |
versionRange | string | no | Semver range. |
The constraint variants
Each entry in constraints is one of:
| Variant | Captures |
|---|---|
constraintPerformance | A quantitative bound: metric, threshold, comparison direction (lt, le, eq, ge, gt), optional sample size. For instance, p99-latency-ms le 50 or error-rate lt 0.001. |
constraintConformance | A verification kind (and optional specific property) the deliverable must pass. |
constraintLicense | An SPDX license expression plus optional allow and deny lists. |
constraintDeadline | A datetime deadline plus optional grace seconds. |
constraintDependency | A pointer to another bounty this one waits on. Claims are ineligible until the dependency's status is fulfilled. |
A consumer matching a candidate deliverable against a bounty walks the constraint list and verifies each one. Failing constraints are surfaced individually so the claimer can tell what is missing.
The eligibility tree
eligibility is a postfix-operator tree (same shape as
recommendation conditions). Atomic predicates plus combinators:
| Variant | Arity | Meaning |
|---|---|---|
eligibilityMember | atomic | Claimer is a member of the named community. |
eligibilityVerificationFor | atomic | Claimer has published a verification for the named lens property. |
eligibilityDid | atomic | Claimer's DID matches exactly. |
eligibilityAnd | combinator | Conjoin top two on stack. |
eligibilityOr | combinator | Disjoin top two on stack. |
eligibilityNot | combinator | Negate top on stack. |
A bounty restricted to a specific community uses
[eligibilityMember(community=...)]. A bounty open to either of
two communities uses
[eligibilityMember(A), eligibilityMember(B), eligibilityOr].
Empty array means no eligibility restriction.
Field details
reward
reward is intentionally underspecified. summary is narrative
prose. externalRef is a URL pointing at the rail that handles
the actual reward (a grant portal, a payment platform, an
attestation service). idiolect does not validate that the
external rail exists, that it is solvent, or that the reward will
be paid. Consumers must verify the external rail and its payment
terms themselves.
An externalRef to a known grant portal gives consumers a payment
claim they can check; a narrative-only reward does not. Consumers
can route their effort on that distinction.
fulfillment
Once a deliverable exists, the bounty publisher edits the bounty
record (a put, not a new record) to set fulfillment to the
deliverable's at-uri and status to fulfilled. Consumers
querying open bounties filter on status: open. Consumers
auditing the fulfilled set filter on status: fulfilled and
follow fulfillment.
basis
For first-party bounties (the repo owner is the requester), omit.
For third-party attribution (a labeler records that someone else
is requesting), set basis and requester accordingly. The
common case is basisCommunityPolicy (a community has a standing
policy of requesting verifications of every published lens) or
basisDerivedFromRecord (a researcher infers a request from a
prior recommendation that listed required verifications).
Example
{
"$type": "dev.idiolect.bounty",
"requester": "did:plc:requester",
"wants": {
"$type": "dev.idiolect.bounty#wantVerification",
"lens": { "uri": "at://did:plc:lens-author/dev.panproto.schema.lens/3l5" },
"kind": "roundtrip-test"
},
"constraints": [
{ "$type": "dev.idiolect.bounty#constraintConformance",
"kind": "roundtrip-test",
"property": {
"$type": "dev.idiolect.defs#lpRoundtrip",
"domain": "all valid v1 records with bodies at most 1024 bytes"
}
},
{ "$type": "dev.idiolect.bounty#constraintDeadline",
"deadline": "2026-06-19T00:00:00.000Z"
}
],
"reward": {
"summary": "USD 500 paid via XYZ grants portal on acceptance.",
"externalRef": "https://grants.example/bounties/3l5"
},
"eligibility": [
{ "$type": "dev.idiolect.bounty#eligibilityMember",
"community": "at://did:plc:community/dev.idiolect.community/canonical" }
],
"status": "open",
"occurredAt": "2026-04-19T00:00:00.000Z"
}
Related records
flowchart LR
R[requester] -->|publishes| B[bounty]
B -->|wants verification| L[lens]
B -->|eligibility| E[claimer]
E -->|publishes| V[verification]
R -->|edits bounty| F[status: fulfilled]
F -.points at.-> V
A community publishing recommendations with requiredVerifications
that nobody has published is asking for verification work. A
bounty is how you request that work. A claimer that
matches the eligibility predicate publishes the verification, the
requester points the bounty at it, and the external rail handles
the payment.
Concept references
dev.idiolect.community
A group of DIDs that declare shared conventions. Self-constituted: there is no central roll and no grading of legitimacy. Communities may be small and many.
Source:
lexicons/dev/idiolect/community.json· Rust:idiolect_records::Community· TS:@idiolect-dev/schema/community· Fixture:idiolect_records::examples::community
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
name | string (≤128) | yes | Human-readable community name. |
description | string (≤2000 graphemes) | yes | Purpose, norms, scope. Narrative. |
members | array of did (≤500) | no | Inline membership for small communities. |
roleAssignments | array of roleAssignment (≤500) | no | Sparse role assignments where the role differs from the default. |
memberRoleVocab | vocabRef | no | Vocab the role slugs resolve against. |
recordHosting | open enum | no | member-hosted / community-hosted / hybrid. |
appviewEndpoint | uri | no | URL of the community AppView when recordHosting is non-default. |
membershipRoll | at-uri | no | External membership record (for communities above ~200 members). |
coreSchemas | array of schemaRef | no | Schemas the community treats as canonical. |
coreLenses | array of lensRef | no | Lenses the community treats as canonical. |
endorsedCommunities | array of at-uri | no | Other communities recognized as legitimate interlocutors. Not transitive. |
conventions | array (≤64) of structured convention variants | no | Decidable subset of community conventions. |
conventionsText | string (≤10000 graphemes) | no | Narrative conventions: style guides, norms not expressible structurally. |
createdAt | datetime | yes | Publication timestamp. |
roleAssignment
| Subfield | Type | Required | Notes |
|---|---|---|---|
did | did | yes | DID of the member. |
role | open enum | yes | member / moderator / delegate / author. |
A DID may appear multiple times when the role vocabulary supports multiple roles per member.
The convention variants
Each entry in conventions is one of three shapes:
| Variant | Captures |
|---|---|
conventionReviewCadence | Maximum business-days expected before a review is posted, with optional scope narrowing. |
conventionVerificationReq | A verification kind (and optionally a specific property) the community requires before endorsing a lens. |
conventionDeprecationPolicy | Minimum deprecation notice in days; whether deprecations require a replacement pointer. |
The structured subset is what consumers can match on
mechanically. Style guides, tone, and norms not expressible as a
predicate live in conventionsText.
Field details
members versus membershipRoll
Membership has two representations:
- Inline
membersis appropriate for small communities. The list lives directly on the community record; reading the community gives you the membership in one fetch. Capped at 500 entries. - External
membershipRollis a pointer to a separate record that maintains the roll. Appropriate for larger communities (above ~200 members) where the roll is updated frequently and shouldn't bloat the community record itself.
A community may use both for the transition period while moving from inline to external. Consumers union the two sets.
roleAssignments versus the default role
The default role (declared on the role vocabulary's top node) applies
to every member who does not have an explicit roleAssignment.
Only members whose role differs from the default need an entry.
A 500-member community with five moderators carries five
roleAssignment entries, not five hundred.
The shipped default vocabulary seeds member (top), moderator,
delegate, author. A community extends by referencing a custom
memberRoleVocab with additional roles.
recordHosting
| Slug | What it means |
|---|---|
member-hosted | Records live on individual member PDSes (default ATProto). |
community-hosted | Records live on a community AppView, gated by membership (Acorn-style). |
hybrid | Both. Some records are member-hosted, others are AppView-hosted. |
Consumers crawling for community records use this to choose a
surface. A community that publishes community-hosted plus an
appviewEndpoint is telling consumers to route XRPC reads
through the AppView instead of crawling member PDSes.
endorsedCommunities
A community lists other communities it recognizes as legitimate interlocutors. The endorsement is not transitive: A endorsing B and B endorsing C does not imply A endorsing C. The record only states the assertion. Consumers decide what to do with it. Common patterns: a quorum policy that requires endorsements from trusted communities; a denylist that excludes communities not endorsed by any trusted party.
Example
{
"$type": "dev.idiolect.community",
"name": "tutorial",
"description": "Tutorial community for the idiolect documentation.",
"members": [
"did:plc:alice", "did:plc:bob", "did:plc:carol"
],
"roleAssignments": [
{ "did": "did:plc:alice", "role": "moderator" }
],
"recordHosting": "member-hosted",
"coreSchemas": [
{ "uri": "at://did:plc:tutorial/dev.panproto.schema.schema/post-v1" }
],
"conventions": [
{
"$type": "dev.idiolect.community#conventionVerificationReq",
"kind": "roundtrip-test"
},
{
"$type": "dev.idiolect.community#conventionDeprecationPolicy",
"noticePeriodDays": 90,
"replacementRequired": true
}
],
"conventionsText": "Lens authors review within five business days. Style: terse, factual.",
"createdAt": "2026-04-19T00:00:00.000Z"
}
Concept references
dev.idiolect.correction
A signed record of a post-translation edit. Corrections are the primary signal an observer uses to detect lens quality issues; the reason taxonomy distinguishes lens errors from domain differences, source errors, downstream requirements, and invocation mistakes.
Source:
lexicons/dev/idiolect/correction.json· Rust:idiolect_records::Correction· TS:@idiolect-dev/schema/correction· Fixture:idiolect_records::examples::correction
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
encounter | encounterRef | yes | The encounter whose output was edited. |
path | string (at most 1,024 characters) | yes | JSON Pointer or equivalent into the produced output. |
originalValue | unknown | no | Value prior to correction. May be elided for visibility reasons. |
correctedValue | unknown | no | Value after correction. |
reason | open enum | yes | lens-error / domain-difference / source-error / downstream-idiosyncrasy / user-mistake / retrospective. |
reasonVocab | vocabRef | no | Vocab the reason slug resolves against. |
rationale | string (at most 2,000 graphemes) | no | Human-readable justification. |
holder | did | no | Party the correction is attributed to. |
basis | basis | no | Structured grounding for third-party attribution. |
visibility | visibility | yes | Visibility scope. |
occurredAt | datetime | yes | When the correction was made. |
Field details
reason
Aggregating corrections naively gives the wrong signal: a lens that produces correct output for half its inputs and domain-different output for the other half is not a buggy lens. The reason taxonomy distinguishes:
| Slug | What it means | Implication for the lens |
|---|---|---|
lens-error | The lens produced wrong output for the input. | Bug in the lens. |
domain-difference | The lens output is correct under one set of conventions; the consumer wants a different set. | Not a bug; a different community translation. |
source-error | The source record was wrong; the lens propagated the error faithfully. | Not a bug; upstream issue. |
downstream-idiosyncrasy | The downstream consumer has an unusual requirement the lens does not target. | Not a bug; consumer-specific. |
user-mistake | The user invoked the wrong lens. | Not a bug; routing issue. |
retrospective | A delayed finding caused by something other than the above; usually escalates to a dev.idiolect.retrospection. | Possibly a bug; investigation pending. |
Observers fold corrections by reason. A high lens-error rate
is signal; a high domain-difference rate is a community
disagreement. Conflating them is the failure mode the taxonomy
exists to prevent.
path
JSON Pointer (RFC 6901) into the produced output. A path of
/body/text identifies the text field under body. Multi-part
paths are supported up to 1024 bytes. Consumers replaying
corrections apply the edit at the named path.
originalValue may be elided
When the encounter's visibility restricts publishing source
data, the correction may carry only correctedValue and a
narrative rationale. A consumer that trusts the corrector can use
the corrected value verbatim; one that wants to verify the
correction needs to fetch the source separately.
Holder versus the repo owner
Most corrections are first-party: the consumer that received the
output is the same as the consumer that edited it. Some are
third-party: a reviewer transcribing an off-network correction.
holder plus basis carry the third-party attribution
machinery, identical to encounter and belief.
Example
{
"$type": "dev.idiolect.correction",
"encounter": {
"uri": "at://did:plc:user/dev.idiolect.encounter/3l5",
"cid": "bafyreidfcm4u3vnuph5ltwdpssiz3a4xfbm2otjrdisftwnbfmnxd6lsxm"
},
"path": "/body/text",
"originalValue": "the quick brown foxes",
"correctedValue": "the quick brown fox",
"reason": "lens-error",
"rationale": "Lens incorrectly pluralized the singular noun.",
"visibility": "public-detailed",
"occurredAt": "2026-04-19T00:00:00.000Z"
}
Related records
flowchart LR
ENC[encounter] -->|downstreamResult: corrected| COR[correction]
COR -->|fold by reason| OBS[observation]
OBS -.consumed by.-> CON[consumer]
CON -->|decides| INVOKE[whether to invoke]
A high lens-error count in observations is a signal. A high
domain-difference count is just a record of community
disagreement. Consumers reading observations make routing
decisions on the former, not the latter. Thus, observers must publish
the breakdown by reason, not just a flat correction count.
Concept references
- Concepts: The dev.idiolect.* lexicon family
- Concepts: Observer protocol
- Lexicons: encounter · observation · retrospection
dev.idiolect.defs
This document defines shared types for the dev.idiolect.*
lexicon family. Two kinds of content live here:
- Cross-cutting reference shapes: lens, schema, encounter, vocab, and strong-record references; tool identity; visibility.
- Content-theory types: purpose, lens property, evidence, caveat, basis. Shared across multiple records.
Record-specific combinator trees (condition, eligibility, constraint, convention) live in their respective record lexicons, not here.
Source:
lexicons/dev/idiolect/defs.json· Rust:idiolect_records::generated::defs· TS:@idiolect-dev/schema/defs
Reference shapes
schemaRef
Reference to a schema. Either an at-uri or a content hash. At least one must be present.
| Subfield | Type | Notes |
|---|---|---|
uri | at-uri | AT-URI pointing to a schema record. |
cid | cid-link | Content hash of the schema. |
language | string | Schema-language identifier (atproto-lexicon, postgres-sql, protobuf, graphql, json-schema). |
lensRef
| Subfield | Type | Notes |
|---|---|---|
uri | at-uri | AT-URI of a lens record. |
cid | cid-link | Content hash of the lens. |
direction | enum | unidirectional / bidirectional. |
encounterRef
| Subfield | Type | Notes |
|---|---|---|
uri | at-uri (required) | AT-URI of the encounter. |
cid | cid-link | Optional CID for revision pinning. |
vocabRef
| Subfield | Type | Notes |
|---|---|---|
uri | at-uri | AT-URI of a vocab record. |
cid | cid-link | Content hash pinning a specific vocab revision. |
strongRecordRef
| Subfield | Type | Required | Notes |
|---|---|---|---|
uri | at-uri | yes | AT-URI of the referenced record. |
cid | cid-link | yes | Content hash. |
Parallel to com.atproto.repo.strongRef. Repeated here so the
defs tree is self-contained.
tool
| Subfield | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Canonical tool name (panproto, coq, tlaplus, z3, nextest). |
version | string | yes | Version string. |
commit | string | no | Optional source commit or build identifier. |
visibility
A closed-enum string. Five values:
| Value | Meaning |
|---|---|
public-detailed | Full record body published. |
public-minimal | Record published with elided detail (e.g. omits source instance). |
public-aggregate-only | Record consumed only by aggregators; individual reads suppressed. |
community-scoped | Declares an intended community scope. The current runtime does not enforce access control for this value. |
private | Should not be published at all. |
idiolect does not enforce these today. They are policy hints.
Content-theory types
use
The compound "what was done, on what material, for what end, by which actor" tuple, reused across records whose subject is an action performed, desired, endorsed, or prohibited.
| Subfield | Type | Required | Notes |
|---|---|---|---|
action | string (≤256) | yes | Action identifier, resolved against actionVocabulary. |
material | materialSpec | no | What is being acted on. |
purpose | string (≤256) | no | The end the action serves. |
actor | string (≤256) | no | Who performs or benefits. |
actionVocabulary | vocabRef | no | Vocab the action slug resolves against. |
purposeVocabulary | vocabRef | no | Vocab the purpose slug resolves against. |
materialSpec
| Subfield | Type | Notes |
|---|---|---|
scope | string (≤256) | Community-defined scope (classroom_materials, production_logs, scraped_corpus). |
uri | uri | Optional pointer to a specific dataset. |
lensProperty
A union covering the seven verification kinds. Each verification
record carries one of these as its property field.
| Variant | Used by |
|---|---|
lpRoundtrip | kind: roundtrip-test |
lpGenerator | kind: property-test |
lpTheorem | kind: formal-proof |
lpConformance | kind: conformance-test |
lpChecker | kind: static-check |
lpConvergence | kind: convergence-preserving |
lpCoercionLaw | kind: coercion-law |
lpRoundtrip
| Subfield | Type | Required | Notes |
|---|---|---|---|
domain | string (≤512) | yes | Symbolic description of the input set. |
generator | uri | no | Optional pointer to a generator that enumerates the domain. |
lpGenerator
| Subfield | Type | Required | Notes |
|---|---|---|---|
spec | string (≤2000) | yes | Generator specification (proptest Strategy reference, Hypothesis strategy, QuickCheck Arbitrary). |
runner | string | no | Name of the PBT runtime. |
seed | integer | no | Optional seed for reproducibility. |
lpTheorem
| Subfield | Type | Required | Notes |
|---|---|---|---|
statement | string (≤4000) | yes | The theorem, in the declared system syntax. |
system | string | no | Proof system (coq, lean4, agda, tlaplus, z3). |
freeVariables | array of strings | no | Names of free variables. |
lpConformance
| Subfield | Type | Required | Notes |
|---|---|---|---|
standard | string | yes | Standard identifier (iso-8601, rfc-3339, en-pos-v2.1). |
version | string | yes | Standard version. |
clauses | array of strings | no | Optional subset of the standard's clauses. |
lpChecker
| Subfield | Type | Required | Notes |
|---|---|---|---|
checker | string | yes | Static-checker identifier (panproto-check, clippy, tsc-strict). |
ruleset | string | no | Named ruleset or configuration preset. |
version | string | no | Checker version. |
lpConvergence
| Subfield | Type | Required | Notes |
|---|---|---|---|
property | string (≤1000) | yes | Symbolic name or description of the preserved property. |
boundSteps | integer | no | Optional bound on steps to fixpoint. |
lpCoercionLaw
| Subfield | Type | Required | Notes |
|---|---|---|---|
standard | string (≤256) | yes | Identifier of the coercion-law standard. |
version | string (≤64) | no | Optional version. |
violationThreshold | integer | no | Cap on the violations a runner may report before falsifying. |
evidence
A union of structured witnesses for retrospection findings.
| Variant | Used when finding kind is |
|---|---|
evidenceDivergence | merge-divergence |
evidenceLoss | data-loss |
evidenceMismatch | reconciliation-mismatch |
evidenceDivergence
| Subfield | Type | Required | Notes |
|---|---|---|---|
pathA | array of lensRef | yes | Lenses composed in path A. |
pathB | array of lensRef | yes | Lenses composed in path B. |
witnessInput | cid-link | no | Optional CID where the two paths diverge. |
evidenceLoss
| Subfield | Type | Required | Notes |
|---|---|---|---|
sourceField | string | yes | Dotted path identifying the lost field. |
targetSchema | schemaRef | no | Target schema where the loss was observed. |
witnessInput | cid-link | no | Witness input. |
evidenceMismatch
| Subfield | Type | Required | Notes |
|---|---|---|---|
leftRecord | cid-link | no | Left record. |
rightRecord | cid-link | no | Right record. |
expectedEqualityOn | string | no | Dotted path or projection under which equality was expected. |
caveat
| Subfield | Type | Required | Notes |
|---|---|---|---|
mode | string | yes | Short failure-mode identifier. |
affects | array of strings | no | Dotted paths or field names. |
severity | enum | no | info / warn / error. |
basis
A union of structured grounds for an attitudinal claim.
| Variant | Use when |
|---|---|
basisSelfAsserted | The holder asserts directly. The default when basis is omitted. |
basisCommunityPolicy | Grounded in a community's published policy. Carries community (at-uri) and optional policyUri. |
basisExternalSignal | Grounded in something outside ATProto. Carries url, optional signalType, optional description. |
basisDerivedFromRecord | Grounded in another ATProto record. Carries source (strongRecordRef) and optional inferenceRule. |
basisSelfAsserted has no fields. The variant tag itself is the
content. basisDerivedFromRecord.inferenceRule is the canonical
hook for declaring how this record derives from another
(classifier:purpose-v1, lens:v1-to-v2, aggregation:byte-mean,
...).
Concept references
- Concepts: The dev.idiolect.* lexicon family
- Concepts: Records as content-addressed signed data
- The defs are referenced by every other lexicon in the family.
dev.idiolect.deliberation
A community-scoped record for a question or proposal under collective
consideration. Companion records carry the rest of the process:
deliberationStatement for
participant utterances,
deliberationVote for stances on those
utterances, and
deliberationOutcome for the
observer-aggregated tally.
Deliberations are intentionally process-shaped: they represent the
unsettled moment. They are distinct from
belief (settled doxastic) and
recommendation (settled normative). A
deliberation that closes can point at an outcome record so
consumers can read the conclusion without re-folding the votes.
Source:
lexicons/dev/idiolect/deliberation.json· Rust:idiolect_records::Deliberation· TS:@idiolect-dev/schema/deliberation· Fixture:idiolect_records::examples::deliberation
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
owningCommunity | at-uri | yes | The community whose membership is deliberating. |
topic | string (≤200 graphemes) | yes | Human-readable topic or question. |
description | string (≤1000 graphemes) | no | Extended framing or context. |
authRequired | boolean (default true) | no | Whether participation requires authenticated membership. |
classification | open enum | no | question / proposal / grievance / retrospective. |
classificationVocab | vocabRef | no | Vocab the classification slug resolves against. |
status | open enum | no | open / closed / tabled / adopted / rejected. |
statusVocab | vocabRef | no | Vocab the status slug resolves against. |
closedAt | datetime | no | When the deliberation moved out of an open status. |
outcome | at-uri | no | Pointer to a deliberationOutcome record summarizing the resolved stance. |
createdAt | datetime | yes | Publication timestamp. |
Field details
owningCommunity
The deliberation is scoped to a single community. Membership and participation rights are resolved through that community's record. A deliberation can be cross-referenced from other communities, but exactly one owns it.
idiolect does not enforce membership. authRequired is a
declared policy: when true, only authenticated members'
statements and votes count toward the outcome. When false, the
deliberation accepts drive-by statements (which observers may
weight differently when folding the tally).
classification
| Slug | What it means |
|---|---|
question | An open question without a proposed resolution. |
proposal | A specific proposal under consideration. |
grievance | A complaint or dispute. |
retrospective | A post-hoc review of a prior decision. |
The classification is open-enum: a community publishing its own
classifications vocabulary (negotiation, process-vote,
amendment, ...) extends the slug set. Resolution goes through
classificationVocab when set, otherwise the canonical idiolect
default.
The classification is optional. A community that does not want to commit to a classification omits the field. The deliberation record is still valid, and observers and consumers just have less metadata to fold on.
status lifecycle
| Slug | What it means |
|---|---|
open | Active. Statements and votes accepted. |
closed | No longer accepting input. May or may not have an outcome. |
tabled | Closed but explicitly deferred for later. |
adopted | Closed with a positive resolution. |
rejected | Closed with a negative resolution. |
Open-enum: a community that wants finer-grained statuses
(closed-pending-revision, escalated, ...) extends via
statusVocab. The lifecycle is a declaration. The record
carries the value the publisher set.
outcome
A pointer to a dev.idiolect.deliberationOutcome record. Set
after closure when an outcome record exists. Consumers reading
a closed deliberation can fetch the outcome without re-folding
the entire vote stream.
Multiple outcome records per deliberation are allowed (different
observers, different cut-offs). The deliberation's outcome
field points at the canonical one. Consumers who want a
different observer's tally query the orchestrator directly.
closedAt versus createdAt
createdAt is when the deliberation was opened. closedAt is
when it moved out of an open status. The difference is the
deliberation's duration. Observers fold this for cadence
metrics: how long a community typically deliberates before
adopting, how often deliberations are tabled rather than
adopted.
Example
{
"$type": "dev.idiolect.deliberation",
"owningCommunity": "at://did:plc:community/dev.idiolect.community/canonical",
"topic": "Should we adopt the v2 lens for post translations?",
"description": "Community discussion on whether to make the v2 lens the default for member-published posts.",
"authRequired": true,
"classification": "proposal",
"status": "open",
"createdAt": "2026-04-19T00:00:00.000Z"
}
Process flow
flowchart LR
DEL[deliberation] -->|opens| ST1[statement]
DEL -->|opens| ST2[statement]
DEL -->|opens| ST3[statement]
ST1 -->|voted on by| V1[vote]
ST2 -->|voted on by| V2[vote]
ST3 -->|voted on by| V3[vote]
V1 --> OBS[observer fold]
V2 --> OBS
V3 --> OBS
OBS -->|publishes| OUT[deliberationOutcome]
DEL -->|closes with| OUT
A deliberation is opened. Participants publish statements referencing the deliberation. Other participants publish votes referencing specific statements. An observer folds the vote stream and publishes a tally. The deliberation closes with an outcome pointer.
Concept references
- Concepts: Deliberation
- Concepts: The dev.idiolect.* lexicon family
- Lexicons: deliberationStatement · deliberationVote · deliberationOutcome
dev.idiolect.deliberationStatement
A participant utterance submitted to a
deliberation. Statements are the units
votes attach to. A strong reference
pins the deliberation revision. The deliberation itself is not voted on
directly. Classification is an open-enum slug resolved against a
community vocabulary, so communities that draw the line between
claim and proposal differently can extend or remap without
forking the lexicon.
Source:
lexicons/dev/idiolect/deliberationStatement.json· Rust:idiolect_records::DeliberationStatement· TS:@idiolect-dev/schema/deliberationStatement· Fixture:idiolect_records::examples::deliberation_statement
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
deliberation | strongRecordRef | yes | AT-URI + CID for the deliberation this statement participates in. |
text | string (≤400 graphemes) | yes | Statement text. |
classification | open enum | no | claim / proposal / dissent / clarification / question. |
classificationVocab | vocabRef | no | Vocab the classification slug resolves against. |
anonymous | boolean (default false) | no | Whether the statement was submitted anonymously. |
createdAt | datetime | yes | Publication timestamp. |
Field details
deliberation
deliberation carries both the AT-URI and the CID. Pinning by
CID prevents a later deliberation revision from silently
rescoping the statement. A consumer that reads the statement and
follows the pointer gets the exact deliberation revision the
participant was responding to.
This matters when deliberations are edited mid-process (e.g. the publisher clarifies the topic). Statements published before the edit pin the pre-edit revision. Statements published after pin the post-edit revision. Folds and consumers can distinguish.
text
The statement itself. The 400-grapheme cap is conventional, not
arbitrary: brevity keeps statements voteable. Long-form
context belongs on the deliberation record's description or
in a community-published companion document linked from the
description.
classification
| Slug | What it captures |
|---|---|
claim | An assertion of fact or opinion. |
proposal | A specific proposed action. |
dissent | An objection to a prior statement or to the deliberation framing. |
clarification | A request for or provision of clarification. |
question | An open question requiring an answer. |
Classifications are argumentative roles, not topics. A
community that draws different distinctions (amendment,
process-objection, tangent, ...) extends via
classificationVocab. The classification is optional. A
deliberation that wants to stay agnostic on argumentative roles
omits it.
anonymous
When true, the statement was submitted anonymously. The
typical implementation: the statement is authored on a
designated service DID rather than the participant's personal
repo, so the repo signature does not reveal identity. Consumers
that need provenance match on the repo DID (the service DID),
not on this record's content.
The flag is a declaration: idiolect does not enforce anonymity beyond what the publishing rail provides. A community that wants strong anonymity uses an anonymizing service DID with its own access controls.
Example
{
"$type": "dev.idiolect.deliberationStatement",
"deliberation": {
"uri": "at://did:plc:community/dev.idiolect.deliberation/3l5",
"cid": "bafyreidfcm4u3vnuph5ltwdpssiz3a4xfbm2otjrdisftwnbfmnxd6lsxm"
},
"text": "Adopting the v2 lens would lose dialect markers on legacy posts.",
"classification": "dissent",
"anonymous": false,
"createdAt": "2026-04-19T00:00:00.000Z"
}
Concept references
dev.idiolect.deliberationVote
A stance taken on a deliberationStatement,
pinned by a strong reference.
Stance is an open-enum slug resolved against a community-published
vote-stance vocabulary. The Acorn-style three-way default
(agree / pass / disagree) is canonical. Richer vocabularies
(conditional-agree, abstain-with-reason, ranked preference) are
expressible by referencing a different vocab. Optional weight
and rationale carry additional signal that observers can fold.
Consumers that don't need them ignore them.
Source:
lexicons/dev/idiolect/deliberationVote.json· Rust:idiolect_records::DeliberationVote· TS:@idiolect-dev/schema/deliberationVote· Fixture:idiolect_records::examples::deliberation_vote
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
subject | strongRecordRef | yes | AT-URI + CID for the statement being voted on. |
stance | open enum | yes | agree / pass / disagree. |
stanceVocab | vocabRef | no | Vocab the stance slug resolves against. |
weight | integer ∈ [0, 1000] | no | Optional ranking signal. Convention: scaled by 1000 for the 0.0–1.0 range. |
rationale | string (≤500 graphemes) | no | Optional narrative reason. |
createdAt | datetime | yes | Publication timestamp. |
Field details
subject
The subject field carries both AT-URI and CID. A statement
edited after the vote was cast does not retroactively change what
was voted on. Observers folding the tally read the CID to confirm
they are aggregating votes against the same statement revision.
If a statement is edited and a participant wants to vote on the
new revision, that is a separate vote record with a different
subject CID. idiolect does not collapse votes across
revisions. Observers do, when their fold method specifies it.
stance
The default vocabulary seeds three slugs:
| Slug | Meaning |
|---|---|
agree | Affirms the statement. |
pass | Abstains. |
disagree | Rejects the statement. |
These match Acorn's +1 / 0 / -1 convention. Communities that
want richer stances publish their own stanceVocab. Common
extensions:
conditional-agree— agree under specified conditions.abstain-with-reason— explicit non-vote with a rationale.rank-1,rank-2, ... — ranked preference.
The stanceVocab machinery means consumers do not have to
coordinate on which vocab is in use ahead of time. The vote
record either references an explicit stanceVocab or falls back
to the canonical idiolect default.
weight
The optional weight is a ranking signal. Its [0, 1000] integer range encodes
the 0.0–1.0 floating-point range with three decimal places of
precision. Convention follows pub.chive.graph.edge#weight.
Consumers that aggregate votes uniformly ignore weight. Ranked
or weighted aggregations consume it. A community that wants
quadratic voting publishes a vote-weights companion vocabulary
and uses weight to encode the scheme. Observers running a
quadratic-vote fold read both the stance and the weight.
rationale
The optional rationale is narrative. Tally folds do not consume it.
Consumer surfaces (e.g. a deliberation viewer) display it
alongside the vote. The 500-grapheme cap matches the
deliberation-statement length: brevity is conventional.
Anonymous votes
There is no anonymous flag on votes (unlike statements). If a
community wants anonymous voting, the implementation is the same
as anonymous statements: votes are authored on a designated
service DID rather than the voter's personal repo. The repo
signature is the authoritative provenance signal.
Example
{
"$type": "dev.idiolect.deliberationVote",
"subject": {
"uri": "at://did:plc:community/dev.idiolect.deliberationStatement/3l5",
"cid": "bafyreidfcm4u3vnuph5ltwdpssiz3a4xfbm2otjrdisftwnbfmnxd6lsxm"
},
"stance": "agree",
"weight": 750,
"rationale": "Strong agree, conditional on the dialect-marker preservation work shipping first.",
"createdAt": "2026-04-19T00:00:00.000Z"
}
Related records
A vote does not produce an outcome on its own. An observer reads
the vote stream for a deliberation, folds by (statement, stance)
(plus optional weight aggregation), and publishes a
deliberationOutcome record. Multiple
observers may publish concurrent outcomes. Consumers that want
consensus require quorum across trusted observers.
Concept references
- Concepts: Deliberation
- Concepts: Observer protocol
- Lexicons: deliberation · deliberationStatement · deliberationOutcome
dev.idiolect.deliberationOutcome
dev.idiolect.deliberationOutcome is an observer-aggregated tally for a
deliberation, pinned through a
strong reference.
An observer, rather than a participant, produces this record by folding the
vote stream and publishing the result from the observer's repository.
Consumers reading a closed deliberation can fetch the outcome
directly rather than re-folding every vote. Tallies are
per-statement and per-stance, so consumers can render a
Polis-style opinion map without further computation.
Source:
lexicons/dev/idiolect/deliberationOutcome.json· Rust:idiolect_records::DeliberationOutcome· TS:@idiolect-dev/schema/deliberationOutcome· Fixture:idiolect_records::examples::deliberation_outcome
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
deliberation | strongRecordRef | yes | AT-URI + CID for the deliberation. |
statementTallies | array (≤4096) of statementTally | yes | Per-statement vote counts. |
adopted | array (≤256) of strongRecordRef | no | Statements the community adopted. |
stanceVocab | vocabRef | no | Vocab the per-tally stance slugs resolve against. |
computedAt | datetime | yes | When the observer computed this tally. |
tool | tool | no | Identity and version of the aggregator. |
occurredAt | datetime | yes | Publication timestamp. |
statementTally
| Subfield | Type | Required | Notes |
|---|---|---|---|
statement | strongRecordRef | yes | The statement these counts aggregate. |
counts | array (≤64) of stanceCount | yes | Per-stance vote counts. |
weightedCounts | array (≤64) of stanceCount | no | Per-stance weighted vote counts (when votes carried weight). Scaled by 1000. |
stanceCount
| Subfield | Type | Required | Notes |
|---|---|---|---|
stance | string (≤256) | yes | Stance slug, resolved through the outcome's stanceVocab. |
count | non-negative integer | yes | Vote count. For weightedCounts, scaled by 1000. |
Field details
Publisher
The deliberation owns the topic. Participants own the statements and votes. The aggregate is opinion: it depends on the observer's fold method, the cut-off time, and which encounter kinds it weights. Two observers can produce different outcomes for the same deliberation.
Outcomes are signed observer records, so consumers can identify their publishers and compare compatible folds. A consumer that distrusts one observer's fold can:
- Fetch all outcomes for the deliberation.
- Pick one based on the observer's identity or the
toolfield. - Require quorum among trusted observers.
- Re-fold the vote stream itself.
stanceVocab
The outcome record uses one stance vocabulary across all tallies. An observer that sees votes referencing different vocabularies must either:
- Publish separate outcomes per vocab, each tallying votes that share a vocab.
- First translate via a
mapEnumlens (see Open enums into a single target vocabulary, then tally.
Mixing vocabularies in a single outcome is invalid: the same slug in two different vocabularies has different semantics, and adding their counts is meaningless.
statementTallies
The array contains one entry per statement that received at least one vote. Statements with zero votes are omitted. Each tally carries:
- The statement (strong-ref, so consumers fetching the tally can fetch the exact statement revision being tallied).
- The per-stance counts.
- Optional weighted counts when the underlying votes carried
weight.
The 4096-entry cap matches the maximum statement count per deliberation in practice. Communities expecting more should publish multiple outcome records partitioned by statement window.
adopted
A list of strong-refs to statements the community adopted as the deliberation's resolution. An adopted statement is one the community treats as the answer to a question, the resolution of a proposal, or the action item from a grievance.
Adoption is a community decision, not a fold rule. The observer publishing the outcome typically follows the deliberation's publishing community: their criterion for adoption (majority agree, supermajority, consensus) is what the observer encodes in this list. A different observer running a different criterion would publish a different outcome.
adopted is empty when the deliberation closed without
adoption (rejected, tabled, or closed without resolution).
tool and method versioning
The tool field carries the aggregator's identity and version.
Different tools or versions may implement different algorithms. Consumers
should compare their outcomes only when the method semantics align; the tool
field identifies the implementation used.
Example
{
"$type": "dev.idiolect.deliberationOutcome",
"deliberation": {
"uri": "at://did:plc:community/dev.idiolect.deliberation/3l5",
"cid": "bafyreidfcm4u3vnuph5ltwdpssiz3a4xfbm2otjrdisftwnbfmnxd6lsxm"
},
"statementTallies": [
{
"statement": {
"uri": "at://did:plc:community/dev.idiolect.deliberationStatement/stmt1",
"cid": "bafyreidfcm4u3vnuph5ltwdpssiz3a4xfbm2otjrdisftwnbfmnxd6lsxm"
},
"counts": [
{ "stance": "agree", "count": 42 },
{ "stance": "pass", "count": 7 },
{ "stance": "disagree", "count": 3 }
]
},
{
"statement": {
"uri": "at://did:plc:community/dev.idiolect.deliberationStatement/stmt2",
"cid": "bafyreidfcm4u3vnuph5ltwdpssiz3a4xfbm2otjrdisftwnbfmnxd6lsxm"
},
"counts": [
{ "stance": "agree", "count": 18 },
{ "stance": "pass", "count": 12 },
{ "stance": "disagree", "count": 22 }
]
}
],
"adopted": [
{
"uri": "at://did:plc:community/dev.idiolect.deliberationStatement/stmt1",
"cid": "bafyreidfcm4u3vnuph5ltwdpssiz3a4xfbm2otjrdisftwnbfmnxd6lsxm"
}
],
"computedAt": "2026-04-30T00:00:00.000Z",
"tool": {
"name": "deliberation-tally",
"version": "1.0.0"
},
"occurredAt": "2026-04-30T00:01:00.000Z"
}
Concept references
- Concepts: Deliberation
- Concepts: Observer protocol
- Lexicons: deliberation · deliberationVote · observation
dev.idiolect.dialect
A community's dialect: a bundle of idiolect references and preferred translations. Dialects are declared, not imposed: downstream consumers may adopt, adapt, or ignore them.
Source:
lexicons/dev/idiolect/dialect.json· Rust:idiolect_records::Dialect· TS:@idiolect-dev/schema/dialect· Fixture:idiolect_records::examples::dialect
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
owningCommunity | at-uri | yes | The community that owns this dialect. |
name | string (≤128) | yes | Human-readable dialect name. |
description | string (≤4000 graphemes) | no | Purpose and scope. |
idiolects | array of schemaRef | no | Schemas that constitute the dialect's idiolect set. |
preferredLenses | array of lensRef | no | Translations the community prefers. |
deprecations | array of Deprecation | no | Deprecated entries with replacement pointers. |
version | string | no | Dialect version (semver when applicable). |
previousVersion | at-uri | no | Predecessor revision in a version chain. |
createdAt | datetime | yes | Publication timestamp. |
Deprecation
| Subfield | Type | Required | Notes |
|---|---|---|---|
ref | at-uri | yes | The deprecated idiolect or lens. |
replacement | at-uri | no | Optional successor. |
deprecatedAt | datetime | yes | When the deprecation took effect. |
reason | string (≤1000 graphemes) | yes | Why it was deprecated. |
Field details
Contract semantics
A dialect is a bundle. It does not introduce new lexicons; it
collects existing ones into a coherent set the community treats
as canonical. A consumer that adopts the dialect routes
translations through preferredLenses, validates incoming records
against the schemas in idiolects, and treats the deprecation
list as a redirect table.
The dialect record is data, not configuration. Adding an entry is
a record edit. Deprecating one is another record edit on the same
dialect with a Deprecation entry. Two dialects from different
communities can list the same NSID with different preferred
lenses. Consumers pick a dialect (or a quorum of dialects) and
follow it.
previousVersion and the version chain
A dialect revision points at its predecessor via previousVersion.
A consumer reading the head dialect can walk the chain back
through prior versions, confirm that deprecations were announced
at the right time, and audit the change history without trusting
the orchestrator's catalog.
The chain is not enforced: a community can publish a dialect with
no previousVersion (a fresh start) or skip versions (publishing
v3 with previousVersion = v1). The record captures what was
done. Consumers decide whether to trust it.
deprecations
Each entry records an idiolect or lens that was once part of the
dialect and is now superseded. The ref field points at the
deprecated artifact. replacement optionally points at the
successor. Consumers reading a record at the deprecated ref can
follow replacement to the new one, with the reason field
explaining why.
The intended lexicon-evolution policy calls for a deprecation entry when a
non-Iso lens revision ships; the current automation does not enforce this
rule reliably. See
Lexicon evolution policy.
Example
{
"$type": "dev.idiolect.dialect",
"owningCommunity": "at://did:plc:community/dev.idiolect.community/canonical",
"name": "tutorial canonical",
"description": "The canonical dialect for the tutorial community.",
"idiolects": [
{ "uri": "at://did:plc:community/dev.panproto.schema.schema/post-v1" }
],
"preferredLenses": [
{ "uri": "at://did:plc:community/dev.panproto.schema.lens/post-v1-to-v2" }
],
"deprecations": [
{
"ref": "at://did:plc:community/dev.panproto.schema.schema/post-v0",
"replacement": "at://did:plc:community/dev.panproto.schema.schema/post-v1",
"deprecatedAt": "2026-04-01T00:00:00.000Z",
"reason": "Replaced by v1 with structured `body` field; lens preserves all v0 records."
}
],
"version": "1.2.0",
"previousVersion": "at://did:plc:community/dev.idiolect.dialect/1.1.0",
"createdAt": "2026-04-19T00:00:00.000Z"
}
Multiple dialects
Two communities can publish disjoint, overlapping, or contradictory dialects. The protocol treats them all as opinions and prefers none. Consumers pick a resolution policy:
first-match— pick the first dialect listed in the consumer's config.quorum— accept a translation when of trusted dialects endorse the same lens path.merge— union the entries; on collision, fall back to a configured tie-breaker.
See Bundle records into a dialect.
Concept references
- Concepts: Idiolect, dialect, language
- Concepts: Lexicon evolution policy
- Guides: Bundle records into a dialect
- Lexicons: community · vocab
dev.idiolect.encounter
A signed record of a single lens invocation. Encounters record that a
translation occurred, with enough context for aggregators (observations) and
correctors to reason about it. Narrative commentary lives in
annotations. The structured payload in use covers the
action / material / purpose / actor of the invocation.
Source:
lexicons/dev/idiolect/encounter.json· Rust:idiolect_records::Encounter· TS:@idiolect-dev/schema/encounter· Fixture:idiolect_records::examples::encounter
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
lens | lensRef | yes | The lens that was invoked. |
sourceSchema | schemaRef | yes | Source schema the lens translated from. |
targetSchema | schemaRef | no | Target schema produced by the lens. Often implied by the lens; elided when unambiguous. |
sourceInstance | cid-link | no | Content-addressed reference to the source instance. Omit when visibility restricts publishing source data. |
producedOutput | cid-link | no | Content-addressed reference to the produced output. |
use | use | yes | Structured action / material / purpose / actor. |
downstreamResult | open enum | no | success / corrected / rejected / unknown. |
downstreamResultVocab | vocabRef | no | Vocab the slug resolves against. |
annotations | string (≤4000 graphemes) | no | Narrative commentary. |
holder | did | no | Party the encounter is attributed to. Omit for first-party records. |
basis | basis | no | Structured grounding when holder differs from the repo owner. |
kind | open enum | yes | Corpus-kind slug (invocation-log, curated, roundtrip-verified, production, adversarial). |
kindVocab | vocabRef | no | Vocab the kind slug resolves against. |
visibility | visibility | yes | public-detailed / public-minimal / public-aggregate-only / community-scoped / private. |
occurredAt | datetime | yes | When the invocation happened. Distinct from the record's createdAt. |
Field details
use
The structured payload. A use carries:
action(open-enum slug, resolved againstactionVocabulary).material(amaterialSpec: scope plus optional corpus pointer).purpose(open-enum slug, resolved againstpurposeVocabulary).actor(string; who ultimately performs or benefits).
All four together form the "what was done, on what, for what
end, by which actor" tuple. Consumers that match on actions
match on subsumption against the referenced vocabulary, not on
substring equality. Two communities that disagree on whether
train_model subsumes fine_tune produce different routing
decisions from the same encounter, as expected when their vocabularies encode
different subsumption relations.
kind
The encounter-kind slug declares what the corpus represents:
| Slug | Meaning |
|---|---|
invocation-log | A real production invocation. |
curated | A hand-picked sample, often used for evaluation. |
roundtrip-verified | An invocation where put(get(a)) == a was verified at write time. |
production | Synonym for invocation-log in some pipelines; exists for distinct trust weighting. |
adversarial | An invocation explicitly chosen to stress the lens. |
Observers declare in their method which kinds they weight and
how. An observation aggregating invocation-log and curated encounters has a
different input distribution from one aggregating only adversarial
encounters. The kind and the observer's method together determine how the
observation should be read.
downstreamResult
The invoking party's at-record-time assessment of the outcome:
| Slug | Meaning |
|---|---|
success | The output was accepted unchanged. |
corrected | The output was edited; a dev.idiolect.correction record exists or is expected. |
rejected | The output was unusable. |
unknown | The party publishing did not know yet. |
corrected links the encounter to the correction record that documents the
edit.
Consumers reading correction records traverse back through the
encounter's downstreamResult to confirm the link.
holder and basis
Most encounters are first-party: the repo owner is the party that
invoked the lens. Some are third-party: a labeler records that
another party invoked a lens. holder identifies the party the
record is attributed to. basis carries structured grounds for
the attribution (a community policy, an external signal, an
inference from another record). See defs#basis for
the variants.
visibility
The five values are policy hints, not access control. The current
runtime does not enforce them. community-scoped declares that
consumers should not serve a record outside the named community;
private declares that the record should remain local.
Example
{
"$type": "dev.idiolect.encounter",
"lens": { "uri": "at://did:plc:lens-author/dev.panproto.schema.lens/3l5" },
"sourceSchema": { "uri": "at://did:plc:schema-author/dev.panproto.schema.schema/v1" },
"targetSchema": { "uri": "at://did:plc:schema-author/dev.panproto.schema.schema/v2" },
"use": {
"action": "train_model",
"material": { "scope": "production_logs" },
"purpose": "non_commercial",
"actor": "researchers"
},
"downstreamResult": "success",
"kind": "invocation-log",
"visibility": "public-detailed",
"occurredAt": "2026-04-19T12:30:00.000Z"
}
Related records
flowchart LR
PUB[publisher] -->|writes| ENC[encounter]
ENC -->|firehose| OBS[observer]
OBS -->|tally| OBSREC[observation]
ENC -.referenced by.-> COR[correction]
ENC -.referenced by.-> RET[retrospection]
OBSREC -.cited by.-> BEL[belief]
An encounter is the base record. Observations and corrections
reference it. A dev.idiolect.belief may cite either the encounter
directly (for narrow claims) or an observation that aggregated
it (for broad claims). A dev.idiolect.retrospection references
an encounter to record a delayed finding about it.
Cross-references
- Concepts: The dev.idiolect.* lexicon family
- Concepts: Records as content-addressed signed data
- Guides: Index a firehose
- Lexicons: observation · correction · retrospection
dev.idiolect.observation
A signed aggregate over a record family. Observations decouple ranking from the orchestrator: many observers publish competing aggregates over the same traces, and consumers choose whom to trust.
Source:
lexicons/dev/idiolect/observation.json· Rust:idiolect_records::Observation· TS:@idiolect-dev/schema/observation· Fixture:idiolect_records::examples::observation
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
observer | did | yes | DID of the observer publishing this aggregate. |
method | object | yes | { name, description?, codeRef?, parameters? }. The aggregator identity and configuration. |
scope | object | yes | The set of records the observation aggregates over. |
output | unknown | yes | Method-defined payload (counts, scores, diagnostic summaries). |
version | string | yes | Method version. Different versions may produce non-comparable outputs. |
basis | basis | no | Grounding when the observer is not the repo owner. |
visibility | visibility | yes | Visibility scope. |
occurredAt | datetime | yes | When the observation was published. |
method
| Subfield | Type | Required | Notes |
|---|---|---|---|
name | string (≤128) | yes | Short method identifier. |
description | string (≤4000 graphemes) | no | Narrative method description. |
codeRef | at-uri | no | Reference to the method's source or specification. |
parameters | unknown | no | Free-form JSON, observer-defined. |
scope
| Subfield | Type | Required | Notes |
|---|---|---|---|
lenses | array of lensRef | no | Lenses included; empty or omitted means "all". |
communities | array of at-uri | no | Communities whose records are in scope. |
encounterKinds | array of open-enum slugs | no | Encounter kinds weighted in the aggregation. |
encounterKindsVocab | vocabRef | no | Vocab the kind slugs resolve against. |
window | { from?, until? } | no | Time window. |
Field details
output
Deliberately untyped (unknown). The shape is determined by
method. Common shapes:
- A correction-rate ranking:
[{ lens, rate, sampleCount }]. - A quality score:
{ score, ci_low, ci_high }. - A structured diagnostic summary:
{ failureModes: [...], hotspots: [...] }.
A per-statement deliberation tally lives in a separate record
kind, deliberationOutcome, rather
than as an observation output.
A consumer reading an observation must know the method to
interpret the output. The method.name, version, and optional
codeRef together tell a consumer how to interpret the output.
scope.encounterKinds
The observer must disclose which encounter kinds it includes or
the observation is uninterpretable. An observation that weights adversarial
and invocation-log encounters equally has a different input distribution from
one that excludes adversarial samples. Consumers reading the observation rely
on this disclosure to decide whether the result fits their use case.
version versus occurredAt
version is the method's version. Different method versions may not be
comparable because the algorithm or parameter semantics may have changed.
occurredAt is when the observation was published. Observations with the same
method and version are comparable as time-series data only when their scopes
and parameters also align.
basis
Most observations are first-party (the repo owner is the
observer). When the observer is a third party (a relay, a cache,
another aggregator that took someone else's output and republished
it), basis records the grounds: typically derivedFromRecord
pointing at the original observation, with inferenceRule set to
the relay or transformation kind.
Example
{
"$type": "dev.idiolect.observation",
"observer": "did:plc:observer.dev",
"method": {
"name": "encounter-throughput",
"version": "1.0.0",
"parameters": { "windowSeconds": 3600 }
},
"scope": {
"lenses": [
{ "uri": "at://did:plc:lens-author/dev.panproto.schema.lens/3l5" }
],
"encounterKinds": ["invocation-log", "production"],
"window": {
"from": "2026-04-19T00:00:00.000Z",
"until": "2026-04-19T01:00:00.000Z"
}
},
"output": {
"total": 1042,
"byKind": {
"invocation-log": 940,
"production": 102
},
"byDownstreamResult": {
"success": 991,
"corrected": 37,
"rejected": 12,
"unknown": 2
}
},
"version": "1.0.0",
"visibility": "public-detailed",
"occurredAt": "2026-04-19T01:00:05.000Z"
}
Trust semantics
A mutable metrics endpoint does not by itself preserve a signed, replayable snapshot. A signed observation records the publisher and can be re-folded when the input history and method are available. Observers may produce comparable counts when method version, scope, and input coverage align. Consumers can require quorum among trusted observers before treating an observation as authoritative.
Concept references
- Concepts: Observer protocol
- Concepts: The dev.idiolect.* lexicon family
- Guides: Run the observer daemon
- Lexicons: encounter · deliberationOutcome
dev.idiolect.recommendation
A community-published lens path with structured applicability
conditions and optional verification requirements. The conditions,
preconditions, and caveats arrays are structured: a consumer
can evaluate them mechanically against an invocation context. The
requiredVerifications array is a list of specific lens properties
the recommendation assumes are in place, so consumers check which
roundtrip domain or theorem or standard has been established, not
just which verification kind was run. Narrative prose lives in
annotations and caveatsText.
Source:
lexicons/dev/idiolect/recommendation.json· Rust:idiolect_records::Recommendation· TS:@idiolect-dev/schema/recommendation· Fixture:idiolect_records::examples::recommendation
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
issuingCommunity | at-uri | yes | Community publishing the recommendation. |
conditions | array (≤128) of Condition | yes | Structured applicability predicate. Empty array means "always applies". |
preconditions | array (≤128) of Condition | no | Additional structured assumptions the consumer must verify. |
lensPath | array (≥1) of lensRef | yes | Ordered sequence of lenses to compose. |
annotations | string (≤8000 graphemes) | no | Narrative explanation. |
requiredVerifications | array of lensProperty | no | Specific properties the recommendation assumes. |
caveats | array (≤32) of Caveat | no | Structured failure-mode list. |
caveatsText | string | no | Narrative companion to caveats. |
basis | basis | no | Structured grounding for the attitudinal claim. |
supersedes | at-uri | no | Prior recommendation this one replaces. |
occurredAt | datetime | yes | Publication timestamp. |
The condition tree
conditions and preconditions are postfix-operator trees over
the combinator set defined inline in this lexicon:
| Variant | Arity | Purpose |
|---|---|---|
conditionSourceIs | atomic | Match invocations whose source schema equals the named at-uri. |
conditionTargetIs | atomic | Match invocations whose target schema equals the named at-uri. |
conditionActionSubsumedBy | atomic | Match invocations whose use.action is subsumed by the named slug in the named action vocabulary. |
conditionPurposeSubsumedBy | atomic | Match invocations whose use.purpose is subsumed by the named slug. |
conditionDataHas | atomic | Match invocations whose data carries the named community-defined property identifier (e.g. length>1024, contains-pii, multilingual). |
conditionAnd | combinator | Pop the top two predicates and conjoin them. |
conditionOr | combinator | Pop the top two predicates and disjoin them. |
conditionNot | combinator | Pop the top predicate and negate it. |
Postfix evaluation: walk the array left to right, push atomic predicates onto a stack, pop operands when a combinator is encountered, push the result. The final stack must contain exactly one truth value, which is the predicate's result.
This shape is closer to a Reverse Polish expression than to a
nested object tree. The wire representation is flat (an array of
discriminated objects), which matches ATProto's union shape and
keeps the validator simple.
Field details
lensPath
The recommendation endorses a path of lenses, not just a single
lens. A lensPath of length 1 is the single-lens case. Longer
paths are a community's recommendation for a chained translation
(e.g. v1 → middle-form → v3 instead of a direct v1 → v3 lens
when the direct lens has worse properties).
A consumer that adopts the recommendation calls apply_lens on
each step in order, threading the complement of each step into
the next. See Concepts: Lens semantics.
requiredVerifications
A list of specific lens properties (lensProperty from
defs). Each entry specifies what the recommendation
assumes is in place: a particular roundtrip domain, a specific
formal theorem, a conformance to a specific standard.
A consumer verifies the recommendation by:
- Querying the verifier registry for verification records on each lens in the path.
- Confirming that each
requiredVerificationis covered by an accepted record (signed by a trusted verifier, withresult: "holds"). - Adopting the recommendation only when all required verifications check out.
The required-verifications list pins exactly what the community is relying on, so a recommendation is auditable rather than a bare opinion.
caveats
A structured failure-mode list. Each caveat has:
| Subfield | Type | Notes |
|---|---|---|
mode | string | Short failure-mode identifier (e.g. loses-dialect-markers). |
affects | array of strings | Dotted paths or field names the caveat applies to. |
severity | enum | info / warn / error. |
Consumers match on mode and affects to decide whether the
caveat applies to their use case. severity is advisory; an
error caveat is the community's notice that the recommendation
should not be adopted in cases the caveat covers. A consumer that ignores it
accepts the failure mode named by the caveat.
Example
{
"$type": "dev.idiolect.recommendation",
"issuingCommunity": "at://did:plc:community/dev.idiolect.community/canonical",
"conditions": [
{ "$type": "dev.idiolect.recommendation#conditionSourceIs",
"schema": { "uri": "at://did:plc:schema-author/dev.panproto.schema.schema/v1" } },
{ "$type": "dev.idiolect.recommendation#conditionPurposeSubsumedBy",
"purpose": "non_commercial",
"vocabulary": { "uri": "at://did:plc:example/dev.idiolect.vocab/purposes" } },
{ "$type": "dev.idiolect.recommendation#conditionAnd" }
],
"lensPath": [
{ "uri": "at://did:plc:lens-author/dev.panproto.schema.lens/3l5" }
],
"requiredVerifications": [
{ "$type": "dev.idiolect.defs#lpRoundtrip",
"domain": "all valid v1 records with bodies at most 1024 bytes" }
],
"caveats": [
{ "mode": "loses-dialect-markers",
"affects": ["body.dialect"],
"severity": "warn" }
],
"occurredAt": "2026-04-19T00:00:00.000Z"
}
Related records
flowchart LR
R[recommendation] -->|conditions match| ROUTE[invocation context]
R -->|lensPath| L[lens chain]
R -->|requiredVerifications| V[verification records]
V -->|holds + signed by trusted verifier| ACCEPT[accept]
V -->|falsified or missing| REJECT[reject]
L -.applied via.-> APPLY[apply_lens chain]
A consumer queries the orchestrator's recommendation endpoint, filters by community, evaluates each recommendation's conditions against its invocation context, audits the required verifications, and applies the lens path of the surviving recommendation.
Concept references
- Concepts: The dev.idiolect.* lexicon family
- Concepts: Lens semantics and laws
- Tutorial: Publish a recommendation
- Lexicons: verification · defs
dev.idiolect.retrospection
A signed record that annotates a prior encounter with a delayed finding. Retrospections address silent-error latency: merges, migrations, and bitemporal reconciliations often surface failures only after long delay.
Source:
lexicons/dev/idiolect/retrospection.json· Rust:idiolect_records::Retrospection· TS:@idiolect-dev/schema/retrospection· Fixture:idiolect_records::examples::retrospection
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
encounter | encounterRef | yes | The encounter being retrospected. |
finding | object | yes | { kind, kindVocab?, detail, evidence? }. |
latency | integer (seconds) | no | detectedAt - encounter.occurredAt. Precomputed for aggregation. |
detectingParty | did | yes | DID of the party that detected the issue. |
confidence | number ∈ [0, 1] | no | Optional confidence score. |
disputedAttribution | boolean | no | The detecting party's hint that the causal claim may be contested. |
basis | basis | no | Structured grounding. |
detectedAt | datetime | yes | When the issue was detected. |
occurredAt | datetime | yes | When this retrospection record was published. |
finding
| Subfield | Type | Required | Notes |
|---|---|---|---|
kind | open enum | yes | merge-divergence / data-loss / reconciliation-mismatch / other. |
kindVocab | vocabRef | no | Vocab the kind slug resolves against. |
detail | string (≤8000 graphemes) | yes | Narrative detail. |
evidence | union of evidenceDivergence / evidenceLoss / evidenceMismatch | no | Structured witness for the finding. |
Finding timing
Encounters are at-record-time. Corrections are short-loop. A retrospection covers the case where the finding surfaces after the original encounter has been folded into observations and moved out of the working set:
- A merge that looked correct at write time turns out to have silently dropped a field three months later.
- A migration that round-tripped on the test corpus loses information on a long-tail input that nobody sampled.
- A bitemporal reconciliation surfaces a divergence between two paths that should have converged.
The encounter / correction / observation triple cannot capture these without distorting their semantics. A retrospection record handles this: it points at the original encounter, records a delayed finding, and carries the evidence.
The four finding kinds
| Slug | Evidence shape | What it captures |
|---|---|---|
merge-divergence | evidenceDivergence (paths A and B + witness input) | Two translation paths that should have converged but produced different outputs. |
data-loss | evidenceLoss (source field path + target schema + witness input) | A source-schema field unrepresented in the target after translation. |
reconciliation-mismatch | evidenceMismatch (left and right records + expected equality projection) | Two records that should reconcile under the lens but do not. |
other | (optional) | Catch-all; evidence may be omitted, detail carries the whole finding. |
The structured evidence variants let downstream consumers match
on the failure mode without parsing the narrative detail. An
aggregator that wants to surface "lenses with high data-loss
counts" filters by finding.kind = data-loss and folds.
Field details
latency
Precomputed for aggregation convenience. The value is
detectedAt - encounter.occurredAt in seconds. Folds that bucket
findings by latency (e.g. "what's the median time-to-detect for
merge-divergence findings?") read this field directly. Authors
may omit it for kind: other findings where latency is not
defined.
confidence
The optional value lies in [0, 1]. A finding the detecting party is sure of
omits it, while a finding hedged on uncertain evidence sets a value below 1.
Aggregators may weight findings by confidence. Consumers treating findings as
ground truth filter for high confidence.
disputedAttribution
A hint the detecting party expects the causal attribution to be
contested. idiolect does not enforce contestation. A
contesting party publishes its own retrospection with
disagreement, or a dev.idiolect.belief over the finding. The
flag exists so consumers can flag the finding as
"interpretation pending" rather than treating it as settled.
detectingParty versus the repo signer
detectingParty is the party who actually detected the issue.
Most retrospections are first-party: the repo owner is the
detecting party. Some are third-party: a researcher republishing
a finding from a trusted source. holder is not a field here
(unlike encounter / belief / correction). detectingParty plus
the repo signer carry the relevant attribution.
Example
{
"$type": "dev.idiolect.retrospection",
"encounter": {
"uri": "at://did:plc:user/dev.idiolect.encounter/3l5"
},
"finding": {
"kind": "data-loss",
"detail": "Lens dropped the `provenance` array on records with more than 100 entries; not detected at write time because all sampled inputs had at most 50 entries.",
"evidence": {
"$type": "dev.idiolect.defs#evidenceLoss",
"sourceField": "provenance",
"targetSchema": {
"uri": "at://did:plc:schema-author/dev.panproto.schema.schema/v2"
}
}
},
"latency": 7776000,
"detectingParty": "did:plc:detector",
"confidence": 0.95,
"detectedAt": "2026-07-19T00:00:00.000Z",
"occurredAt": "2026-07-19T00:01:00.000Z"
}
Concept references
dev.idiolect.verification
A signed verification of a formal property of a lens. Verifications
record formal claims alongside operational evidence from encounters,
corrections, and observations; neither evidence source gates the other.
property is a structured lensProperty (see
defs) so consumers dispatch on the specific claim:
a Theorem for proof checkers, a GeneratorSpec for PBT runners,
a ConformanceStandard for conformance runners, and so on.
Source:
lexicons/dev/idiolect/verification.json· Rust:idiolect_records::Verification· TS:@idiolect-dev/schema/verification· Fixture:idiolect_records::examples::verification
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
lens | lensRef | yes | The lens whose property is being asserted. |
kind | open enum | yes | roundtrip-test / property-test / formal-proof / conformance-test / static-check / convergence-preserving / coercion-law. |
kindVocab | vocabRef | no | Vocab the kind slug resolves against. |
verifier | did | yes | DID of the party asserting the verification. |
tool | tool | yes | Tool identity and version. |
property | union of seven lensProperty shapes | yes | Structured statement of what is being asserted. |
result | open enum | yes | holds / falsified / inconclusive. |
resultVocab | vocabRef | no | Vocab the result slug resolves against. |
counterexample | cid-link | no | For result: falsified: minimal counterexample. |
dependencies | array of at-uri | no | Other verifications this one depends on (e.g. a proof assuming a lemma). |
proofArtifact | cid-link | no | For kind: formal-proof: checkable proof artifact (Coq / Lean / Agda term). |
basis | basis | no | Structured grounding when relevant. |
occurredAt | datetime | yes | When the verification was recorded. |
The seven kinds
Each kind has its own property shape, defined in
defs#lensProperty. The kind plus the property
together pin exactly what was verified.
| Kind | Property shape | What the runner does |
|---|---|---|
roundtrip-test | lpRoundtrip (domain string + optional generator URI) | Run put(get(a)) == a on samples drawn from the domain. |
property-test | lpGenerator (spec + runner identifier + seed) | Run an arbitrary boolean predicate over generator samples. |
formal-proof | lpTheorem (statement in proof-system syntax + system + free variables) | Check the proof artifact in the named system. |
conformance-test | lpConformance (standard identifier + version + clause subset) | Run the standard's conformance suite. |
static-check | lpChecker (checker identifier + ruleset + version) | Run the checker against the lens chain. |
convergence-preserving | lpConvergence (property + optional step bound) | Verify the property is preserved under repeated application (fixpoint, reconciliation). |
coercion-law | lpCoercionLaw (standard + version + violation threshold) | Check panproto's coercion-law gate over samples. |
A consumer reading a verification record dispatches on kind,
matches against the embedded property, and decides whether the
specific verification meets its needs. A roundtrip-test verification covering
domain: "all valid v1 records with bodies at most 1024 bytes" tests a
different input set from one covering domain: "the training corpus"; both
are valid, and neither subsumes the other.
Field details
result
| Slug | Meaning |
|---|---|
holds | The runner did not falsify the property within its budget. |
falsified | The runner found a counterexample. |
inconclusive | The runner ran out of time, the corpus was exhausted, or the proof checker bailed. |
Falsified verifications are first-class records and are how the community learns a lens is wrong. A consumer that ignores a falsified verification is making a routing decision. The record captures the falsification and lets consumers decide.
tool
The tool field records the tool's name, version, and optional
commit. Consumers reading a verification can decide whether to
trust the tool: panproto-check@0.71.0 plus a known-good commit
is a different signal from a tool the consumer has never heard of.
verifier
The party signing the verification. The PDS commit ties the verifier's signature to the record. Consumers maintain their own trust list of verifiers (per kind) and ignore verifications signed by unknown parties.
dependencies
A formal proof may depend on lemmas. A property test may depend on
a generator that itself was verified. The dependencies array
lists at-uris of those upstream verifications. A consumer auditing
the verification follows the chain, confirms each dependency is
itself trustworthy, and adopts the result only when the entire
chain checks out.
proofArtifact
For kind: formal-proof: a content-addressed reference to the
checkable proof. An orchestrator that has the proof checker
configured can mechanically verify; one that does not takes the
verifier's signed assertion on trust. The proof artifact is the
escape hatch from "trust the verifier" to "check the proof
yourself".
counterexample
For result: falsified: a content-addressed reference to a
minimal counterexample (often the smallest sample the runner
found that violated the property). Consumers can fetch the
counterexample, replay the lens against it, and confirm the
falsification independently.
Example
{
"$type": "dev.idiolect.verification",
"lens": { "uri": "at://did:plc:lens-author/dev.panproto.schema.lens/3l5" },
"kind": "roundtrip-test",
"verifier": "did:plc:verifier",
"tool": {
"name": "panproto-check",
"version": "0.71.0",
"commit": "02158abb"
},
"property": {
"$type": "dev.idiolect.defs#lpRoundtrip",
"domain": "all valid v1 records with bodies at most 1024 bytes",
"generator": "https://corpus.example/v1-1k-sample.zip"
},
"result": "holds",
"occurredAt": "2026-04-19T00:00:00.000Z"
}
Related records
A dev.idiolect.recommendation lists requiredVerifications. A
consumer adopting the recommendation queries the verifier registry
for verification records on each lens, accepts the records signed
by trusted verifiers with result: "holds", and confirms each
required verification is covered. A recommendation with required
verifications that nobody has published is a community asking for
work to be done. A dev.idiolect.bounty is how you ask for it.
Concept references
- Concepts: Lens semantics and laws
- Concepts: Lexicon evolution policy
- Tutorial: Run a verification
- Guides: Author a verification runner
- Lexicons: defs (
#lensProperty,#tool) · recommendation · bounty
dev.idiolect.vocab
A community-published vocabulary. Two compatible shapes are supported:
- The legacy single-relation tree (
actions+top+world), where every entry declares its direct subsumers and the world pins the inference discipline. - The typed multi-relation knowledge graph (
nodes+edges), where every node is first-class and edges carry a relation slug.
Authors choose either. Consumers normalize the legacy tree to the
graph form at access time by lifting each actionEntry to a
node and each parents entry to a subsumed_by edge. New
vocabularies should prefer the graph shape. The tree stays valid
for backward compatibility and remains a good fit for pure
subsumption hierarchies.
The graph shape is modeled after pub.chive.graph.{node,edge},
so cross-vocabulary translation, SKOS-style
broader_than/narrower_than mappings, and external-id
alignment (Wikidata, ROR, ORCID, ...) are first-class.
Source:
lexicons/dev/idiolect/vocab.json· Rust:idiolect_records::Vocab· TS:@idiolect-dev/schema/vocab· Fixture:idiolect_records::examples::vocab
Top-level shape
| Field | Type | Required | Notes |
|---|---|---|---|
name | string (≤128) | yes | Human-readable name. |
description | string (≤4000 graphemes) | no | Narrative description. |
world | enum | yes | closed-with-default / open / hierarchy-closed. Default subsumption discipline. |
defaultRelation | at-uri | no | Pointer to the relation-kind node consumers should treat as canonical when no relation is specified. |
top | string (≤256) | no | Identifier of the vocabulary's top action under subsumed_by. Required when actions is populated and world=closed-with-default. |
actions | array (≤4096) of actionEntry | no | Legacy tree shape. |
nodes | array (≤4096) of vocabNode | no | Graph shape. |
edges | array (≤16384) of vocabEdge | no | Graph shape. |
supersedes | at-uri | no | Prior vocabulary this one replaces. |
occurredAt | datetime | yes | Publication timestamp. |
The world discipline
A closed-enum field controlling how undeclared identifiers are treated:
| Slug | Behavior |
|---|---|
closed-with-default | Every undeclared id is subsumed by top and nothing else. The natural default for hierarchical taxonomies. |
open | Undeclared ids are incomparable to every declared id. Right when the vocab is one community's view of an open-ended space. |
hierarchy-closed | Only the declared edges exist. Strictest; appropriate when the vocabulary is a closed enumeration. |
The slug is closed enum: it is meta-policy on the inference engine, not a domain term, so extending it would change runtime semantics. Per-relation overrides live on the relation node's metadata.
The actionEntry (legacy tree)
| Field | Type | Required | Notes |
|---|---|---|---|
id | string (≤256) | yes | Stable action identifier. |
parents | array of strings | yes | Direct subsumers. Empty for top. |
class | string (≤256) | no | Identifier of the attitudinal composition this action instances. |
description | string (≤500 graphemes) | no | Optional human-readable description. |
The vocabNode (graph form)
| Field | Type | Required | Notes |
|---|---|---|---|
id | string (≤256) | yes | Stable slug. Used as edge endpoint. |
kind | open enum | no | concept / relation / instance / type / collection. |
kindVocab | vocabRef | no | Vocab the kind slug resolves against. |
subkindUri | at-uri | no | Pointer to another node typing this node's subkind. |
label | string (≤500) | no | Primary human-readable label (SKOS prefLabel). |
alternateLabels | array (≤50) | no | SKOS altLabel. |
hiddenLabels | array (≤50) | no | SKOS hiddenLabel: searchable, not displayed. |
description | string (≤2000 graphemes) | no | SKOS definition. |
scopeNote | string (≤2000 graphemes) | no | SKOS scopeNote: usage guidance. |
example | string (≤2000 graphemes) | no | SKOS example. |
historyNote | string (≤2000 graphemes) | no | SKOS historyNote. |
editorialNote | string (≤2000 graphemes) | no | SKOS editorialNote. |
changeNote | string (≤2000 graphemes) | no | SKOS changeNote. |
notation | string (≤500) | no | SKOS notation: non-text identifier. |
externalIds | array (≤20) of externalId | no | Mappings to external knowledge bases. |
status | open enum | no | proposed / provisional / established / deprecated. |
relationMetadata | relationMetadata | no | OWL 2 property characteristics. Required for kind: relation. |
vocabNode.kind
| Slug | Meaning |
|---|---|
concept | A SKOS concept; the typical case. |
relation | This node represents a relation type. Edges with this slug reference this node. |
instance | An individual; a specific entity. |
type | A metaclass; a type-of-types. |
collection | A SKOS Collection. Members linked via member_of edges. |
The vocabEdge
| Field | Type | Required | Notes |
|---|---|---|---|
source | string | yes | Source node id. |
target | string | yes | Target node id. |
relationSlug | string | yes | Relation slug. References a relation-kind node. |
metadata | unknown | no | Edge metadata, free-form. |
OWL property characteristics
A relation-kind node carries metadata declaring algebraic
properties:
| Property | Meaning |
|---|---|
symmetric | |
asymmetric | |
transitive | |
reflexive | for all in scope |
irreflexive | for all in scope |
functional | |
inverseFunctional | |
inverseOf | Pointer to the inverse relation node. |
world | Per-relation override of the vocabulary-level world. |
Contradictions (symmetric+asymmetric, reflexive+irreflexive)
are caught at validation time. Functional / inverse-functional
violations are caught at edge-walk time.
VocabGraph::validate walks
the asserted edges and emits one violation per inconsistency.
SKOS Core annotations
The full annotation set on every concept node:
| Field | SKOS counterpart |
|---|---|
label | prefLabel |
alternateLabels | altLabel |
hiddenLabels | hiddenLabel |
description | definition |
scopeNote | scopeNote |
example | example |
historyNote | historyNote |
editorialNote | editorialNote |
changeNote | changeNote |
notation | notation |
externalIds[] | exactMatch / closeMatch / broadMatch / narrowMatch / relatedMatch |
External-id mappings
Each externalId carries:
| Subfield | Type | Notes |
|---|---|---|
system | open enum | Knowledge-base identifier (wikidata, ror, orcid, lcsh, ...). |
systemVocab | vocabRef | Vocab the system slug resolves against. |
identifier | string | Identifier in that system. |
uri | uri | Optional direct URI for the external entity. |
matchType | open enum | exact / close / broader / narrower / related. |
matchTypeVocab | vocabRef | Vocab the matchType slug resolves against. |
External ids enable cross-system translation. A consumer holding
two vocabs with wikidata:Q4116214 external ids can match the
nodes across the vocabs even when the slugs differ.
Backward compatibility
The lifting from tree to graph form is mechanical:
| Tree element | Graph element |
|---|---|
actionEntry | vocabNode { id, kind: "concept" } |
Each parent in actionEntry.parents | vocabEdge { source = entry.id, target = parent, relationSlug: "subsumed_by" } |
actionEntry.class (when present) | vocabEdge { source = entry.id, target = class, relationSlug: "instance_of" } |
top | The unique node with no outbound subsumed_by edge. |
A vocab record with both actions and nodes/edges populated
is interpreted as the union after lifting. This is the
transition shape. New vocabularies should prefer pure graph
form.
Example (graph form)
{
"$type": "dev.idiolect.vocab",
"name": "vote-stances",
"description": "Default deliberation vote stances.",
"world": "open",
"nodes": [
{ "id": "agree", "kind": "concept", "label": "Agree" },
{ "id": "disagree", "kind": "concept", "label": "Disagree" },
{ "id": "pass", "kind": "concept", "label": "Pass" },
{ "id": "polar_opposite_of",
"kind": "relation",
"label": "Polar opposite of",
"relationMetadata": { "symmetric": true, "irreflexive": true }
}
],
"edges": [
{ "source": "agree",
"target": "disagree",
"relationSlug": "polar_opposite_of" }
],
"occurredAt": "2026-04-19T00:00:00.000Z"
}
Concept references
- Concepts: Open enums and vocabularies
- Concepts: The vocabulary knowledge graph
- Guides: Author a community vocabulary
- Crate reference: idiolect-records (
VocabGraph,VocabRegistry)
CLI
idiolect is the command-line tool. The surface below reflects
idiolect 0.12.1.
Top-level subcommands
idiolect resolve <did>
idiolect fetch <at-uri>
idiolect orchestrator <subcommand>
idiolect encounter record [...]
idiolect oauth login | list | logout [...]
idiolect publish <kind> --record <path> [...]
idiolect verify <kind> [...]
idiolect version # also accepts --version, -V
idiolect help [<sub>] # also accepts --help, -h
The hand-written subcommands (resolve, fetch, oauth,
publish, verify, encounter) live in their own modules
under crates/idiolect-cli/src/. The orchestrator subcommand
is generated from orchestrator-spec/queries.json and routes
its calls to the orchestrator's HTTP API.
resolve
idiolect resolve <did>
Resolve a DID
via idiolect-identity::ReqwestIdentityResolver.
Prints { did, method, handle, pds_url, also_known_as }.
fetch
idiolect fetch <at-uri>
Fetch a record body via com.atproto.repo.getRecord (under the
hood: idiolect-lens::fetcher_for_did). Prints the record value
as JSON.
orchestrator <subcommand>
The orchestrator dispatcher accepts a flat path-and-flags shape
generated from orchestrator-spec/queries.json. Each query
maps onto a subcommand and a flag set. The CLI translates the
invocation into an HTTP path and calls the orchestrator at
--url (default http://localhost:8787).
The current subcommands (run idiolect help orchestrator for
the live list):
| Command | Calls |
|---|---|
idiolect orchestrator adapters --framework <NAME> | GET /v1/adapters?framework=... |
idiolect orchestrator bounties | GET /v1/bounties/open |
idiolect orchestrator bounties --requester_did <DID> | GET /v1/bounties/by-requester?requester_did=... |
idiolect orchestrator recommendations | GET /v1/recommendations |
idiolect orchestrator verifications --lens_uri <AT-URI> | GET /v1/verifications?lens_uri=... |
Adding a query to the spec extends both the HTTP and the CLI
surface. See Run codegen. The CLI's
top-level --url flag overrides the default orchestrator base.
oauth
Authenticated PDS sessions for idiolect publish and downstream
record-writing flows.
idiolect oauth login --handle HANDLE --app-password PASSWORD [--pds-url URL]
idiolect oauth list
idiolect oauth logout --did DID
login exchanges (handle, app-password) for an access JWT via
com.atproto.server.createSession and persists {did, handle, pds_url, access_jwt, refresh_jwt} as one JSON file per DID
under $IDIOLECT_SESSION_DIR (default
~/.config/idiolect/sessions/). --app-password may be passed
as a flag or via ATPROTO_APP_PASSWORD / ATPROTO_PASSWORD
env vars to avoid leaking into shell history.
list enumerates every stored session as a JSON array of
{did, handle, pds_url} triples.
logout deletes the session file for --did. A missing file is
not an error.
publish <kind>
idiolect publish <kind> --record <path> [--rkey RKEY] [--did DID]
Loads a JSON file, validates it against the typed Record impl
for <kind> (which can be either the unqualified kind like
recommendation or the fully-qualified NSID like
dev.idiolect.recommendation), splices in a $type
discriminator, and POSTs com.atproto.repo.createRecord using
the stored session's bearer auth.
When --did is omitted, the CLI uses the first session returned by
the filesystem scan; that order is unspecified. Pass --did in
scripts and multi-account environments.
When --rkey is omitted the CLI generates a TID-shaped key.
Prints {uri, cid} of the published record on success.
verify <kind>
idiolect verify roundtrip-test --lens AT_URI [--corpus PATH] [--pds-url URL] [--verifier-did DID]
idiolect verify property-test --lens AT_URI --corpus PATH [--budget N] [--pds-url URL] [--verifier-did DID]
idiolect verify static-check --lens AT_URI [--pds-url URL] [--verifier-did DID]
idiolect verify coercion-law --lens AT_URI --vcs-url URL --standard STD [--version V] [--violation-threshold N] [--verifier-did DID]
Runs the shipped VerificationRunner for the given kind against
the live PDS, prints the typed Verification record as JSON,
and exits non-zero on Falsified / Inconclusive so CI surfaces
failures.
The corpus file (for roundtrip-test and property-test) may
be a JSON array or JSON Lines. property-test's generator
cycles through the corpus by index, so --budget controls how
many cases run.
encounter record
idiolect encounter record \
--lens <AT-URI> --source-schema <AT-URI> [--target-schema <AT-URI>] \
[--action-vocab <AT-URI>] [--kind <KIND>] [--visibility <V>] [--text-only]
Prints a dev.idiolect.encounter body after prompting for a structured
use value. It does not publish the body. Save the JSON and pass it to
idiolect publish encounter --record <path>.
Output format
Data commands print pretty-printed JSON to stdout. version and
help print text. Errors go to stderr:
error: <message>
Pipe stdout to jq for further processing.
Authentication boundary
The shipped login path uses app passwords in legacy Bearer
mode (com.atproto.server.createSession plus
Authorization: Bearer <token>). Its session file is not an
idiolect_oauth::OAuthSession and contains no DPoP private key.
Applications that require OAuth with DPoP should use
idiolect-oauth and an OAuth client directly.
HTTP query API
idiolect-orchestrator exposes this read-only endpoint set under the
query-http feature. All requests are GET. Query endpoints return JSON;
health checks and metrics return text.
The route surface is generated from
orchestrator-spec/queries.json. Each query maps onto two
endpoints: a friendly REST path under /v1/… and an
ATProto-style xrpc path under
/xrpc/dev.idiolect.query.<queryName>. Both call the same
handler. The snapshot below reflects idiolect 0.12.1.
Liveness and metrics
| Path | Returns |
|---|---|
GET /healthz | 200 OK if the process is alive. |
GET /readyz | 200 OK once the catalog has caught up. |
GET /metrics | Prometheus exposition. |
GET /v1/stats | Per-kind record counts. |
GET /v1/verifications/sufficient?lens_uri=...&kinds=...&hold=true | { sufficient, required_kinds, require_holds }; kinds is comma-separated. |
Generated query endpoints
| REST path | xrpc path | Returns |
|---|---|---|
GET /v1/bounties/open | /xrpc/dev.idiolect.query.openBounties | Bounties whose status is open, claimed, or unset. |
GET /v1/bounties/want-lens?source_uri=...&target_uri=... | /xrpc/dev.idiolect.query.bountiesForWantLens | Bounties requesting a lens for the schema pair. |
GET /v1/bounties/by-requester?requester_did=... | /xrpc/dev.idiolect.query.bountiesByRequester | Bounties by requester DID. |
GET /v1/adapters?framework=... | /xrpc/dev.idiolect.query.adaptersForFramework | Adapters declared for a framework. |
GET /v1/adapters/by-invocation-protocol?kind=... | /xrpc/dev.idiolect.query.adaptersByInvocationProtocol | Adapters by invocation-protocol kind. |
GET /v1/adapters/with-verification | /xrpc/dev.idiolect.query.adaptersWithVerification | Adapters that carry a verification reference. |
GET /v1/recommendations | /xrpc/dev.idiolect.query.recommendationsStartingFrom | Recommendations starting from a given source schema. |
GET /v1/verifications?lens_uri=... | /xrpc/dev.idiolect.query.verificationsForLens | Verifications for a specific lens. |
GET /v1/verifications/by-kind?kind=... | /xrpc/dev.idiolect.query.verificationsByKind | Verifications by kind. |
GET /v1/communities?member_did=... | /xrpc/dev.idiolect.query.communitiesForMember | Communities for a member DID. |
GET /v1/communities/by-name?name=... | /xrpc/dev.idiolect.query.communitiesByName | Communities by case-insensitive name. |
GET /v1/dialects/for-community?community_uri=... | /xrpc/dev.idiolect.query.dialectsForCommunity | Dialects owned by a community. |
GET /v1/beliefs/about?subject_uri=... | /xrpc/dev.idiolect.query.beliefsAboutRecord | Beliefs whose subject is a given record. |
GET /v1/beliefs/by-holder?holder_did=... | /xrpc/dev.idiolect.query.beliefsByHolder | Beliefs by holder DID. |
GET /v1/vocabularies/by-world?world=... | /xrpc/dev.idiolect.query.vocabulariesWithWorld | Vocabularies declared with a given world. |
GET /v1/vocabularies/by-name?name=... | /xrpc/dev.idiolect.query.vocabulariesByName | Vocabularies by exact name. |
The authoritative parameter list per endpoint is in
orchestrator-spec/queries.json.
The codegen-emitted handlers live in
crates/idiolect-orchestrator/src/generated/http.rs.
Pagination and response shape
Every generated list endpoint accepts limit (default 100, maximum
1000) and offset (default 0). Its response is:
{
"items": [
{ "uri": "at://did:plc:example/dev.idiolect.bounty/3l5", "author": "did:plc:example", "rev": "3l5", "record": {} }
],
"total": 1,
"limit": 100,
"offset": 0
}
Error shape
A request that fails parameter validation returns 400; internal
failures return 500. Both use { "error": "<code>", "message": "<detail>" }.
Versioning
The v1 and /xrpc/ prefixes are the route contract. New
endpoints are additive. Pre-1.0 the project may rename or
restructure endpoints between minor versions. See
Stability and versioning. At 1.0 the prefixes
become stable and breaking changes ship under v2 (or, for the
xrpc surface, under new method names that deprecate the old).
Stability and versioning
idiolect is pre-1.0. Minor releases in the 0.x series may break Rust
APIs, lexicon
shapes, wire formats, HTTP routes, and CLI flags.
Pin to an exact version if you depend on this project. Read the changelog before bumping.
What changes between minor versions
Pre-1.0:
- Trait signatures can tighten or widen between minor versions.
- Lexicon shapes can change. Wire-compatible changes go through the lexicon-evolution policy. Breaking changes ship with a derived migration lens.
- CLI subcommands can rename or reshape. The output JSON shape is more stable than the flag surface.
- HTTP routes can change under the
v1prefix between minor versions. After 1.0 they will not.
Current commitments
- The
dev.idiolect.*namespace stays as is. NSID renames are possible but extraordinarily unusual. One would ship with a deprecation note indev.idiolect.dialect#deprecations. - A breaking lexicon revision follows the evolution policy and should ship with a migration lens. This process does not guarantee that an older record validates unchanged against every later minor release.
- Generated Rust and TypeScript remain derived from the checked-in
lexicons, and
idiolect-codegen --checkverifies that relationship.
What changes at 1.0
- Breaking changes between minor versions stop. Breaking changes ship in major versions only.
- The lexicon-evolution
check-compatgate flips from advisory to a hard fail. - The HTTP API's
v1prefix becomes a stability commitment. New endpoints are additive. - Trait signatures in
idiolect-records,idiolect-lens,idiolect-indexer, andidiolect-orchestratorbecome semver-stable.
The project has not committed to a 1.0 release date.
Reading the changelog
The project follows Keep a Changelog plus Semantic Versioning. Every release section has six fixed buckets:
| Bucket | Contents |
|---|---|
| Added | New features. |
| Changed | Behavior changes; trait surface tightenings; lexicon shape changes. |
| Deprecated | Features that still work but are scheduled for removal. |
| Removed | Features that are gone. |
| Fixed | Bug fixes for behavior introduced in earlier versions. |
| Security | Security-relevant fixes. |
The Changelog is in
CHANGELOG.md.
Compatibility matrix
| Component | Source of truth | Lock at |
|---|---|---|
idiolect-records | crates.io | exact version |
@idiolect-dev/schema | npm | exact version |
| panproto crates | Git tag | v0.72.0 for idiolect 0.12.1 |
idiolect CLI | binary release on GitHub | release tag |
idiolect-orchestrator container | ghcr.io/idiolect-dev/orchestrator | image SHA |
idiolect-observer container | ghcr.io/idiolect-dev/observer | image SHA |
The container images are sigstore-signed. Verification policy is
in
docs/ci-cd.md.
Glossary
This glossary gives the short definitions used throughout the book. Entries for named standards link to their normative specification or the standard's official project documentation.
AT Protocol
AT Protocol, usually shortened to ATProto or atproto, is the federated protocol on which idiolect publishes records and services.
AT URI
An AT URI identifies an ATProto repository or record with an authority, an optional collection, and an optional record key. An AT URI is mutable unless paired with a CID.
Catalog
The catalog is the orchestrator's indexed store of typed idiolect records. Queries evaluate over this local read model; the catalog is not a global registry or an authority over the records it contains.
CID
A Content Identifier (CID) is a self-describing content address built from a cryptographic hash and format metadata. ATProto uses CIDs for links whose target bytes must be verifiable.
Complement
A complement stores source information that a lens cannot reconstruct from
its view alone. A put operation uses that information when it propagates an
edited view back to the source schema.
DID
A decentralized identifier (DID) is a URI that identifies an entity and resolves according to a DID method. ATProto accounts use DIDs as stable account identifiers even when handles or hosting providers change.
Dialect
A dialect is a community's published bundle of preferred schemas, lenses, and deprecations. It records a collective convention without making that convention global.
DPoP
Demonstrating Proof of Possession (DPoP) binds an OAuth token to a client-held key. The ATProto OAuth profile requires DPoP with server-issued nonces.
Encounter
An encounter is a signed record of one lens invocation, including the lens, the source and target schemas, the purpose, and the observed outcome.
Firehose
An ATProto firehose is the repository event stream described by the ATProto synchronization specification. PDSs emit events for hosted accounts; relays may aggregate many upstream streams.
Idiolect
An idiolect is one party's choice of schemas, lenses, vocabularies, and conventions. The term names the local unit of variation that idiolect preserves.
Language
A language, in this book's three-level model, is the federated substrate on which idiolects and dialects interact. It does not denote one centrally managed schema catalog.
Lens
A lens is a bidirectional translation between a source schema and a target
schema. Its get operation produces a target view; its put operation
propagates edits to that view back to the source, subject to stated lens laws.
Lexicon
Lexicon is ATProto's schema language for records, XRPC endpoints, and event-stream messages. Each Lexicon file is named by an NSID.
Lexicon family
A Lexicon family, also called a record family in generated APIs, is the set of related Lexicon records emitted and versioned together under one namespace policy.
NSID
A Namespaced Identifier (NSID) is a global
semantic identifier whose authority appears in reverse-domain order, followed by
a final name segment. dev.idiolect.belief is an NSID.
Observation
An observation is a signed aggregate computed from a stated scope of encounters by a named observer method. It is evidence produced by a method, not the raw trace of a single translation.
Observer
An observer consumes encounter or record data, applies a declared method, and publishes observations. The method and its inputs remain inspectable so consumers can evaluate the result.
Open enum
An open enum accepts a known set of values while preserving unknown strings. This representation lets older consumers retain values added by later producers.
OAuth
OAuth is the authorization framework ATProto clients use to obtain scoped access to PDS resources. The ATProto profile combines OAuth with PKCE, PAR, and DPoP requirements.
Panproto
Panproto supplies the schema graphs, protocols, protolenses, lens runtime, compatibility checks, and parsing machinery that idiolect uses. This book targets Panproto 0.71.0.
PDS
A Personal Data Server (PDS) hosts ATProto accounts, repositories, authentication, and blobs. An account may migrate between PDS providers without changing its DID.
Protocol
A protocol, in Panproto's formal model, supplies operations and laws against which schemas and lenses can be interpreted. This use is narrower than “network protocol.”
Protolens
A protolens is Panproto's schema-level description of a bidirectional transformation before that description is instantiated as a runtime lens.
Recommendation
A recommendation is a community's signed endorsement of particular schemas or lenses, optionally conditioned on verifications. It records social authority and does not itself prove a mechanical property.
Record
An ATProto record is a typed data object stored in an account repository. Its
$type value names the governing Lexicon schema, and its collection is normally
the same NSID.
Repository
An ATProto repository is an account's signed, content-addressed collection of records. A PDS stores the repository and distributes its commits through the ATProto synchronization protocol.
Schema
A schema describes the admissible structure of a record. Idiolect reads ATProto Lexicons into Panproto schema graphs for validation, comparison, and lens execution.
Strong reference
An ATProto strong reference pairs an AT URI with the target record's CID. The URI locates the record and the CID identifies the exact content.
Theory
A theory is a named collection of formal structure and constraints used to compose Panproto schemas. Idiolect's theory files state reusable semantic components rather than runtime records.
Verification
A verification is a signed report that a named runner checked a particular
property of a lens and obtained holds, falsified, or inconclusive.
Vocabulary
A vocabulary is a governed graph of concepts and relations that record fields may reference. Values remain ordinary identifiers; the vocabulary supplies their machine-readable relations and provenance.
XRPC
Lexicon RPC (XRPC) is ATProto's convention for HTTP query and procedure endpoints named by NSIDs.