DisclosureOS documentation · v2 Beta
Materials and laboratory
Follow a specimen from collection through preparation and testing, with results tied to the portion actually analyzed.
A specimen is a physical object. A laboratory result describes an analysis of a particular specimen or portion. The report is a separate source file, and a review records what someone concludes from it. Keep those identities connected without treating them as interchangeable.
This guide follows a fictional parent sample split into two portions. Only portion A is analyzed. You will check the saved documents, inspect three forms of result and try assigning one to the untested portion B.
Choose the workflow you need:
| Task | Availability | Start here |
|---|---|---|
| Describe specimens, preparation, analyses, results and attributed reviews | Published in npm 2.0.0-beta.4 | Laboratory walkthrough |
| Check collected-specimen handoff declarations and cited record bytes | Published profile, using claim history 0.1.0 | Source and custody check |
These are separate checks. The laboratory evaluator can resolve a saved specimen selection, but does not run the collected-specimen profile. Its claim_history version 0.5.0 cannot be substituted for the profile's history 0.1.0.
1. Run the laboratory example
Requirements: Node.js 22 and npm. Install the published APIs, then download the five files from the retained example revision:
mkdir disclosureos-laboratory-beta4
cd disclosureos-laboratory-beta4
npm init -y
npm install --save-exact @disclosureos/records@2.0.0-beta.4 @disclosureos/schema@2.0.0-beta.4
for file in observation.json entities.json selection.json history.json run.mjs; do
curl --fail --location \
"https://raw.githubusercontent.com/disclosurefoundation/disclosureos/5a686c14b002aacb614a505ba74217f73fff63dc/examples/v2/laboratory-review-demo/$file" \
--output "$file" || break
done
node run.mjsThe runner prints a full report. Read these fields first:
{
"success": true,
"checks": {
"structural": "passed",
"semantic": "passed",
"profile": "not_checked",
"external": "passed"
},
"sourceArtifactIntegrity": "not_checked",
"scientificInterpretation": "not_checked"
}The supplied JSON snapshots and their connections pass. The source reports and physical specimens have not been verified. The example includes report declarations with a placeholder digest, not report bytes. Do not reuse that digest for a real file.
| File | What it holds |
|---|---|
observation.json | Sources, methods and a numerical measurement with its unit and uncertainty state. |
entities.json | The parent sample, two portions, preparation, custody actions, analysis and three results. |
selection.json | Selected specimen and handoff declarations referenced by the parent sample. |
history.json | The embedded observation, exact snapshot references, source wording and a separate review. |
To use the APIs yourself, save this as check-laboratory.mjs in your npm project:
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import {
parseLaboratoryEntities,
parseLaboratoryClaimHistory,
} from "@disclosureos/records/experimental/v2";
import { evaluateLaboratoryClaimHistory } from "@disclosureos/schema/experimental/v2";
const read = (name) => readFileSync(name);
const entities = JSON.parse(read("entities.json").toString("utf8"));
const history = JSON.parse(read("history.json").toString("utf8"));
const local = [
parseLaboratoryEntities(entities),
parseLaboratoryClaimHistory(history),
];
if (local.some((result) => !result.success)) {
console.error(
JSON.stringify(
local.map((result) => result.issues),
null,
2
)
);
process.exitCode = 1;
} else {
const documents = new Map(
["observation.json", "entities.json", "selection.json"].map((name) => {
const bytes = read(name);
return [createHash("sha256").update(bytes).digest("hex"), bytes];
})
);
const result = await evaluateLaboratoryClaimHistory(history, { documents });
console.log(
JSON.stringify(
{
success: result.success,
checks: result.checks,
sourceArtifactIntegrity: result.sourceArtifactIntegrity,
scientificInterpretation: result.scientificInterpretation,
},
null,
2
)
);
if (!result.success) {
console.error(JSON.stringify(result.issues, null, 2));
process.exitCode = 1;
}
}Run node check-laboratory.mjs. It prints the compact result above. Local parsers check document structure and local relationships. The evaluator additionally checks supplied snapshot bytes, identities and connections, including selected specimen and transfer references. It does not fetch URLs or inspect report contents.
2. Follow the tested portion
Open entities.json. The preparation split connects the parent to both resulting portions:
| Entry | Connection | Result scope |
|---|---|---|
parent | Collected specimen and input to split | No laboratory results assigned |
aliquot-a | Derived portion produced by split; input to analysis-a | Three results explicitly assigned |
aliquot-b | Another portion produced by split | No analysis or results assigned |
trace | Separately described trace | Not a collected specimen or a tested portion |
Each laboratory_result names both analysisId and materialId. The analysis lists its inputMaterialIds and resultIds. These connections must agree. A result for A does not automatically describe the parent, B or the trace.
An aliquot is a portion taken from a larger sample. The example records a split, not proof that either portion represents the whole. The parent's handling declarations also do not fill the explicit custody gap for A after preparation.
3. Try assigning a result to the wrong portion
Save this as break-result-scope.mjs in the same folder. It changes only the in-memory copy:
import { readFileSync } from "node:fs";
import { parseLaboratoryEntities } from "@disclosureos/records/experimental/v2";
const entities = JSON.parse(readFileSync("entities.json", "utf8"));
const resultEntry = entities.entities.find((entity) => entity.id === "numeric");
resultEntry.materialId = "aliquot-b";
const result = parseLaboratoryEntities(entities);
console.log(
JSON.stringify({ success: result.success, issues: result.issues }, null, 2)
);
if (!result.success) process.exitCode = 1;Run node break-result-scope.mjs. It exits with status 1 and prints:
{
"success": false,
"issues": [
{
"code": "LAB.RESULT_SCOPE",
"pointer": "/entities/9",
"message": "A result must belong to its analysis and one exact input specimen; parent and sibling results are not interchangeable.",
"stage": "semantic"
}
]
}B exists, but is not an input to this analysis. That is a relationship error, not an invalid ID format. Run node check-laboratory.mjs again to confirm the original files still pass.
Describe materials and preparation
Use research_entities version 0.4.0 for laboratory records. Its field reference leads with six material and laboratory types and retains the shared witness and document types.
| Entity | Use it for |
|---|---|
material_trace | A reported mark or effect, including one that was never sampled. |
material | A specimen's label, descriptions, collection, storage and declared lineage. |
material_preparation | A split, extraction, mixture or other preparation connecting input and output specimens. |
material_custody_action | A specimen's handling action, predecessor or explicitly unknown earlier history. |
laboratory_analysis | Tested specimens, preparation references, method, laboratory, operators and linked results. |
laboratory_result | One analysis's result for one input specimen, with supporting report references. |
To assemble your own document:
- Put sources, methods and numerical measurements in the Observation inventory. Preserve real access declarations and available file digests.
- Reference the exact observation snapshot from
observationRef. IncludecontextRefswhen declarations use event or setting records. - Assign a distinct material ID to each portion you need to describe. Record lineage and preparation inputs and outputs explicitly. Keep a reported trace separate from a collected object.
- Record handling actions for the relevant specimen. Use predecessor links to express the declared sequence, including gaps. Array position is not the declared custody order.
- Add an analysis with its tested material IDs, preparation IDs and explicit result IDs. An empty result list can describe reported analytical work with no supplied results.
- Give each result its analysis and exact tested specimen. Add source citations, keeping report contents separate from your review.
Fields retain attributed assertions and missing-information states. Conflicting source descriptions can coexist; the parser does not choose a preferred account. When you change saved snapshot bytes, update the references that identify those bytes instead of retaining their old digest.
Preserve what the laboratory reported
The three result forms convey different information:
| Form | Fictional example | How to represent it |
|---|---|---|
quantitative | 1.25 mg/kg; uncertainty not reported | Reference the Observation measurement with measurementId. Declare a reported limit, an unknown limit or a reason it does not apply. |
below_limit | Below a reported detection threshold of 0.1 mg/kg | Retain the threshold, unit, limit kind and basis. This is not a measurement of zero. |
qualitative | “Gray particulate matter observed.” | Preserve the report's wording without inventing a numerical value. |
The evaluator checks measurement references and requires an explicitly reported quantitative limit's unit to match the referenced measurement's unit. It does not convert units, establish method performance or infer scientific significance. Unknown uncertainty remains unknown.
Use claim_history version 0.5.0 for the connected review. reportedMaterialReview preserves source wording about quality or origin. A separate assessment's materialReview records the reviewer's findings, limitations and selected inputs. In this example the reviewer keeps the outcome inconclusive because uncertainty and aliquot custody are incomplete. That review is distinct from the source's “field collected” description.
Run the published evaluator
For a collected-specimen source-and-custody check, use the separate synthetic provenance download. Extract it into a different folder and follow its installation instructions. That archive pins npm 2.0.0-beta.4. Do not pass the laboratory history into it.
Save this as check-custody.mjs in the extracted provenance folder, then run node check-custody.mjs:
import { readFileSync } from "node:fs";
import { evaluatePhysicalSamples } from "@disclosureos/schema/experimental/v2";
const read = (name) => readFileSync(`physical-samples/${name}`);
const result = await evaluatePhysicalSamples(
JSON.parse(read("history.json").toString("utf8")),
JSON.parse(read("selection.json").toString("utf8")),
{ assets: new Map([["source:report", new Uint8Array(read("custody.txt"))]]) }
);
console.log(result.samples, result.checks, result.custodyAuthenticity);The supplied example passes structural, semantic, profile and external. custodyAuthenticity remains not_checked. The shared exercise provides the runnable JavaScript runner and complete output.
Describe the selection
The sample-provenance reference describes physical_sample_selection version 0.1.0. Each entry has a local id, lineage, label, collection declaration, custody declaration and current custodian. Known collection and holder declarations cite a recordRef; unknown declarations retain a reason. catalogIdentifier is optional.
The schema export is @disclosureos/schema/experimental/v2/physical-samples/schema. Its identifier is urn:disclosureos:experimental:physical-sample-selection:0.1.0.
Derived samples and mixtures can be represented but fail this collected-specimen profile's applicability. Do not relabel a processed sample to obtain a pass.
What custody continuity means here
Here, the transfer array is the declared handoff sequence. Each entry names an ID, from, to and recordRef. The first sender must match the collector, each subsequent sender the previous recipient, and the final recipient the current custodian. Matching uses exact strings. Self-transfers fail; a connected return is allowed.
A documented empty transfer list can pass when collector and current holder agree. It must not replace missing history: use unknown custody for that. With incomplete endpoints, a conflict-free sequence remains unchecked. This profile has no transfer timestamps; it cannot establish chronology. Collection time and place must not be inferred from observation metadata.
Check the records, preserve the limits
Each cited record must resolve to a source in the history with a declared SHA-256 digest. Supply the nonempty local bytes under its source reference. The evaluator compares bytes; it neither fetches URLs nor reads the record to establish its meaning.
Missing required declarations, conflicting handoffs and mismatching bytes fail. Unavailable bytes leave an otherwise eligible profile unchecked. Reusing one source does not create independent corroboration.
Neither workflow certifies physical identity, actual custody, complete chronology, contamination control, composition or origin. A passing check does not establish scientific eligibility, assessment support or redistribution rights. Access labels do not remove private information from your records; review what you publish separately.
Field reference
- Material and laboratory fields: choose Specimens & laboratory, Sources & reviews or Sample provenance, then search within that document. All three are published in Beta 4. Sample provenance uses a separate collected-specimen profile.
- Explore materials in the showcase: inspect the fuller fictional example case. Its source packet is separate from this focused laboratory exercise; use the showcase implementation guide to reproduce that packet.