Skip to content

Cards schema reference

The canonical wire format for Lemma cards is defined as a single JSON Schema document at schema/card.v0.1.json. This page summarises the shape of every variant; the schema file itself is the authority.

IdentifierValue
$idhttps://openlemma.dev/schema/card.v0.1.json
Spec versionJSON Schema 2020-12
Discriminatortop-level kind field
LicenceMIT

A card is a oneOf over four variants — principle, ops, hypothesis, unidentified. Every variant is closed (additionalProperties: false); unknown fields fail validation.


PrincipleCard

The structural shape used for physics, chemistry, biology, climate, mathematics, engineering, and numerical methods. The discriminator is structural, not subject-area — the actual domain lives in the domain field.

FieldTypeRequiredNotes
kindconst "principle"yesVariant discriminator
idstring, kebab-caseyesPattern ^[a-z][a-z0-9-]*$. Stable and never reused once published.
versionsemver stringyesMAJOR for breaking convention changes, MINOR for added limits, PATCH for typos / refs.
namestringyesHuman-readable title.
domainstringnoSubject area, e.g. "physics-condensed-matter", "chemistry-electrochemistry". Conventional values are listed in the schema.
principlesstring arrayyesThe underlying physical / mathematical principles in plain English.
formulaTeXstringyesCanonical equation in LaTeX. KaTeX-renderable.
conventionsstring arrayyesSign conventions, unit conventions, coordinate frames.
expectedLimitsstring arrayyesPlain-English asymptotic / boundary expectations.
referencesstring arrayyesCanonical sources — textbooks, papers, official documentation.
validationEnvelopesobjectnoNumerical bounds asserted at runtime. Keys are domain-conventional (e.g. gEarth_m_per_s2); values are either [min, max] tuples or richer objects.
crossMethodTolerancesobjectnoHow far two independent methods may disagree on the same observable before the disagreement is a finding. See Cross-method agreement.
formulaMachineFormulanoMachine-readable counterpart to formulaTeX — see MachineFormula.
seriesConditionsarraynoConditions every reported sample must satisfy — see seriesConditions.
convergenceobjectnoQualifies how a claimed convergence order is measured — see convergence.
metadataCardMetadatanoAuthorship and review tier. Stripped before the card reaches a model — see Card metadata.

MachineFormula

formulaTeX is written for a human reader and carries presentational markup; it is not something a program can evaluate. formula is the machine-readable counterpart. Both are kept, because they serve different readers.

FieldTypeRequiredNotes
exprstringyesPlain-ASCII expression, e.g. "y0 + v0*t - (1/2)*g*t**2". Same grammar as the dimensional check’s expr, so the corpus has one expression language rather than two.
symbolsobjectyesEvery free symbol in expr, mapped to its DimVec.
relationstringyesWhat the expression actually encodes, in plain English.

relation is required, and the reason is worth stating plainly: expr need not be a transcription of formulaTeX. A card may state the integrated form for a human and the differential form for a machine. free-fall-uniform-gravity does exactly that — its formulaTeX is the position form y(t), its expr is the net force -m*g. Both describe the same physics at different levels of integration. Without relation, a consumer would assume they are the same relation and compare two expressions that were never meant to match.

A card must declare formula before a hypothesis card’s limit claim can name it as the thing it reduces to.

seriesConditions

Conditions every reported sample of a quantity must satisfy.

"seriesConditions": [
{
"of": "g",
"op": ">=",
"value": 0,
"basis": "a density of states counts levels per unit energy, so it is non-negative by definition"
},
{
"of": "imChi0",
"op": "<=",
"value": 0,
"where": { "of": "omega", "op": ">", "value": 0 },
"basis": "passivity: a system cannot supply energy to the perturbation driving it"
}
]
FieldTypeRequiredNotes
ofstringyesWhich reported quantity this constrains.
op< <= > >=yes
valuenumberyes
where{of, op, value}noRestricts which samples the claim covers.
basisstringnoWhy the condition holds.

This is not a small validationEnvelopes. An envelope bounds a magnitude; these bound a sign or relation, which can be universal where a magnitude is not. That is why cards deliberately carrying no envelope can still carry these — density-of-states and joint-dos are both envelope refusals, because a DOS scales with cell volume and band count, yet neither can ever be negative.

where is load-bearing, not a convenience. Im chi_0(omega > 0) <= 0 says nothing about negative frequencies, where Im chi_0 is legitimately positive. Without the restriction the check would report a violation the card never asserted — turning correct physics into a failure.

Conditions are structured rather than written as expressions on purpose: an expression language would need a computer-algebra system to evaluate, which would make the check Python-only like symbolic verification. Every such claim in the corpus fits this shape without algebra.

convergence

Qualifies how a claimed order of accuracy is measured.

"convergence": {
"maxPerLevelSpread": 0.4,
"basis": "matches the ±0.2 acceptance width on this card's observedConvergenceOrder envelope"
}
FieldTypeRequiredNotes
orderKeystringnoWhich validationEnvelopes key holds the expected order. Defaults to observedConvergenceOrder.
maxPerLevelSpreadnumber > 0noHow far per-level orders may spread before the study stops supporting an estimate at all.
basisstringnoWhy this number.

maxPerLevelSpread exists because it decides verdicts — it separates “this refinement study is not a clean power law” (warn) from “the method converges at the wrong rate” (fail). It began as a constant inside the engine, which put a judgement in the engine that everywhere else in Lemma lives in the card with a recorded reason.

Omit it and a documented default applies — but the verdict then says so, naming the source of the number that decided the outcome (declared by <card>, supplied by the caller, or the engine default, not declared by this card). A fallback is fine; a silent fallback is not.


OpsCard

Parameterised templates for scripting and job-submission tasks (SLURM, Snakemake, Singularity recipes).

FieldTypeRequiredNotes
kindconst "ops"yesVariant discriminator
idstring, kebab-caseyesSame pattern as PrincipleCard.id
versionsemver stringyes
namestringyes
descriptionstringyesOne-paragraph plain-English summary.
parametersarray of OpsParameteryesEach entry: { key, label, defaultValue, required, note? }
validationstring arrayyesValidation rules in plain English.
referencesstring arrayyesSources.
metadataCardMetadatanoAuthorship and review tier. Stripped before the card reaches a model — see Card metadata.

OpsParameter

FieldTypeRequiredNotes
keystringyesIdentifier used by the template engine.
labelstringyesHuman-readable label.
defaultValuestringyesDefault; empty string allowed.
requiredbooleanyesWhether the template requires this parameter at instantiation.
notestringnoFree-form explanation, rendered alongside the label.

HypothesisCard

Proposed extensions to the corpus — generated by an LLM, by a human, or by symbolic regression. Explicitly not yet verified; the cross-check engine consumes the declared checks to decide whether the hypothesis can be promoted to a principle.

FieldTypeRequiredNotes
kindconst "hypothesis"yes
idstring, kebab-caseyes
versionsemver stringyes
namestringyes
proposalstringyesPlain-English statement of what the hypothesis claims.
proposedFormulaTeXstringyesThe proposed equation in LaTeX.
derivedFromobjectno{ cardId: string, relationship: "extends" | "replaces" | "complements" }
checksHypothesisChecksSpecyesThe cross-checks the engine should run.
referencesstring arrayyes
originenumyes"llm", "human", or "symbolic-regression".
rationalestringnoWhy this hypothesis was proposed.
metadataCardMetadatanoAuthorship and review tier. Stripped before the card reaches a model — see Card metadata.

HypothesisChecksSpec

FieldTypeNotes
dimensionalobjectlhsLabel/lhsDims/rhsLabel/rhsDims (canonical DimVecs). Optionally expr (a plain-ASCII RHS expression, e.g. "(1/2)*m*v**2") + symbols (map of symbol → DimVec): when both are present the engine derives the RHS dimensions from the formula and checks them against lhsDims, instead of trusting the declared rhsDims.
limitsarrayEach entry: { name, regime, expectedReducesTo } in prose, plus an optional machine form — see Machine-readable claims. Without a machine form the claim is recorded and returns warn.
conservationLawsarrayEach entry: { law, statement }, plus an optional evolution block — see Machine-readable claims. law names the quantity and is free-form; conventional values are energy, momentum, charge, particle-number, total-spin, parity, mass, probability, lyapunov-function.
referenceCorpusobject{ mustAgreeWith?: string[], mayContradict?: string[] }. The engine resolves every id against the live corpus.

DimVec

Canonical dimension vector. Each axis is an integer exponent on a primitive dimension. Omitted axes default to zero.

AxisMeaningNotes
Llength
Ttime
Mmass
Eenergy
Qcharge
ThetatemperatureRenders as Greek capital Θ in human output.
Nparticle count

Machine-readable claims

A hypothesis card states its limit and conservation claims in prose, for a human. It may additionally state them in a form a program can evaluate. Prose alone is recorded and returns warn — the engine confirms the claim is well-formed and says so honestly, rather than implying it was tested.

Discharging these claims requires the symbolic adapter, which is opt-in and Python-only. The fields below are part of the schema regardless, so a card carrying them is valid and portable whether or not anything in the pipeline evaluates them.

Limit claims

limits[] originally held one field, regime, describing “the thing that happens in this limit”. In practice cards were using it for four different operations, which give different answers:

FieldOperationExample
limit{ symbol, to } — a genuine limiting process. to takes 0, oo, -oo, or a numeric literal.b -> 0
substitutemap of symbol → expression — setting a variabley = 0
solveFor{ symbol } — a root: the value at which the expression vanishesterminal velocity is the v where net force is zero
fixedPoint{ system, solveFor, parameters?, conditions? } — a stationary point of a coupled systemthe coexistence equilibrium of a predator-prey model

Exactly one may be declared. A claim carrying two is ambiguous rather than richer, and the engine says so instead of guessing.

The distinctions are not pedantry. A substitution and a limit disagree wherever the function is discontinuous at the point. A root is not a limit at all. And a fixed point of a two-equation system cannot be found from one equation — which is why fixedPoint declares its own system rather than assuming the card’s single formula is the whole story.

target says what the claim reduces to, in one of three forms:

FormMeaning
{ cardId }another corpus card, which must then declare a formula
{ expr }an inline expression
{ point }map of symbol → expression, for a fixedPoint — an equilibrium is a tuple, not a single value

conditions — which fixed point

A coupled system usually has several stationary points, and a predator-prey model is exactly as stationary at extinction as at coexistence. Verifying stationarity alone would therefore accept (0, 0) for a claim about coexistence. conditions is the list of inequalities that select the intended branch:

"conditions": ["y > 0", "gamma/delta < K"]

A condition that is demonstrably false refutes the claim. One that cannot be decided without sign assumptions on the parameters leaves the stationarity verdict standing and is reported as unverified — an undecidable side condition must not retract a result that was actually established.

parameters

Names the block introduces beyond the card’s own declared symbols. Declared rather than inferred, because several ordinary physics parameter names — gamma, beta, zeta, lambda, pi — collide with functions and constants in the algebra system. An unbound collision does not necessarily raise; pi*r**2 parses cleanly as 3.14159…, silently computing a different expression than the card declared. Nothing here is guessed.

Conservation claims

conservationLaws[].evolution describes how a declared quantity changes along the system’s dynamics:

FieldTypeRequiredNotes
quantitystringyesThe conserved or dissipated quantity, e.g. total mechanical energy.
systemarrayyesEach entry { of, expr } — the time derivative of a state variable.
ratestringyesThe claimed time derivative of quantity. "0" means genuinely conserved.
parametersstring arraynoAs above.

law is free-form, like domain. It was a closed list of six physics values until the corpus demonstrated the problem: lotka-volterra-with-logistic-prey is a population-dynamics card whose claim is about a Lyapunov function, and the enum left it no option but to declare law: "energy" — after which its own statement opened “No conserved energy-like quantity.” A vocabulary that forces a card to mislabel itself is not a constraint worth keeping. Use a conventional value where one fits; invent one where none does.

Note that the check compares against a declared rate rather than testing whether the derivative vanishes. That is deliberate: both cards in the public corpus that carry a conservation law assert a quantity is not conserved, at a specific rate — mechanical energy under linear drag (dE/dt = -b v²), and a Lyapunov function under logistic predation. A checker that only asked “does this vanish” would have covered neither.


Cross-method agreement

validationEnvelopes bounds what a single run may produce. crossMethodTolerances sits one relation higher: it bounds how far two independent methods may disagree with each other.

Keys match the output keys engines emit, exactly as validationEnvelopes does.

"crossMethodTolerances": {
"latticeConstant_A": {
"relative": 0.01,
"gating": true,
"basis": "the scale at which independent all-electron and pseudopotential codes are held to agree in the Delta-gauge reproducibility studies"
},
"bulkModulus_GPa": {
"absolute": 25.0,
"gating": false,
"basis": "reported for information only — B_0 depends on curvature and is dominated by basis quality"
}
}
FieldTypeRequiredNotes
relativenumber > 0one ofMaximum fractional spread, about the mean.
absolutenumber > 0one ofMaximum spread in the observable’s own unit.
gatingbooleannoWhether disagreement fails the verdict. Defaults to true; set false for observables you report but know to be method-sensitive.
basisstringnoWhy this number — the citation or reasoning behind it.

Exactly one of relative / absolute per observable. Declaring both would leave which one binds ambiguous.

For each key reported by at least two methods, the spread (max − min) is tested against the tolerance. A single method raises rather than passing: it cannot corroborate itself.

basis exists because a bare number is unreviewable. A tolerance with no recorded provenance cannot be argued with later — when a run fails it, nobody can say whether the bound or the engine was wrong. The same gap still exists on validationEnvelopes, which has nowhere structured to record it.

The comparator is available as a library function in both SDKs (runAgreementChecks / run_agreement_checks). It is deliberately not exposed as an MCP tool.


UnidentifiedCard

Sentinel returned by the IDENTIFY phase when no card honestly matches the request. Surfaced verbatim instead of fabricating a fallback.

FieldValueNotes
kindconst "unidentified"
idconst "none"
versionconst "0.0.0"
namestring
reasonstringWhy nothing matched.

Card metadata

Every card variant may carry an optional metadata block recording who wrote the card and how far it has been reviewed. It is additive: cards without it remain valid.

FieldTypeRequiredNotes
authorsarray of AuthoryesAt least one. Every card credits a human.
tierenumno"bronze", "silver", or "gold" — the review level reached.
createdstringnoDate the card was first written.
updatedstringnoDate of the last substantive change.

Author

FieldTypeRequiredNotes
namestringyes
orcidstringnoPreferred stable identifier for academic authors.
githubstringno
roleenumno"author", "curator", "reviewer", "translator", or "maintainer".

Why it is separated from the card body

cards_get removes metadata before returning a card. A model asking the substrate for a principle receives the physics and nothing about who wrote it. That is deliberate, and there are four reasons:

  • Authority bias. A verification substrate should not encourage a model to weight a claim by the prestige of its author. The card is correct or it is not; who curated it is irrelevant to that judgement.
  • Privacy. Author names, ORCIDs and affiliations would otherwise be sent to a third-party model provider on every single tool call.
  • Token budget. Metadata is dead weight in a context window that is paying for physics.
  • Training-data hygiene. Anything that reaches a model can end up in a future training corpus. Author identity is not a physical property and should not become a learned feature.

Metadata is for humans, attribution tooling, and the review process — read it from the card files or the Python SDK, not from the MCP tool surface.

"metadata": {
"authors": [
{ "name": "Ada Lovelace", "orcid": "0000-0000-0000-0000", "role": "curator" }
],
"tier": "silver"
}

Validating a card

Terminal window
npx ajv-cli@5 validate --spec=draft2020 \
-s schema/card.v0.1.json \
-d "cards/**/*.json"

or in Python:

from artano_lemma import validate_card_payload, parse_card
# Strict validation against the schema:
validate_card_payload(my_card_dict)
# Or validate + parse to a typed model in one call:
card = parse_card(my_card_dict)

See the Python SDK page for the typed models and the validator.