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:

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:

  1. Install the CLI and fetch a lens from the idiolect project account.
  2. Validate a typed record through the generated NSID dispatcher.
  3. Apply the lens to a small JSON record with Panproto 0.71.0.
  4. Verify the round trip over a three-record corpus.
  5. 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:

  1. Index a firehose into your own handler and choose a cursor store.
  2. Run the orchestrator HTTP API over the indexed catalog.
  3. 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

GuideWhen to reach for it
Index a firehoseYou want to stream commits from a PDS firehose into your own indexer.
Run the orchestrator HTTP APIYou want a read-only query surface over cataloged records.
Run the observer daemonYou want to fold encounter-family records into observation records.
Author a verification runnerYou want to add a new property kind to the verifier.
Publish and resolve a lensYou have a panproto lens and want it on the network.
Migrate records across a revisionA schema you depend on changed, and you want to lift records across the change.
Configure OAuth sessionsYou want a session store the publishing path can use.
Run codegenYou edited a lexicon or a spec and need the generated tree refreshed.
Author a community vocabularyYou want to extend an open enum or publish a typed knowledge graph.
Bundle records into a dialectYou 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: yields RawEvents from a PDS firehose. Shipped impls: JetstreamEventStream (Jetstream websocket feed) and TappedEventStream (the at-proto-native firehose via tapped).
  • RecordHandler<F: RecordFamily = IdiolectFamily>: handles one decoded IndexerEvent<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:

  1. 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.
  2. Run cargo run -p idiolect-codegen.
  3. The generated tree picks up the HTTP route, XRPC alias, query-string parser, and response shape. Add a cli mapping 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 method descriptor (name, version, optional parameters and code reference),
  • a scope describing which records the aggregation covers,
  • the method's output payload (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-tapped feature, transitively pulled in by daemon).
  • A SQLite cursor store.
  • A CorrectionRateMethod inside ObserverHandler<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/.

MethodFolds
correction-ratePer-lens correction counts grouped by reason.
encounter-throughputEncounter traffic by kind and downstream result.
verification-coveragePer-lens verification counts by kind, result, and distinct verifiers.
lens-adoptionPer-lens encounter count and distinct invokers.
action-distributionEncounter counts grouped by use.action, optionally rolled up through a vocab.
purpose-distributionEncounter counts grouped by use.purpose.
basis-distributionRecord counts grouped by basis variant, bucketed by record kind.
attribution-chainsdev.idiolect.belief counts by holder and subject.
deliberation-tallyPer-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/:

KindRunner
roundtrip-testRoundtripTestRunner
property-testPropertyTestRunner
static-checkStaticCheckRunner
coercion-lawCoercionLawRunner

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:

  1. Add an entry to verify-spec/runners.json declaring the kind and its description. Run cargo run -p idiolect-codegen. The generated kind taxonomy (crates/idiolect-verify/src/generated.rs) picks up the new kind.
  2. Implement VerificationRunner for a struct in a new module under crates/idiolect-verify/src/. Re-export it from lib.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:

  1. A dev.panproto.schema.lens record on a PDS, carrying the protolens (or protolens chain) blob and pointers to the source and target schemas.
  2. Its source schema, as a dev.panproto.schema.schema record.
  3. Its target schema, as a dev.panproto.schema.schema record. A getSchema XRPC 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_hash is a content-addressed identifier for the chain bytes. The VerifyingResolver will refuse to hand the lens to the runtime unless the hash matches the canonical bytes.
  • round_trip_class uses the wire values iso, retraction, projection, and opaque. Consumers may use the class to select an appropriate review path.
  • laws_verified is a soft assertion that the chain passed panproto's coercion-law and existence checks. A true value here is meaningless without a corresponding dev.idiolect.verification record 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.recommendation from a community DID endorsing the lens path under stated conditions.
  • Publish dev.idiolect.verification records covering the properties consumers care about.
  • Register the lens in a dev.idiolect.dialect's preferredLenses so 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:

  1. Hand-author the chain in panproto's protolens DSL.
  2. Run schema lens inspect to classify it.
  3. Run schema theory check-coercion-laws against any CoerceType step.
  4. Run the round-trip runner against a corpus snapshot.
  5. 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

StoreFeatureUse when
InMemoryOAuthTokenStore(always)Tests and fixtures.
FilesystemOAuthTokenStorestore-filesystemA single operator process running on one host. Sessions live under a directory; one file per DID.
SqliteOAuthTokenStorestore-sqliteMulti-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

OutputSourceWhere
Per-record Rust typeslexicons/dev/idiolect/*.json and the vendored lexicons/dev/panproto/*crates/idiolect-records/src/generated/
idiolect-records family modulethe shipped lexiconscrates/idiolect-records/src/generated/family.rs
Per-record fixtures (Rust)lexicons/dev/idiolect/examples/*.jsoncrates/idiolect-records/src/generated/examples.rs
TypeScript validators + typessame lexiconspackages/schema/src/generated/
Orchestrator HTTP routesorchestrator-spec/queries.jsoncrates/idiolect-orchestrator/src/generated/
Orchestrator CLI dispatcherorchestrator-spec/queries.jsoncrates/idiolect-cli/src/generated.rs
Observer method descriptorsobserver-spec/methods.jsoncrates/idiolect-observer/src/generated.rs
Verifier kind taxonomyverify-spec/runners.jsoncrates/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

  1. Drop a JSON document under lexicons/dev/idiolect/.
  2. Run cargo run -p idiolect-codegen.
  3. Optional: add a fixture under lexicons/dev/idiolect/examples/<name>.json so the examples module emits a typed accessor.
  4. 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:

  1. Add a JSON entry to the spec file.
  2. Run cargo run -p idiolect-codegen.
  3. Implement the hand-written half (the panproto-expr predicate for an orchestrator query, the ObservationMethod impl for an observer method, the VerificationRunner impl 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 knownValues list.
  • 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.
  • version and previousVersion — 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. preferredLenses records which translations the community recommends; it does not override a consumer's trust policy.
  • An audit trail of deprecations. A consumer that sees a Deprecation entry can keep reading the deprecated NSID for some grace period and route to replacement afterwards.

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:

  1. Why idiolect exists names the private-converter problem and follows one translation through the record family.
  2. What you need first supplies the small amount of ATProto and panproto background used elsewhere.
  3. Idiolect, dialect, language separates the linguistic analogy from the concrete runtime artifacts.
  4. 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:

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:

  1. A dev.idiolect.verification reports the result of running a named check on a lens.
  2. A dev.idiolect.recommendation endorses a lens path under stated conditions and caveats.
  3. A dev.idiolect.encounter records an invocation; a correction or later observation can qualify that evidence.
  4. A dev.idiolect.dialect bundles 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 exampleRuntime 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.

LevelConcrete representationScope
IdiolectA publisher's observed records and choicesOne publisher or application
Dialectdev.idiolect.dialectA community's stated schema and lens policy
LanguageATProto repositories, event streams, Lexicons, and lensesThe 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:

  1. 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.
  2. Vocabulary collision. Two communities reuse a slug with different intended meanings. A *Vocab reference can disambiguate the vocabulary, but an omitted reference may leave policy to a canonical default outside the wire record.
  3. 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.encounter records one lens invocation, including the lens, source schema, structured use, encounter kind, and visibility.
  • dev.idiolect.correction attaches a path, reason, and corrected value to an encounter reference.
  • dev.idiolect.observation carries an observer DID, method descriptor, scope, version, and free-form aggregate output.
  • dev.idiolect.verification records a runner's holds, falsified, or inconclusive judgment about a structured lens property.
  • dev.idiolect.retrospection reports 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.belief is a holder-attributed claim about a strongly referenced record. Its subject is required; holder, basis, annotations, and visibility are optional.
  • dev.idiolect.recommendation publishes a conditioned lens path from an issuing community, with optional preconditions, verification requirements, caveats, and supersession.
  • dev.idiolect.bounty requests a lens, adapter, or verification under stated constraints and eligibility rules.
  • dev.idiolect.dialect bundles the schema references in a community's idiolect set, along with preferred lenses, deprecations, and version links.
  • dev.idiolect.community describes 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:

  1. dev.idiolect.deliberation names the community, topic, and optional status.
  2. dev.idiolect.deliberationStatement places a statement in that deliberation.
  3. dev.idiolect.deliberationVote pins a statement revision and records a stance.
  4. dev.idiolect.deliberationOutcome carries 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:

  1. PdsResolver and the PDS clients fetch record values. They do not currently validate an ATProto repository proof chain or commit signature.
  2. VerifyingResolver<R, H> canonicalizes a resolved lens record's blob, hashes those JSON bytes, and compares the result with that same record's objectHash. The bundled Sha256Hasher accepts the sha256: 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:

KindStructural readingComplement role
IsoBijectionEmpty
LensSingle-focus projection or extensionRetains dropped data or required defaults
PrismVariant injectionRetains a variant tag
AffineComposition of lens-like and prism-like behaviorRetains both forms of state
TraversalMulti-focus transformTracks 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:

ClassClaim
IsoBoth round trips are identities.
RetractionThe inverse recovers every value in the forward image.
ProjectionThe target is deterministically derived from source data, but no inverse recovers the source from that target alone.
OpaqueNo 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:

  • RoundtripTestRunner checks forward-then-backward equality on a nonempty, caller-supplied corpus.
  • PropertyTestRunner performs the same round trip on values from a caller-supplied generator and finite budget.
  • StaticCheckRunner validates the source and target panproto schema graphs; it does not execute the lens.
  • CoercionLawRunner delegates 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_by tests the subsumed_by relation in one VocabGraph.
  • satisfies tests reachability under a caller-selected relation.
  • translate_to asks a VocabRegistry for an equivalent_to translation 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) uses subsumed_by with reflexive reachability.
  • is_subsumed_by(specific, general) tests membership in that closure.
  • direct_targets and direct_sources return one-hop neighbors.
  • equivalent_in(source, other) searches equivalent_to reachability for a node id known to the other graph.
  • top returns a unique non-relation root under subsumed_by, when one exists; top_with gives 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:

  1. functional relations have at most one target per source;
  2. inverse-functional relations have at most one source per target;
  3. irreflexive relations contain no self-loop;
  4. asymmetric relations contain no pair of reverse edges; and
  5. declarations do not combine symmetric with asymmetric or reflexive with irreflexive.

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:

  • InMemoryPublisher retains typed observations for tests and local callers.
  • LogPublisher emits structured tracing events without repository durability.
  • PdsPublisher adds $type: "dev.idiolect.observation" and calls the configured PdsWriter to 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:

MethodSnapshot coordinate
correction-rateCorrection counts by lens and reason
encounter-throughputEncounters by kind and downstream result
verification-coverageVerifications by lens, kind, result, and verifier
lens-adoptionEncounter and invoker counts by lens
action-distributionEncounters by structured action
purpose-distributionEncounters by structured purpose
basis-distributionRecords by basis variant and record kind
attribution-chainsBeliefs by holder and subject
deliberation-tallyVotes 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:

  1. no structural change;
  2. a compatible change that needs no record migration; or
  3. 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 diff accepts positional OLD NEW paths rather than --src and --tgt, and it has no --json flag;
  • schema lens generate requires --protocol and needs --chain to emit a reusable chain;
  • schema lens inspect reports the current optic kinds; and
  • schema lens verify accepts a data file and optional schema, not the directory/--schema/--chain combination 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:

  1. compare the old and new parsed schemas with idiolect-migrate::classify;
  2. require a reviewed MigrationPlan or a hand-authored chain for each breaking change;
  3. inspect the current OpticKind and complement requirements;
  4. run the applicable verification runners on versioned inputs;
  5. publish the lens and verification records through an authenticated writer;
  6. update dialect, deprecation, and recommendation records deliberately; and
  7. retain the artifacts that identify the two schema revisions and test corpus.

Two points follow:

  1. The EEC does not make every migration reversible.
  2. 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.

SectionContents
CratesOne page per workspace crate, with public types, traits, error variants, and feature flags.
LexiconsOne page per dev.idiolect.* lexicon, with field-by-field shape.
CLIEvery shipped idiolect subcommand, its flags, and its output.
HTTP query APIEvery endpoint exposed by the orchestrator, request and response shape.
Stability and versioningThe 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 boundaryStart hereThen inspect
Add a record family or emitter targetidiolect-codegenRecordFamily and the emit functions
Add a stream, handler, or cursor backendidiolect-indexerTrait signatures, feature flags, and error variants
Add a lens resolver or schema loaderidiolect-lensResolver, loader, apply-input, and apply-output types
Add an observation or verification methodidiolect-observer or idiolect-verifyGenerated taxonomies and implementation traits
Integrate over process boundariesCLI or HTTP APIExact 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.

CratePurpose
idiolect-recordsGenerated record types for the dev.idiolect.* lexicons; Record trait; family modules.
idiolect-codegenLexicon-driven Rust + TypeScript emitter; generated-source check; breaking-change classifier.
idiolect-lensResolve PanprotoLens records; run apply_lens.
idiolect-identityDID resolution (did:plc, did:web).
idiolect-indexerFirehose consumer with pluggable stream / handler / cursor store.
idiolect-oauthOAuthTokenStore trait and shipped impls.
idiolect-observerFold encounter-family records into observation records.
idiolect-orchestratorRead-only HTTP query API over a record catalog.
idiolect-verifyVerification runners with declarative dispatch.
idiolect-migrateSchema diff plus lens-based record migration.
idiolect-cliCommand-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-records

This 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

TypeFormat
AtUriat-uri
Diddid
Nsidnsid
DatetimeRFC 3339
UriURL
CidCID
LanguageBCP 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:

SubcommandPurpose
generate (default)Emit every generated tree.
checkVerify 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.
listList lexicons, their kinds, and fixture availability.
doctorCheck the workspace layout and fixtures.
check-compat --baseline <path>Classify changes against a baseline lexicon tree; exit 1 if a breaking change is found.
helpPrint 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>.rs with the typed record struct, every nested defs type, the Record impl, 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.json produces the orchestrator's HTTP routes (crates/idiolect-orchestrator/src/generated/) and the matching CLI dispatcher (crates/idiolect-cli/src/generated.rs).
  • observer-spec/methods.json produces the observer's method taxonomy (crates/idiolect-observer/src/generated.rs).
  • verify-spec/runners.json produces 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:

  1. NSIDs are ASCII, lowercase, dot-separated. The emitter rejects non-conforming input.
  2. PascalCase names are derived deterministically from a slug. On collision (foo-bar and foo_bar), the second occurrence gets a numeric suffix (FooBar2).
  3. 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 = false and is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally with cargo 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:

TypeBacking store
InMemoryResolverHashMap<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_symmetric pairs two state-based lenses that share a middle schema. This view-only helper calls panproto 0.71.0's put_without_complement, so the incoming span leg must be an isomorphism. A lossy incoming leg requires complement state from an earlier get and direct use of panproto's lower-level SymmetricLens API.

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:

FeatureAdds
pds-reqwestReqwestPdsClient (read-only). The reqwest-backed write surface uses SigningPdsWriter plus a DpopProver (one of StaticDpopProver, NoOpDpopProver, or P256DpopProver with the dpop-p256 feature).
pds-atriumAtriumPdsClient.
pds-resolvefetcher_for_did, publisher_for_did; DID-to-PDS resolution helpers. Pulls in idiolect-identity.
dpop-p256The 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-identity

This 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.

TypeFeatureBacking
InMemoryIdentityResolver(always)HashMap<Did, DidDocument>. Tests and fixtures.
ReqwestIdentityResolverresolver-reqwestReqwest-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

FeatureAdds
resolver-reqwestThe 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-indexer

This 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

TypeFeaturePurpose
JetstreamEventStreamfirehose-jetstreamSubscribes to a Jetstream websocket feed.
TappedEventStreamfirehose-tappedSubscribes to the at-proto-native firehose via tapped.
ReconnectingEventStream<C, F, S>reconnectingRecreates an S: EventStream with a caller-supplied async connection factory and exponential backoff.
InMemoryCursorStore(always)HashMap-backed; for tests.
FilesystemCursorStorecursor-filesystemOne JSON file containing the cursor map for every subscription ID.
SqliteCursorStorecursor-sqliteOne row per stream. Pairs with handlers that also write SQLite.
NoopRecordHandler(always)Counts events and drops them. Useful as a baseline.
RetryingHandler / CircuitBreakerHandlerresilienceWraps an inner handler with retry / circuit-breaker policies.

Error surface

IndexerError flattens the failure modes from all three boundaries. Variants:

VariantTrigger
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

FeatureAdds
firehose-jetstreamJetstream websocket client.
firehose-tappedTapped at-proto-native firehose client.
cursor-filesystemFilesystem cursor store.
cursor-sqliteSQLite cursor store.
reconnectingReconnect wrapper.
resilienceRetry 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 = false and is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally with cargo 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

StoreFeatureBacking
InMemoryOAuthTokenStore(always)HashMap-backed; for tests.
FilesystemOAuthTokenStorestore-filesystemOne JSON file per session.
SqliteOAuthTokenStorestore-sqliteOne 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

FeatureAdds
store-filesystemThe filesystem-backed session store.
store-sqliteThe 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 = false and is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally with cargo 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 nameModuleFolds
correction-ratecorrection_ratePer-lens correction counts grouped by reason.
encounter-throughputencounter_throughputEncounter traffic by kind and downstream result.
verification-coverageverification_coveragePer-lens verification counts by kind, result, and distinct verifiers.
lens-adoptionlens_adoptionPer-lens encounter count and distinct invokers.
action-distributionaction_distributionEncounter counts grouped by use.action (with optional vocab roll-up).
purpose-distributionpurpose_distributionEncounter counts grouped by use.purpose.
basis-distributionbasis_distributionRecord counts grouped by basis variant, bucketed by record kind.
attribution-chainsattribution_chainsCounts of dev.idiolect.belief records by holder and subject.
deliberation-tallydeliberation_tallyPer-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 implement ObservationMethod.
  • Instance-form methods consume a panproto WInstance plus the NSID and implement InstanceMethod. They wrap into ObservationMethod via InstanceMethodAdapter.

default_methods() returns boxed instances of every record-form method.

Publisher

ObservationPublisher is the persistence boundary. Shipped implementations:

TypeBacking
InMemoryPublisherVec<Observation>. For tests.
PdsPublisherWrites via an idiolect_lens::PdsWriter to the observer's PDS.
LogPublisherEmits each observation as a structured tracing event.

Errors

ObserverError is a flattened error type. ObserverResult<T> is its alias.

Feature flags

FeatureAdds
daemonThe idiolect-observer binary plus its CLI. Pulls in tracing-subscriber, anyhow, and the indexer's tapped firehose / sqlite cursor features.
pds-atriumForwards 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 = false and is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally with cargo 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 holding Entry<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 (under catalog-sqlite) — persistent catalog backing.
  • CatalogHandler — the indexer's RecordHandler<IdiolectFamily> impl that upserts every accepted record into the catalog.
  • AppState plus http_router() — axum router wiring under query-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:

PathReturns
GET /healthz, GET /readyzLiveness + readiness.
GET /metricsPrometheus exposition.
GET /v1/statsPer-kind record counts.
GET /v1/bounties/openCataloged 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/recommendationsRecommendations 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

FeatureAdds
catalog-sqliteSQLite-backed catalog store.
query-httpHTTP server (axum-based).
daemonThe 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 = false and is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally with cargo 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:

KindRunner
roundtrip-testRoundtripTestRunner — runs put(get(a)) == a over a corpus.
property-testPropertyTestRunner — runs an arbitrary boolean predicate over a corpus.
static-checkStaticCheckRunner — resolves the lens's source and target schemas and runs panproto's schema validator on both.
coercion-lawCoercionLawRunner — 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

  1. Add the kind's entry to verify-spec/runners.json.
  2. Run cargo run -p idiolect-codegen to refresh the generated kind taxonomy.
  3. Implement the VerificationRunner trait against the new kind in a new module under crates/idiolect-verify/src/.
  4. Re-export it from lib.rs and add to the runner registry wiring.

idiolect-migrate

Source: crates/idiolect-migrate/

This crate is publish = false and is not on docs.rs. The authoritative reference is the source above plus the rustdoc built locally with cargo 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 a CompatReport distinguishing compatible from breaking changes.
  • plan_auto(old, new, protocol, source_schema_hash, target_schema_hash) asks panproto's auto_generate to derive a MigrationPlan carrying source / target schema hashes plus a protolens chain the caller can publish. For breaking diffs that resist automatic derivation, it returns Err(PlannerError::NotAutoDerivable) listing the offending changes.
  • migrate_record(resolver, schema_loader, protocol, lens_uri, source_record) wraps idiolect_lens::apply_lens for one record.
  • MigrationPlan — the typed plan struct.
  • MigrateError, MigrateResult, PlannerError — the error types.
  • Re-exported CompatReport and SchemaDiff from panproto-check for convenience.

Migration shapes

DiffBehavior
Non-breaking (added optional, added vertex, added edge)classify returns compatible = true; no plan is needed.
Auto-derivable breakingplan_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:

  1. The migration-shaped API (classify-then-plan-then-migrate) is a different shape than the runtime API (apply_lens plus resolvers).
  2. idiolect-migrate depends on panproto-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 = false and 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:

  1. 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.
  2. 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

SettingDefaultOverride
Orchestrator URLhttp://localhost:8787--url flag on orchestrator subcommands
Log levelwarnRUST_LOG environment variable
Session directory$HOME/.config/idiolect/sessionsIDIOLECT_SESSION_DIR environment variable
Login app passwordnone--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/.

NSIDPage
dev.idiolect.adapteradapter
dev.idiolect.beliefbelief
dev.idiolect.bountybounty
dev.idiolect.communitycommunity
dev.idiolect.correctioncorrection
dev.idiolect.defsdefs
dev.idiolect.deliberationdeliberation
dev.idiolect.deliberationStatementdeliberationStatement
dev.idiolect.deliberationVotedeliberationVote
dev.idiolect.deliberationOutcomedeliberationOutcome
dev.idiolect.dialectdialect
dev.idiolect.encounterencounter
dev.idiolect.observationobservation
dev.idiolect.recommendationrecommendation
dev.idiolect.retrospectionretrospection
dev.idiolect.verificationverification
dev.idiolect.vocabvocab

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

FieldTypeRequiredNotes
frameworkstring (at most 128 characters)yesCanonical framework name (e.g. hasura, prisma, coq).
versionRangestringyesSemver range supported.
invocationProtocolobjectyesHow the adapter is invoked.
isolationobjectyesSandboxing requirements an adapter host is expected to enforce.
authordidyesDID of the adapter author.
verificationat-urinoOptional verification record demonstrating conformance.
occurredAtdatetimeyesPublication timestamp.

invocationProtocol

SubfieldTypeRequiredNotes
kindopen enumyessubprocess / http / wasm.
kindVocabvocabRefnoVocab the kind slug resolves against.
entryPointstringnoBinary name (subprocess), URL (http), or WASM module reference.
inputSchemaschemaRefnoSchema of the adapter's input.
outputSchemaschemaRefnoSchema of the adapter's output.

isolation

SubfieldTypeRequiredNotes
kindopen enumyesnone / process / container / vm / wasm-sandbox.
kindVocabvocabRefnoVocab the kind slug resolves against.
networkPolicyopen enumnonone / egress-denylist / egress-allowlist / full.
networkPolicyVocabvocabRefnoVocab the policy slug resolves against.
filesystemPolicyopen enumnoreadonly / scratch / writable-subtree / full.
filesystemPolicyVocabvocabRefnoVocab the policy slug resolves against.
resourceLimits{ maxMemoryBytes?, maxCpuSeconds?, maxWallSeconds? }noRequested 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:

SlugWhat it means
subprocessFork entryPoint as a child process and exchange JSON over standard input and output.
httpSend JSON to the URL at entryPoint.
wasmInstantiate 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:

SlugWhat it means
noneRun in the orchestrator's own process. Only safe for fully-trusted code.
processFork into a separate process; OS-level isolation.
containerRun in a container (Docker, Podman, Firecracker microVM).
vmRun in a full VM.
wasm-sandboxRun 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:

networkPolicyWhat it means
noneNo network access.
egress-denylistNetwork access except to listed denied hosts.
egress-allowlistNetwork access only to listed allowed hosts.
fullUnrestricted.
filesystemPolicyWhat it means
readonlyThe adapter sees a read-only mount.
scratchThe adapter writes to a scratch directory cleaned up after each invocation.
writable-subtreeThe adapter writes to a designated subtree.
fullUnrestricted.

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:

FieldUnit
maxMemoryBytesRAM, in bytes.
maxCpuSecondsCPU time, in seconds.
maxWallSecondsWall-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

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

FieldTypeRequiredNotes
subjectstrongRecordRefyesAT-URI + CID for the record the belief is about.
holderdidnoParty whose attitude is represented. Omit for first-party.
basisbasisnoStructured grounding (load-bearing when holder differs from the repo owner).
annotationsstring (at most 4,000 graphemes)noNarrative commentary.
visibilityvisibilitynoVisibility scope.
occurredAtdatetimeyesWhen 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:

VariantUse when
basisSelfAssertedThe holder asserted directly with no external grounding claimed. The default when basis is omitted.
basisCommunityPolicyGrounded in a community's published policy.
basisExternalSignalGrounded in something outside ATProto (a license, an external standard, a statement on another network).
basisDerivedFromRecordGrounded 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:

  1. The lens record (in the lens author's PDS).
  2. The encounter or recommendation describing the use (in either the labeler's or the third party's PDS).
  3. A belief record (in the labeler's PDS) with subject pointing at the encounter / recommendation, holder set to the third party's DID, and basis carrying 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"
}
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

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

FieldTypeRequiredNotes
requesterdidyesWho is requesting.
wantsunionyesExactly one of wantLens / wantVerification / wantAdapter.
constraintsarray (at most 64 entries)noStructured constraints the deliverable must satisfy.
rewardobjectno{ summary?, externalRef? }. idiolect does not transact.
eligibilityarray (at most 128 entries)noPostfix eligibility tree.
fulfillmentat-urinoOnce fulfilled, points to the deliverable record.
statusopen enumnoopen / claimed / fulfilled / withdrawn.
statusVocabvocabRefnoVocab the status slug resolves against.
basisbasisnoStructured grounding.
occurredAtdatetimeyesPublication timestamp.

The three want shapes

wantLens

Asks for a lens between two schemas.

SubfieldTypeRequiredNotes
sourceschemaRefyesSource schema.
targetschemaRefyesTarget schema.
bidirectionalbooleannoWhether the requested lens must be invertible.

wantVerification

Asks for a verification of a lens.

SubfieldTypeRequiredNotes
lenslensRefyesThe lens to verify.
kindopen enumyesVerification 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.)
kindVocabvocabRefnoVocab the kind slug resolves against.

wantAdapter

Asks for an adapter for a framework.

SubfieldTypeRequiredNotes
frameworkstring (at most 128 characters)yesFramework name.
versionRangestringnoSemver range.

The constraint variants

Each entry in constraints is one of:

VariantCaptures
constraintPerformanceA 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.
constraintConformanceA verification kind (and optional specific property) the deliverable must pass.
constraintLicenseAn SPDX license expression plus optional allow and deny lists.
constraintDeadlineA datetime deadline plus optional grace seconds.
constraintDependencyA 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:

VariantArityMeaning
eligibilityMemberatomicClaimer is a member of the named community.
eligibilityVerificationForatomicClaimer has published a verification for the named lens property.
eligibilityDidatomicClaimer's DID matches exactly.
eligibilityAndcombinatorConjoin top two on stack.
eligibilityOrcombinatorDisjoin top two on stack.
eligibilityNotcombinatorNegate 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"
}
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

FieldTypeRequiredNotes
namestring (≤128)yesHuman-readable community name.
descriptionstring (≤2000 graphemes)yesPurpose, norms, scope. Narrative.
membersarray of did (≤500)noInline membership for small communities.
roleAssignmentsarray of roleAssignment (≤500)noSparse role assignments where the role differs from the default.
memberRoleVocabvocabRefnoVocab the role slugs resolve against.
recordHostingopen enumnomember-hosted / community-hosted / hybrid.
appviewEndpointurinoURL of the community AppView when recordHosting is non-default.
membershipRollat-urinoExternal membership record (for communities above ~200 members).
coreSchemasarray of schemaRefnoSchemas the community treats as canonical.
coreLensesarray of lensRefnoLenses the community treats as canonical.
endorsedCommunitiesarray of at-urinoOther communities recognized as legitimate interlocutors. Not transitive.
conventionsarray (≤64) of structured convention variantsnoDecidable subset of community conventions.
conventionsTextstring (≤10000 graphemes)noNarrative conventions: style guides, norms not expressible structurally.
createdAtdatetimeyesPublication timestamp.

roleAssignment

SubfieldTypeRequiredNotes
diddidyesDID of the member.
roleopen enumyesmember / 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:

VariantCaptures
conventionReviewCadenceMaximum business-days expected before a review is posted, with optional scope narrowing.
conventionVerificationReqA verification kind (and optionally a specific property) the community requires before endorsing a lens.
conventionDeprecationPolicyMinimum 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 members is 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 membershipRoll is 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

SlugWhat it means
member-hostedRecords live on individual member PDSes (default ATProto).
community-hostedRecords live on a community AppView, gated by membership (Acorn-style).
hybridBoth. 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

FieldTypeRequiredNotes
encounterencounterRefyesThe encounter whose output was edited.
pathstring (at most 1,024 characters)yesJSON Pointer or equivalent into the produced output.
originalValueunknownnoValue prior to correction. May be elided for visibility reasons.
correctedValueunknownnoValue after correction.
reasonopen enumyeslens-error / domain-difference / source-error / downstream-idiosyncrasy / user-mistake / retrospective.
reasonVocabvocabRefnoVocab the reason slug resolves against.
rationalestring (at most 2,000 graphemes)noHuman-readable justification.
holderdidnoParty the correction is attributed to.
basisbasisnoStructured grounding for third-party attribution.
visibilityvisibilityyesVisibility scope.
occurredAtdatetimeyesWhen 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:

SlugWhat it meansImplication for the lens
lens-errorThe lens produced wrong output for the input.Bug in the lens.
domain-differenceThe lens output is correct under one set of conventions; the consumer wants a different set.Not a bug; a different community translation.
source-errorThe source record was wrong; the lens propagated the error faithfully.Not a bug; upstream issue.
downstream-idiosyncrasyThe downstream consumer has an unusual requirement the lens does not target.Not a bug; consumer-specific.
user-mistakeThe user invoked the wrong lens.Not a bug; routing issue.
retrospectiveA 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"
}
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

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.

SubfieldTypeNotes
uriat-uriAT-URI pointing to a schema record.
cidcid-linkContent hash of the schema.
languagestringSchema-language identifier (atproto-lexicon, postgres-sql, protobuf, graphql, json-schema).

lensRef

SubfieldTypeNotes
uriat-uriAT-URI of a lens record.
cidcid-linkContent hash of the lens.
directionenumunidirectional / bidirectional.

encounterRef

SubfieldTypeNotes
uriat-uri (required)AT-URI of the encounter.
cidcid-linkOptional CID for revision pinning.

vocabRef

SubfieldTypeNotes
uriat-uriAT-URI of a vocab record.
cidcid-linkContent hash pinning a specific vocab revision.

strongRecordRef

SubfieldTypeRequiredNotes
uriat-uriyesAT-URI of the referenced record.
cidcid-linkyesContent hash.

Parallel to com.atproto.repo.strongRef. Repeated here so the defs tree is self-contained.

tool

SubfieldTypeRequiredNotes
namestringyesCanonical tool name (panproto, coq, tlaplus, z3, nextest).
versionstringyesVersion string.
commitstringnoOptional source commit or build identifier.

visibility

A closed-enum string. Five values:

ValueMeaning
public-detailedFull record body published.
public-minimalRecord published with elided detail (e.g. omits source instance).
public-aggregate-onlyRecord consumed only by aggregators; individual reads suppressed.
community-scopedDeclares an intended community scope. The current runtime does not enforce access control for this value.
privateShould 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.

SubfieldTypeRequiredNotes
actionstring (≤256)yesAction identifier, resolved against actionVocabulary.
materialmaterialSpecnoWhat is being acted on.
purposestring (≤256)noThe end the action serves.
actorstring (≤256)noWho performs or benefits.
actionVocabularyvocabRefnoVocab the action slug resolves against.
purposeVocabularyvocabRefnoVocab the purpose slug resolves against.

materialSpec

SubfieldTypeNotes
scopestring (≤256)Community-defined scope (classroom_materials, production_logs, scraped_corpus).
uriuriOptional pointer to a specific dataset.

lensProperty

A union covering the seven verification kinds. Each verification record carries one of these as its property field.

VariantUsed by
lpRoundtripkind: roundtrip-test
lpGeneratorkind: property-test
lpTheoremkind: formal-proof
lpConformancekind: conformance-test
lpCheckerkind: static-check
lpConvergencekind: convergence-preserving
lpCoercionLawkind: coercion-law

lpRoundtrip

SubfieldTypeRequiredNotes
domainstring (≤512)yesSymbolic description of the input set.
generatorurinoOptional pointer to a generator that enumerates the domain.

lpGenerator

SubfieldTypeRequiredNotes
specstring (≤2000)yesGenerator specification (proptest Strategy reference, Hypothesis strategy, QuickCheck Arbitrary).
runnerstringnoName of the PBT runtime.
seedintegernoOptional seed for reproducibility.

lpTheorem

SubfieldTypeRequiredNotes
statementstring (≤4000)yesThe theorem, in the declared system syntax.
systemstringnoProof system (coq, lean4, agda, tlaplus, z3).
freeVariablesarray of stringsnoNames of free variables.

lpConformance

SubfieldTypeRequiredNotes
standardstringyesStandard identifier (iso-8601, rfc-3339, en-pos-v2.1).
versionstringyesStandard version.
clausesarray of stringsnoOptional subset of the standard's clauses.

lpChecker

SubfieldTypeRequiredNotes
checkerstringyesStatic-checker identifier (panproto-check, clippy, tsc-strict).
rulesetstringnoNamed ruleset or configuration preset.
versionstringnoChecker version.

lpConvergence

SubfieldTypeRequiredNotes
propertystring (≤1000)yesSymbolic name or description of the preserved property.
boundStepsintegernoOptional bound on steps to fixpoint.

lpCoercionLaw

SubfieldTypeRequiredNotes
standardstring (≤256)yesIdentifier of the coercion-law standard.
versionstring (≤64)noOptional version.
violationThresholdintegernoCap on the violations a runner may report before falsifying.

evidence

A union of structured witnesses for retrospection findings.

VariantUsed when finding kind is
evidenceDivergencemerge-divergence
evidenceLossdata-loss
evidenceMismatchreconciliation-mismatch

evidenceDivergence

SubfieldTypeRequiredNotes
pathAarray of lensRefyesLenses composed in path A.
pathBarray of lensRefyesLenses composed in path B.
witnessInputcid-linknoOptional CID where the two paths diverge.

evidenceLoss

SubfieldTypeRequiredNotes
sourceFieldstringyesDotted path identifying the lost field.
targetSchemaschemaRefnoTarget schema where the loss was observed.
witnessInputcid-linknoWitness input.

evidenceMismatch

SubfieldTypeRequiredNotes
leftRecordcid-linknoLeft record.
rightRecordcid-linknoRight record.
expectedEqualityOnstringnoDotted path or projection under which equality was expected.

caveat

SubfieldTypeRequiredNotes
modestringyesShort failure-mode identifier.
affectsarray of stringsnoDotted paths or field names.
severityenumnoinfo / warn / error.

basis

A union of structured grounds for an attitudinal claim.

VariantUse when
basisSelfAssertedThe holder asserts directly. The default when basis is omitted.
basisCommunityPolicyGrounded in a community's published policy. Carries community (at-uri) and optional policyUri.
basisExternalSignalGrounded in something outside ATProto. Carries url, optional signalType, optional description.
basisDerivedFromRecordGrounded 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

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

FieldTypeRequiredNotes
owningCommunityat-uriyesThe community whose membership is deliberating.
topicstring (≤200 graphemes)yesHuman-readable topic or question.
descriptionstring (≤1000 graphemes)noExtended framing or context.
authRequiredboolean (default true)noWhether participation requires authenticated membership.
classificationopen enumnoquestion / proposal / grievance / retrospective.
classificationVocabvocabRefnoVocab the classification slug resolves against.
statusopen enumnoopen / closed / tabled / adopted / rejected.
statusVocabvocabRefnoVocab the status slug resolves against.
closedAtdatetimenoWhen the deliberation moved out of an open status.
outcomeat-urinoPointer to a deliberationOutcome record summarizing the resolved stance.
createdAtdatetimeyesPublication 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

SlugWhat it means
questionAn open question without a proposed resolution.
proposalA specific proposal under consideration.
grievanceA complaint or dispute.
retrospectiveA 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

SlugWhat it means
openActive. Statements and votes accepted.
closedNo longer accepting input. May or may not have an outcome.
tabledClosed but explicitly deferred for later.
adoptedClosed with a positive resolution.
rejectedClosed 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

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

FieldTypeRequiredNotes
deliberationstrongRecordRefyesAT-URI + CID for the deliberation this statement participates in.
textstring (≤400 graphemes)yesStatement text.
classificationopen enumnoclaim / proposal / dissent / clarification / question.
classificationVocabvocabRefnoVocab the classification slug resolves against.
anonymousboolean (default false)noWhether the statement was submitted anonymously.
createdAtdatetimeyesPublication 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

SlugWhat it captures
claimAn assertion of fact or opinion.
proposalA specific proposed action.
dissentAn objection to a prior statement or to the deliberation framing.
clarificationA request for or provision of clarification.
questionAn 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

FieldTypeRequiredNotes
subjectstrongRecordRefyesAT-URI + CID for the statement being voted on.
stanceopen enumyesagree / pass / disagree.
stanceVocabvocabRefnoVocab the stance slug resolves against.
weightinteger ∈ [0, 1000]noOptional ranking signal. Convention: scaled by 1000 for the 0.0–1.0 range.
rationalestring (≤500 graphemes)noOptional narrative reason.
createdAtdatetimeyesPublication 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:

SlugMeaning
agreeAffirms the statement.
passAbstains.
disagreeRejects 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"
}

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

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

FieldTypeRequiredNotes
deliberationstrongRecordRefyesAT-URI + CID for the deliberation.
statementTalliesarray (≤4096) of statementTallyyesPer-statement vote counts.
adoptedarray (≤256) of strongRecordRefnoStatements the community adopted.
stanceVocabvocabRefnoVocab the per-tally stance slugs resolve against.
computedAtdatetimeyesWhen the observer computed this tally.
tooltoolnoIdentity and version of the aggregator.
occurredAtdatetimeyesPublication timestamp.

statementTally

SubfieldTypeRequiredNotes
statementstrongRecordRefyesThe statement these counts aggregate.
countsarray (≤64) of stanceCountyesPer-stance vote counts.
weightedCountsarray (≤64) of stanceCountnoPer-stance weighted vote counts (when votes carried weight). Scaled by 1000.

stanceCount

SubfieldTypeRequiredNotes
stancestring (≤256)yesStance slug, resolved through the outcome's stanceVocab.
countnon-negative integeryesVote 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 tool field.
  • 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 mapEnum lens (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

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

FieldTypeRequiredNotes
owningCommunityat-uriyesThe community that owns this dialect.
namestring (≤128)yesHuman-readable dialect name.
descriptionstring (≤4000 graphemes)noPurpose and scope.
idiolectsarray of schemaRefnoSchemas that constitute the dialect's idiolect set.
preferredLensesarray of lensRefnoTranslations the community prefers.
deprecationsarray of DeprecationnoDeprecated entries with replacement pointers.
versionstringnoDialect version (semver when applicable).
previousVersionat-urinoPredecessor revision in a version chain.
createdAtdatetimeyesPublication timestamp.

Deprecation

SubfieldTypeRequiredNotes
refat-uriyesThe deprecated idiolect or lens.
replacementat-urinoOptional successor.
deprecatedAtdatetimeyesWhen the deprecation took effect.
reasonstring (≤1000 graphemes)yesWhy 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

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

FieldTypeRequiredNotes
lenslensRefyesThe lens that was invoked.
sourceSchemaschemaRefyesSource schema the lens translated from.
targetSchemaschemaRefnoTarget schema produced by the lens. Often implied by the lens; elided when unambiguous.
sourceInstancecid-linknoContent-addressed reference to the source instance. Omit when visibility restricts publishing source data.
producedOutputcid-linknoContent-addressed reference to the produced output.
useuseyesStructured action / material / purpose / actor.
downstreamResultopen enumnosuccess / corrected / rejected / unknown.
downstreamResultVocabvocabRefnoVocab the slug resolves against.
annotationsstring (≤4000 graphemes)noNarrative commentary.
holderdidnoParty the encounter is attributed to. Omit for first-party records.
basisbasisnoStructured grounding when holder differs from the repo owner.
kindopen enumyesCorpus-kind slug (invocation-log, curated, roundtrip-verified, production, adversarial).
kindVocabvocabRefnoVocab the kind slug resolves against.
visibilityvisibilityyespublic-detailed / public-minimal / public-aggregate-only / community-scoped / private.
occurredAtdatetimeyesWhen the invocation happened. Distinct from the record's createdAt.

Field details

use

The structured payload. A use carries:

  • action (open-enum slug, resolved against actionVocabulary).
  • material (a materialSpec: scope plus optional corpus pointer).
  • purpose (open-enum slug, resolved against purposeVocabulary).
  • 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:

SlugMeaning
invocation-logA real production invocation.
curatedA hand-picked sample, often used for evaluation.
roundtrip-verifiedAn invocation where put(get(a)) == a was verified at write time.
productionSynonym for invocation-log in some pipelines; exists for distinct trust weighting.
adversarialAn 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:

SlugMeaning
successThe output was accepted unchanged.
correctedThe output was edited; a dev.idiolect.correction record exists or is expected.
rejectedThe output was unusable.
unknownThe 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"
}
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

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

FieldTypeRequiredNotes
observerdidyesDID of the observer publishing this aggregate.
methodobjectyes{ name, description?, codeRef?, parameters? }. The aggregator identity and configuration.
scopeobjectyesThe set of records the observation aggregates over.
outputunknownyesMethod-defined payload (counts, scores, diagnostic summaries).
versionstringyesMethod version. Different versions may produce non-comparable outputs.
basisbasisnoGrounding when the observer is not the repo owner.
visibilityvisibilityyesVisibility scope.
occurredAtdatetimeyesWhen the observation was published.

method

SubfieldTypeRequiredNotes
namestring (≤128)yesShort method identifier.
descriptionstring (≤4000 graphemes)noNarrative method description.
codeRefat-urinoReference to the method's source or specification.
parametersunknownnoFree-form JSON, observer-defined.

scope

SubfieldTypeRequiredNotes
lensesarray of lensRefnoLenses included; empty or omitted means "all".
communitiesarray of at-urinoCommunities whose records are in scope.
encounterKindsarray of open-enum slugsnoEncounter kinds weighted in the aggregation.
encounterKindsVocabvocabRefnoVocab the kind slugs resolve against.
window{ from?, until? }noTime 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

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

FieldTypeRequiredNotes
issuingCommunityat-uriyesCommunity publishing the recommendation.
conditionsarray (≤128) of ConditionyesStructured applicability predicate. Empty array means "always applies".
preconditionsarray (≤128) of ConditionnoAdditional structured assumptions the consumer must verify.
lensPatharray (≥1) of lensRefyesOrdered sequence of lenses to compose.
annotationsstring (≤8000 graphemes)noNarrative explanation.
requiredVerificationsarray of lensPropertynoSpecific properties the recommendation assumes.
caveatsarray (≤32) of CaveatnoStructured failure-mode list.
caveatsTextstringnoNarrative companion to caveats.
basisbasisnoStructured grounding for the attitudinal claim.
supersedesat-urinoPrior recommendation this one replaces.
occurredAtdatetimeyesPublication timestamp.

The condition tree

conditions and preconditions are postfix-operator trees over the combinator set defined inline in this lexicon:

VariantArityPurpose
conditionSourceIsatomicMatch invocations whose source schema equals the named at-uri.
conditionTargetIsatomicMatch invocations whose target schema equals the named at-uri.
conditionActionSubsumedByatomicMatch invocations whose use.action is subsumed by the named slug in the named action vocabulary.
conditionPurposeSubsumedByatomicMatch invocations whose use.purpose is subsumed by the named slug.
conditionDataHasatomicMatch invocations whose data carries the named community-defined property identifier (e.g. length>1024, contains-pii, multilingual).
conditionAndcombinatorPop the top two predicates and conjoin them.
conditionOrcombinatorPop the top two predicates and disjoin them.
conditionNotcombinatorPop 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:

  1. Querying the verifier registry for verification records on each lens in the path.
  2. Confirming that each requiredVerification is covered by an accepted record (signed by a trusted verifier, with result: "holds").
  3. 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:

SubfieldTypeNotes
modestringShort failure-mode identifier (e.g. loses-dialect-markers).
affectsarray of stringsDotted paths or field names the caveat applies to.
severityenuminfo / 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"
}
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

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

FieldTypeRequiredNotes
encounterencounterRefyesThe encounter being retrospected.
findingobjectyes{ kind, kindVocab?, detail, evidence? }.
latencyinteger (seconds)nodetectedAt - encounter.occurredAt. Precomputed for aggregation.
detectingPartydidyesDID of the party that detected the issue.
confidencenumber ∈ [0, 1]noOptional confidence score.
disputedAttributionbooleannoThe detecting party's hint that the causal claim may be contested.
basisbasisnoStructured grounding.
detectedAtdatetimeyesWhen the issue was detected.
occurredAtdatetimeyesWhen this retrospection record was published.

finding

SubfieldTypeRequiredNotes
kindopen enumyesmerge-divergence / data-loss / reconciliation-mismatch / other.
kindVocabvocabRefnoVocab the kind slug resolves against.
detailstring (≤8000 graphemes)yesNarrative detail.
evidenceunion of evidenceDivergence / evidenceLoss / evidenceMismatchnoStructured 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

SlugEvidence shapeWhat it captures
merge-divergenceevidenceDivergence (paths A and B + witness input)Two translation paths that should have converged but produced different outputs.
data-lossevidenceLoss (source field path + target schema + witness input)A source-schema field unrepresented in the target after translation.
reconciliation-mismatchevidenceMismatch (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

FieldTypeRequiredNotes
lenslensRefyesThe lens whose property is being asserted.
kindopen enumyesroundtrip-test / property-test / formal-proof / conformance-test / static-check / convergence-preserving / coercion-law.
kindVocabvocabRefnoVocab the kind slug resolves against.
verifierdidyesDID of the party asserting the verification.
tooltoolyesTool identity and version.
propertyunion of seven lensProperty shapesyesStructured statement of what is being asserted.
resultopen enumyesholds / falsified / inconclusive.
resultVocabvocabRefnoVocab the result slug resolves against.
counterexamplecid-linknoFor result: falsified: minimal counterexample.
dependenciesarray of at-urinoOther verifications this one depends on (e.g. a proof assuming a lemma).
proofArtifactcid-linknoFor kind: formal-proof: checkable proof artifact (Coq / Lean / Agda term).
basisbasisnoStructured grounding when relevant.
occurredAtdatetimeyesWhen 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.

KindProperty shapeWhat the runner does
roundtrip-testlpRoundtrip (domain string + optional generator URI)Run put(get(a)) == a on samples drawn from the domain.
property-testlpGenerator (spec + runner identifier + seed)Run an arbitrary boolean predicate over generator samples.
formal-prooflpTheorem (statement in proof-system syntax + system + free variables)Check the proof artifact in the named system.
conformance-testlpConformance (standard identifier + version + clause subset)Run the standard's conformance suite.
static-checklpChecker (checker identifier + ruleset + version)Run the checker against the lens chain.
convergence-preservinglpConvergence (property + optional step bound)Verify the property is preserved under repeated application (fixpoint, reconciliation).
coercion-lawlpCoercionLaw (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

SlugMeaning
holdsThe runner did not falsify the property within its budget.
falsifiedThe runner found a counterexample.
inconclusiveThe 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"
}

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

dev.idiolect.vocab

A community-published vocabulary. Two compatible shapes are supported:

  1. The legacy single-relation tree (actions + top + world), where every entry declares its direct subsumers and the world pins the inference discipline.
  2. 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

FieldTypeRequiredNotes
namestring (≤128)yesHuman-readable name.
descriptionstring (≤4000 graphemes)noNarrative description.
worldenumyesclosed-with-default / open / hierarchy-closed. Default subsumption discipline.
defaultRelationat-urinoPointer to the relation-kind node consumers should treat as canonical when no relation is specified.
topstring (≤256)noIdentifier of the vocabulary's top action under subsumed_by. Required when actions is populated and world=closed-with-default.
actionsarray (≤4096) of actionEntrynoLegacy tree shape.
nodesarray (≤4096) of vocabNodenoGraph shape.
edgesarray (≤16384) of vocabEdgenoGraph shape.
supersedesat-urinoPrior vocabulary this one replaces.
occurredAtdatetimeyesPublication timestamp.

The world discipline

A closed-enum field controlling how undeclared identifiers are treated:

SlugBehavior
closed-with-defaultEvery undeclared id is subsumed by top and nothing else. The natural default for hierarchical taxonomies.
openUndeclared ids are incomparable to every declared id. Right when the vocab is one community's view of an open-ended space.
hierarchy-closedOnly 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)

FieldTypeRequiredNotes
idstring (≤256)yesStable action identifier.
parentsarray of stringsyesDirect subsumers. Empty for top.
classstring (≤256)noIdentifier of the attitudinal composition this action instances.
descriptionstring (≤500 graphemes)noOptional human-readable description.

The vocabNode (graph form)

FieldTypeRequiredNotes
idstring (≤256)yesStable slug. Used as edge endpoint.
kindopen enumnoconcept / relation / instance / type / collection.
kindVocabvocabRefnoVocab the kind slug resolves against.
subkindUriat-urinoPointer to another node typing this node's subkind.
labelstring (≤500)noPrimary human-readable label (SKOS prefLabel).
alternateLabelsarray (≤50)noSKOS altLabel.
hiddenLabelsarray (≤50)noSKOS hiddenLabel: searchable, not displayed.
descriptionstring (≤2000 graphemes)noSKOS definition.
scopeNotestring (≤2000 graphemes)noSKOS scopeNote: usage guidance.
examplestring (≤2000 graphemes)noSKOS example.
historyNotestring (≤2000 graphemes)noSKOS historyNote.
editorialNotestring (≤2000 graphemes)noSKOS editorialNote.
changeNotestring (≤2000 graphemes)noSKOS changeNote.
notationstring (≤500)noSKOS notation: non-text identifier.
externalIdsarray (≤20) of externalIdnoMappings to external knowledge bases.
statusopen enumnoproposed / provisional / established / deprecated.
relationMetadatarelationMetadatanoOWL 2 property characteristics. Required for kind: relation.

vocabNode.kind

SlugMeaning
conceptA SKOS concept; the typical case.
relationThis node represents a relation type. Edges with this slug reference this node.
instanceAn individual; a specific entity.
typeA metaclass; a type-of-types.
collectionA SKOS Collection. Members linked via member_of edges.

The vocabEdge

FieldTypeRequiredNotes
sourcestringyesSource node id.
targetstringyesTarget node id.
relationSlugstringyesRelation slug. References a relation-kind node.
metadataunknownnoEdge metadata, free-form.

OWL property characteristics

A relation-kind node carries metadata declaring algebraic properties:

PropertyMeaning
symmetric
asymmetric
transitive
reflexive for all in scope
irreflexive for all in scope
functional
inverseFunctional
inverseOfPointer to the inverse relation node.
worldPer-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:

FieldSKOS counterpart
labelprefLabel
alternateLabelsaltLabel
hiddenLabelshiddenLabel
descriptiondefinition
scopeNotescopeNote
exampleexample
historyNotehistoryNote
editorialNoteeditorialNote
changeNotechangeNote
notationnotation
externalIds[]exactMatch / closeMatch / broadMatch / narrowMatch / relatedMatch

External-id mappings

Each externalId carries:

SubfieldTypeNotes
systemopen enumKnowledge-base identifier (wikidata, ror, orcid, lcsh, ...).
systemVocabvocabRefVocab the system slug resolves against.
identifierstringIdentifier in that system.
uriuriOptional direct URI for the external entity.
matchTypeopen enumexact / close / broader / narrower / related.
matchTypeVocabvocabRefVocab 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 elementGraph element
actionEntryvocabNode { id, kind: "concept" }
Each parent in actionEntry.parentsvocabEdge { source = entry.id, target = parent, relationSlug: "subsumed_by" }
actionEntry.class (when present)vocabEdge { source = entry.id, target = class, relationSlug: "instance_of" }
topThe 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

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):

CommandCalls
idiolect orchestrator adapters --framework <NAME>GET /v1/adapters?framework=...
idiolect orchestrator bountiesGET /v1/bounties/open
idiolect orchestrator bounties --requester_did <DID>GET /v1/bounties/by-requester?requester_did=...
idiolect orchestrator recommendationsGET /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

PathReturns
GET /healthz200 OK if the process is alive.
GET /readyz200 OK once the catalog has caught up.
GET /metricsPrometheus exposition.
GET /v1/statsPer-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 pathxrpc pathReturns
GET /v1/bounties/open/xrpc/dev.idiolect.query.openBountiesBounties whose status is open, claimed, or unset.
GET /v1/bounties/want-lens?source_uri=...&target_uri=.../xrpc/dev.idiolect.query.bountiesForWantLensBounties requesting a lens for the schema pair.
GET /v1/bounties/by-requester?requester_did=.../xrpc/dev.idiolect.query.bountiesByRequesterBounties by requester DID.
GET /v1/adapters?framework=.../xrpc/dev.idiolect.query.adaptersForFrameworkAdapters declared for a framework.
GET /v1/adapters/by-invocation-protocol?kind=.../xrpc/dev.idiolect.query.adaptersByInvocationProtocolAdapters by invocation-protocol kind.
GET /v1/adapters/with-verification/xrpc/dev.idiolect.query.adaptersWithVerificationAdapters that carry a verification reference.
GET /v1/recommendations/xrpc/dev.idiolect.query.recommendationsStartingFromRecommendations starting from a given source schema.
GET /v1/verifications?lens_uri=.../xrpc/dev.idiolect.query.verificationsForLensVerifications for a specific lens.
GET /v1/verifications/by-kind?kind=.../xrpc/dev.idiolect.query.verificationsByKindVerifications by kind.
GET /v1/communities?member_did=.../xrpc/dev.idiolect.query.communitiesForMemberCommunities for a member DID.
GET /v1/communities/by-name?name=.../xrpc/dev.idiolect.query.communitiesByNameCommunities by case-insensitive name.
GET /v1/dialects/for-community?community_uri=.../xrpc/dev.idiolect.query.dialectsForCommunityDialects owned by a community.
GET /v1/beliefs/about?subject_uri=.../xrpc/dev.idiolect.query.beliefsAboutRecordBeliefs whose subject is a given record.
GET /v1/beliefs/by-holder?holder_did=.../xrpc/dev.idiolect.query.beliefsByHolderBeliefs by holder DID.
GET /v1/vocabularies/by-world?world=.../xrpc/dev.idiolect.query.vocabulariesWithWorldVocabularies declared with a given world.
GET /v1/vocabularies/by-name?name=.../xrpc/dev.idiolect.query.vocabulariesByNameVocabularies 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 v1 prefix 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 in dev.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 --check verifies that relationship.

What changes at 1.0

  • Breaking changes between minor versions stop. Breaking changes ship in major versions only.
  • The lexicon-evolution check-compat gate flips from advisory to a hard fail.
  • The HTTP API's v1 prefix becomes a stability commitment. New endpoints are additive.
  • Trait signatures in idiolect-records, idiolect-lens, idiolect-indexer, and idiolect-orchestrator become 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:

BucketContents
AddedNew features.
ChangedBehavior changes; trait surface tightenings; lexicon shape changes.
DeprecatedFeatures that still work but are scheduled for removal.
RemovedFeatures that are gone.
FixedBug fixes for behavior introduced in earlier versions.
SecuritySecurity-relevant fixes.

The Changelog is in CHANGELOG.md.

Compatibility matrix

ComponentSource of truthLock at
idiolect-recordscrates.ioexact version
@idiolect-dev/schemanpmexact version
panproto cratesGit tagv0.72.0 for idiolect 0.12.1
idiolect CLIbinary release on GitHubrelease tag
idiolect-orchestrator containerghcr.io/idiolect-dev/orchestratorimage SHA
idiolect-observer containerghcr.io/idiolect-dev/observerimage 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.