Guarantees#
What the infrastructure promises, and the interfaces you can build against — three contracts on one page, each for a different reader. Building a platform or registry on BattINFO: start with the infrastructure contract. Integrating validation into a pipeline: the validation contract. Feeding bulk data in from a folder of files: the ingest manifest contract.
The infrastructure contract#
This page is for the person deciding whether to build on BattINFO: what the guarantees are, where they are enforced, and what breaks loudly instead of silently. Everything here is backed by tests or CI gates in this repo or the registry.
Records are versioned#
Every canonical record carries schema_version (currently 0.2.0, a single
module-level constant). The registry’s publish gate validates against pinned,
vendored copies of the same schemas and flags unknown versions rather than
guessing. Changes to record shape are CHANGELOG entries, never silent.
Three versions exist in the wild, and consumers should accept all three:
|
What it means |
|---|---|
|
The original record shape; records published before the 0.2.0 consolidation still carry it |
|
An interim stamp used briefly before the numbering was consolidated |
|
The current shape: snake_case keys throughout, |
New records always stamp the current version. The value names a record shape, not a revision of the schemas: records of all three versions are checked against the one set of schemas served under https://w3id.org/battinfo/schema/, and those schemas changed in place before 0.8.0, as the erratum to section 11.1 of the identifier policy lists. So a record written before 0.8.0 does not always validate against the 0.8.0 schemas. The schemas no longer take specs or sameAs, so a 0.1.0 record that uses them fails (some other older spellings, such as legalName and co_type, are still accepted), and so does a record with text in a quantity’s value or a date string that is not YYYY, YYYY-MM or YYYY-MM-DD. battinfo’s record models still accept specs and sameAs as input and write the current names, and its readers move text in value to value_text, so reading such a record with battinfo and saving it again fixes both; a date string in another form has to be rewritten. From the v0.8.0 tag the schemas served at each URL no longer change, apart from their descriptions; what a new schema_version value will say about the schemas is to be defined in 0.9.
Records are attributable#
Every emitted record’s provenance block carries battinfo_version — the
library build that wrote it. A malformed record found in a corpus two years
from now is forensically traceable to its producer. Explicitly set values are
preserved, so re-serialising another build’s record never falsifies origin.
Identifiers are deterministic#
Records saved without an explicit uid mint their IRI on your machine from the
record’s natural identity key, through one primitive (entities.stable_uid)
and the shared seed builders in battinfo.entities. A cell spec’s key is its
manufacturer, model, format, chemistry and size code; a cell’s is its spec and
its serial number (or name); a test’s is its cell, kind, protocol and run.
Free text in a key is normalised first (surrounding and repeated whitespace, a
no-break space and a decomposed accent change nothing, case does), and a
chemistry counts by its vocabulary key, so Li-ion and LI-ION are one cell
spec. Consequences you can rely on:
re-running an identical ingest is a no-op, never a duplicate corpus;
running the same
ws.load()orws.add()call again in one session, as a notebook does when a cell is re-run, reuses the record the session already holds instead of adding a copy;an IRI a record already has (pinned, imported, or given by an earlier save, in this session or an earlier one in the same folder, a cell spec or test saved before 0.8.0 included) never moves when a record with the same identity fields joins: the newcomer is minted from its content. A record saved by an earlier session is recognised by its content, so one you edit in the same save in which such a newcomer joins is minted from its content too; pin it;
a test’s IRI names one run on one cell, told apart by its protocol and its first data file (or, for a test without data, a start time you pass, else the name you give it), so a later run with its own data, start time or name never takes an earlier run’s IRI, and renaming a test with data or a start time keeps its IRI; a title BattINFO generates (
C01 cycling) never enters, and a save refuses to let a test with none of these replace a saved run with another instrument or time window;the workspace, its engine, a save without an id, and the
save_*andcreate_*functions for cell specs, cells, test specs, tests without data files, materials, electrodes, channels, parameter sets and organizations mint identical IRIs for identical identities, and so do the workspace, its engine andsave_datasetfor datasets, the publication package for cell specs and cells, and the batch path for a cell with a serial number (withws.add("cell")only when the batch cell has no batch id) (tests/test_idempotent_minting.py);tests/test_golden_iris.pyfails if any path’s IRI moves without a changelog entry;records with no distinguishing identity still mint randomly — two anonymous but physically distinct cells never silently merge.
Some paths are outside this until 0.9, which moves every minting path onto
battinfo.entities and keeps the IRIs they minted in 0.8 findable (section 5
of the identifier policy):
the component and equipment builders (
create_component_spec,create_component_instance, their per-family wrappers,create_equipment_specandcreate_equipment) give a record withoutuid=orid=a random IRI, a new one on every call;battinfo batch initandbattinfo batch addgive a cell without a serial number a random IRI, recorded in itsbattinfo.yaml;the interop importers (BattInfoConverter, the solid-state database, Discovery and the Battery Data Commons bridge) mint from their own seeds: stable when the same source is imported again, but not the IRIs the workspace gives the same records;
the publication package and
battinfo batch packageseed a dataset from its folder and title, not its file, so its dataset IRI differs from the workspace’s, and the publication package does the same for the run of a test with data;battinfo batch packageseeds a test from its raw file’s path in the cell folder, so it matches the workspace’s test only when the workspace’s data file has that path in the workspace folder;save_test()and a save without an id never see a data file, so they give a test with data another IRI than the workspace does, and a save without an id leaves a dataset’s cell and test out of its seed.
The known limitations say what to do on each of these paths.
Governance of the IRI space itself is written down in the identifier policy.
One schema contract, three consumers#
The canonical JSON Schemas in src/battinfo/data/schemas/ are enforced in
three places, each with a CI gate against drift:
Consumer |
Mechanism |
Drift gate |
|---|---|---|
|
jsonschema at save/publish |
the test suite |
Registry publish gate |
vendored schemas, fails closed on unknown record types |
|
vendored schemas compiled with Ajv |
web CI: sync check + 103-record agreement corpus |
The same record gets the same structural verdict in all three.
Bulk operations are safe to automate#
bulk_save_sessioningests ~400 records/s; a 10k-record ingest takes under half a minute and is re-runnable without duplicates.ws.submit()journals every outcome and resumes interrupted batches, re-sending only what is missing; transient registry failures retry with exponential backoff.Submission content conflicts return a structured 409 naming the existing record, not a bare string.
A record whose IRI another publisher already holds comes back from
ws.submit()as aconflictoutcome that names the IRI and that publisher and says how to rename or pin the record. It is not retried as a new version.
Deprecations are announced#
Public API removals go through one release of DeprecationWarning naming the
replacement — never straight to ImportError. The policy lives in
CONTRIBUTING;
expiring shims are swept at each release.
Validation contract#
This document defines the validation behavior that consumers can rely on for the supported BattINFO scope.
Supported Entry Points#
Core validation entry points:
battinfo validatebattinfo save ... --validatebattinfo publish ... --validatebattinfo index build --validatebattinfo.validate.validate_json(...)battinfo.validate.validate_record(...)battinfo.validate.validate_publication(...)
Policy Names#
BattINFO currently exposes these named validation policies:
defaultstrictpublisheringest
Policy semantics:
default: schema and references are errors; semantic issues default to warnings.strict: schema, references, and semantic issues are errors.publisher: publisher-oriented validation with semantic and publication checks as errors. A deposit graph that the SHACL publication shapes could not check is an error too, so it is never published unchecked.ingest: schema remains strict while semantic and reference issues can remain warnings during staged cleanup.
Issue Model#
Structured validation issues expose these fields:
codeseveritypathmessagehintvalidatorresource_typeprofile
Severity values:
errorwarning
Representative issue codes:
schema.requiredschema.format.urischema.format.date_timeschema.profile_unknownreference.missingreference.type_mismatchsemantic.short_id_mismatchsemantic.temporal_order_invalidsemantic.electrode_holders_mixed(withsemantic.electrode_role_expected/semantic.electrode_polarity_expected: the electrode holders disagree with the cell configuration — see Electrodes)semantic.chemistry_electrode_conflict(the negative electrode basis contradicts the chemistry, such as lithium metal in a fullli-ioncell or graphite in ali-metalone; see the chemistry labels in the property keys reference)semantic.percent_basis_mismatch(a warning under every policy: a fraction’s percent unit states the other basis, such asvol%on amass_fractionorwt%on avolume_fractionor aporosity; see the units in the property keys reference)semantic.method_step_unmapped(a warning under every policy: no published EMMO process class fits a test-protocol step, such as a constant-resistance charge or a cr or cccv step with no direction, so the JSON-LD export gives it none; see Tests)semantic.handle_kind_word_expected(a warning under every policy: the record’shandledoes not end with its type’s kind word; see Name your records)semantic.schema_version_supersededandsemantic.schema_version_unknown(warnings under every policy: the record’sschema_versionis one an earlier release wrote,0.1.0or1.0.0, or one this release does not know at all; the current version is0.2.0)semantic.timestamp_implausible(a warning under every policy: a*_atUnix time outside 2000 to 2100) andsemantic.timestamp_out_of_range(a*_atnumber that cannot be a Unix time in seconds, because it falls outside the years 1 to 9999 as a time in milliseconds does; an error under the strict and publisher policies, a warning under the others)jsonld.type_term_unknown(an error under every policy: a bare@typethat the document’s own@contextdoes not define as a class, so it would expand to a machine-local IRI;record_to_jsonldraises on it for every record type)publication.distribution_url_invalidjsonld.parse_error(the JSON-LD could not be parsed into an RDF dataset)publication.shacl_unavailable,publication.shapes_not_foundandpublication.shacl_runtime_error(the SHACL publication shapes could not check a deposit graph because pyshacl could not be imported, the shapes file is missing or pyshacl failed: an error under thepublisherpolicy, whichws.zenodo()applies before it mints a DOI, and a warning under the others)
CLI Contract#
battinfo validate supports:
--format text--format json
Text mode:
emits a human-readable pass/fail summary
includes warnings when validation succeeds with non-fatal issues
JSON mode emits a machine-readable payload with:
okmodepolicyprofilesource_rootissue_counterror_countwarning_counterrorsissues
Example issue object:
{
"code": "schema.format.uri",
"severity": "error",
"path": "dataset.url",
"message": "'not-a-uri' is not a 'uri'",
"hint": null,
"validator": "format",
"resource_type": "dataset",
"profile": null
}
Exit codes:
0when no error-severity issues are present1when one or more error-severity issues are present
Current Boundary#
Reference validation is supported against repository-style source trees through source_root.
Every *_id field on a record body resolves to a record of one expected type, and a reference that resolves to the wrong type is reference.type_mismatch rather than a pass. The distinctions the checks enforce are meaningful ones: cell_instance.cell_spec_id must be a cell-spec, while cell_instance.working_electrode_id / counter_electrode_id must be electrode records — the built batch, not the electrode-spec design, which the cell spec already links.
This is sufficient for the core scope, but it is not yet the long-term scalability model for larger external registries or snapshots.
Ingest manifest contract#
battinfo.ingest.json is the folder-local manifest for the battinfo ingest ...
workflow.
Purpose:
declare the typed subject being ingested
point that subject to a curated reusable type record
provide the minimum publication metadata needed for repeatable ingest
configure folder scanning rules for attached files
Normative schema: assets/schemas/ingest-manifest.schema.json
Packaged runtime copy: src/battinfo/data/schemas/ingest-manifest.schema.json
Scope#
This manifest is not the source of truth for reusable type definitions.
Use it for:
one operational ingest folder
one typed instance-like resource
file discovery and publication defaults
Do not use it for:
curated
cell-specauthoringediting canonical registry output
replacing the source folder itself
Required fields#
resource_typecurrent allowed value:
cell-instance
type_recordpath to the curated reusable type record
Optional fields#
resource_iripreserved canonical IRI for the ingested resource when already assigned
resource_namehuman-facing instance label used as the default generated serial/name
workspace_idregistry workspace id used during bundle/publish
publisher_idpublisher id used during bundle/publish
source_versionsubmission version string written into generated workspace/package state
licensedefault dataset license
rulesfile discovery and filename-to-test-kind inference overrides
Rules object#
rules currently supports:
photo_globstring or string array
default:
image/photo/*.jpgimage/photo/*.jpegimage/photo/*.png
timeseries_globstring or string array
default:
timeseries/raw/*.csv
test_kind_from_filenameobject mapping lowercase filename tokens to BattINFO test kinds
default:
rate -> rate_capabilityici -> icicapacity -> capacity_check
Minimal example#
{
"resource_type": "cell-instance",
"type_record": "battinfo-records/records/cell-spec/google--g20m7--2025/record.json"
}
Practical example#
{
"resource_type": "cell-instance",
"type_record": "battinfo-records/records/cell-spec/google--g20m7--2025/record.json",
"resource_iri": "https://w3id.org/battinfo/cell/15qn-rpd4-xhy7-kx2q",
"resource_name": "google--g20m7--2025--15qnrp",
"workspace_id": "google-g20m7-instance-demo",
"publisher_id": "demo-lab",
"source_version": "2026-04-10",
"license": "CC-BY-4.0",
"rules": {
"photo_glob": [
"image/photo/*.jpg",
"image/photo/*.jpeg",
"image/photo/*.png"
],
"timeseries_glob": [
"timeseries/raw/*.csv"
],
"test_kind_from_filename": {
"rate": "rate_capability",
"ici": "ici",
"capacity": "capacity_check"
}
}
}
Operational rule#
For routine use:
users edit the ingest folder and
battinfo.ingest.jsonBattINFO generates the
Workspaceusers do not treat the generated workspace as the primary long-term source by default
Current implementation boundary#
The manifest contract is designed to expand to other typed resource kinds, but the current ingest engine only implements:
cell-instance
Additional resource_type values should only be added when the ingest engine and
its downstream record-generation logic actually support them.