hachure
v0.18.0
Published
Hachure — canonical distribution of the open trust format: normative JSON schemas, conformance test vectors, spec constants, and a bundled dependency-free implementation (status derivation, merge, CLI).
Maintainers
Readme
Hachure — an open trust format
Namespace: hachure.org/v1
Status: pre-1.0, hard versioning, no compatibility promises yet
Originally developed by: Kontour AI
Install
npm i hachureThe package ships the normative JSON schemas, conformance test vectors, the
statusFunctionVersion constant that ties implementations to a specific
algorithm revision, and a bundled, dependency-free implementation of status
derivation and merge — so you can produce, validate, merge, and evaluate
Hachure records with nothing but this package:
import { deriveStatuses, mergeBundles, canonicalize } from 'hachure';
const merged = mergeBundles([bundleFromScanner, bundleFromCI]);
const statusByClaimId = deriveStatuses(merged, new Date());Or from the command line:
npx hachure validate bundle.json # schema-validate a TrustBundle (needs ajv)
npx hachure derive bundle.json # check, then derive per-claim statuses
npx hachure diff before.json after.json # check both, then status transitions as evidence arrives
npx hachure merge a.json b.json # merge producer bundles
npx hachure vectors # run the conformance vectorsThe library functions do not validate their input, and some guarantees hold
only for schema-valid bundles (see
status-function.md). The
derive and diff commands therefore check a bundle before deriving and exit
1 on one that fails:
- They always run a built-in check, also exported as
checkBasisInvariants(bundle): inconclusive evidence iscitedwith nopassing, and theschemaVersion9evidence fields are declared. - When
ajvcan be loaded they also run full schema validation. This package deliberately does not depend onajv. It is picked up when it is resolvable from this package's own install location, that is, installed into the samenode_modulesashachure(npm i hachure ajv). When it is not,deriveanddiffstill run, after the built-in check, and warn on stderr that full validation was skipped. --no-validateskips both checks and prints a warning.
Worked example. conformance/sf-reference-bundle-snapshot.json is a
{ now, input, expect } vector fixture; write its input bundle to a file and
derive it at the vector's own now:
node -e "const v=require('./conformance/sf-reference-bundle-snapshot.json'); \
require('fs').writeFileSync('/tmp/bundle.json', JSON.stringify(v.input))"
npx hachure derive /tmp/bundle.json --now "2026-06-10T00:00:00.000Z"{
"statusFunctionVersion": "4",
"evaluatedAt": "2026-06-10T00:00:00.000Z",
"statusByClaimId": {
"claim.repo-governance.api-proof": "verified",
"claim.field-attested-records.registration-status": "stale",
"claim.fact-resolution.w2-wages": "verified",
"claim.roadmap.future-service": "unknown"
}
}claim.field-attested-records.registration-status folds to stale because its
verification event's freshness window (per its verificationPolicyId) has
expired by the given now — the staleness step of the fold in
status-function.md, not a missing-evidence gap.
The prose specification is normative; the bundled code is a conforming implementation of it (proven in-repo by running every conformance vector), not a privileged one.
Claiming conformance: run the test vectors from testVectors against your
implementation (testVectors covers the status-derivation vectors; the L3 merge
vectors ship separately under conformance/merge/ and via the
./conformance/*.json export path). For each vector, call your status-derivation function with
vector.input and vector.now, then assert that the derived status for every
claim ID matches vector.expect.statusByClaimId. A vector that carries a
statusFunctionVersions array applies only to the versions it lists; one
without it applies to every version. Passing every vector that applies to a
given status function version is the bar for a conforming implementation.
import { testVectors, statusFunctionVersion } from 'hachure';
for (const { name, vector } of testVectors) {
if (vector.statusFunctionVersions && !vector.statusFunctionVersions.includes(statusFunctionVersion)) continue;
const results = deriveStatuses(vector.input, new Date(vector.now));
for (const [claimId, expected] of Object.entries(vector.expect.statusByClaimId)) {
assert.equal(results[claimId], expected, `${name} / ${claimId}`);
}
}What this is
Hachure is an open format for portable trust state. It defines how claims about real-world subjects — and the evidence, policies, verification events, authority records, and derivation rules behind them — are represented so they can cross product and vendor boundaries without the receiver needing access to the producer's internals.
Hachures are the short strokes on hand-drawn maps that show the shape and steepness of terrain. This format does the same for trust: it shows the contours of what is supported, what is stale, what is disputed, and what is simply asserted.
The format is deliberately not named after any company or product, and depends on
no vendor's software: the hachure package alone produces, validates, merges, and
evaluates records. Known conforming implementations are listed under
Implementations; anyone can add one by passing the conformance
vectors.
Governance intent: Hachure is currently developed by Kontour AI, which holds the name to protect it. We intend to move the specification to neutral governance as adoption warrants.
Why this exists
Every system that verifies anything — CI pipelines, security scanners, compliance reviews, data-quality checks, human sign-offs — stores its conclusion in its own database. The moment that conclusion crosses a boundary (vendor to customer, tool to dashboard, agent to deployment gate), it degrades into a boolean, a badge, or a PDF: the evidence is gone, there is no expiry, and the receiver has no way to re-check the reasoning. You either trust the summary or redo the work.
The standards world has solved portable attestation — signed, frozen, point-in-time statements (SCITT receipts, RATS passports, in-toto/DSSE envelopes, Verifiable Credentials). What none of them standardize is the step after: every one of them delegates "what is this claim worth now?" to verifier-specific judgment. SCITT leaves registration policy to each transparency-service operator; EAT (RFC 9711) states its verifier rules "are a matter of local policy"; VC 2.0 says verification "does not imply evaluation of the truth of claims." The status decision is always someone's unpublished local policy.
Hachure's core move is to publish that function. A claim travels with its
evidence, the policy it was judged against, and the append-only event history —
and the status is not an opinion stored in a field, it is a pure, versioned
function of that data (status = f(claim, evidence, events, policy, authorityTrace,
now)). Any receiver can recompute it, watch it go stale on its own as evidence
ages, and merge bundles from producers that disagree without one silently
overwriting the other: conflicts are preserved as contradiction gaps, never
resolved by last-write-wins. Not portable trust — recomputable trust.
The defaults are deliberately honest about trust: an unsigned bundle is a valid bundle (Assurance L0), because most trust state inside an organization never needed a signature — it needed structure. Signing is a dial you turn up (assurance.md) when records cross a boundary where identity matters.
Where you might use it
- AI agent gates. An agent (or CI job) may act only when specific claims are
verifiedand fresh: express the gate as a DerivationRule ("deploy allowed iftest-suite-passesandsecurity-scan-cleanare both inacceptedStatuses: [verified]"), and record every decision as an InquiryRecord — an audit receipt that says exactly what was knowable, from which claims, under whichstatusFunctionVersion, at the moment the agent acted. This is the difference between "the agent said it checked" and a replayable record of what it checked. - Vendor assurance without the PDF. Instead of a static compliance answer, a
vendor publishes a TrustBundle and serves the
verification endpoint. The customer re-derives
statuses themselves; when the pen-test evidence passes its policy's validity
window, the claim goes
staleon the customer's side automatically — no annual-questionnaire lag. - Release provenance with living status. Signatures (in-toto, SLSA) freeze what was true at signing time. Wrap a bundle in a DSSE envelope (interop-in-toto.md) to anchor the release moment, then keep serving event deltas so a consumer can see that a claim verified at release has since been disputed or revoked. hachure.org's own /trust page runs this pattern live.
- Merging scanners that disagree. Two security tools scan the same artifact
and reach different conclusions. Merge both bundles (merge.md):
both claims survive under their producers, the disagreement surfaces as a
contradictiontransparency gap, and a human (or an authority-gated resolution event) settles it on the record instead of the louder tool winning. - Third-party enrichment. An external feed (a vulnerability database, a
license scanner, an end-of-life tracker) watches subjects it knows about and
emits its own bundles about them — ordinary Evidence and claims under its own
producerId, merged into the ledger like any producer. This is the certifier pattern that aggregation services (e.g. GUAC) run inside a deployed server, expressed as portable records instead: the enrichment travels with the state, and any consumer re-derives the effect.
Namespace and versioning
All core trust-format records use the hachure.org/v1 namespace. Producers
that define extension records outside this specification use their own
product-scoped namespaces (a domain the producer controls), never
hachure.org/*.
Pre-1.0: the format uses hard breaking changes rather than compatibility aliases.
No forward or backward compatibility guarantees are made across versions. Version
bumps are reflected in schemaVersion (an integer field in TrustBundle, currently
9) and in the status function version (a string exported by this package and by
every conforming implementation as statusFunctionVersion, currently "4").
Schema version 4 adds optional claim freshness fields (expiresAt /
ttlSeconds) and an optional invalidation event vocabulary (event status:
"revoked" and event type: "invalidation"). All additions are optional, so
every bundle valid at schemaVersion 3 remains valid; only the deriver
(statusFunctionVersion 2) folds the new fields into a status. See
status-function.md and the sf-expired-window / sf-revoked-event /
sf-no-freshness-fields conformance vectors.
Schema version 5 renames the Claim surface field to facet and makes it
optional (previously required) — the one deliberate hard break named above.
Bundles declaring schemaVersion 2 through 4 are no longer schema-valid
under this release: their surface field is rejected by claim.schema.json's
additionalProperties: false. Producers MUST re-emit as facet and self-declare
schemaVersion: 5. See merge.md §4 for facet's (unchanged) treatment in claim
identity.
Schema version 6 adds an optional TrustBundle proof block (an object holding
integrity anchors — e.g. a transparency_log anchor with a Rekor entry UUID),
resolving the previous contradiction where assurance.md,
interop-in-toto.md, and
verification-endpoint.md referenced a proof field
the schema rejected. The addition is optional: every bundle valid at
schemaVersion 5 remains valid (the schema enum accepts both 5 and 6), and
proof never changes status derivation — signing remains an out-of-band
assurance concern.
Schema version 7 adds the runtime_observation Evidence type and an optional
execution.environment field (test, staging, or production). The addition
is additive: every bundle valid at schemaVersion 6 remains valid. The status
function's existing required-evidence step consults runtime_observation when a
policy requires it, so older validators reject bundles that use the new value;
statusFunctionVersion remains "2" because no fold step changed.
Schema version 8 adds an optional conclusionConfidence.calibration reference
(tableRef, tableVersion, and optional method, sampleSize,
boundMethod) naming the versioned calibration table that produced
conclusionConfidence.value (see ai-evaluation.md). A
bundle declaring schemaVersion 8 MUST carry calibration whenever value
is present, and its interval bounds MUST lie in [0, 1]; hachure validate
also checks low <= high and low <= value <= high for such bundles, which
JSON Schema cannot express. Bundles at 5–7 validate exactly as before (all
of this is a SHOULD there). The status function never reads
conclusionConfidence.
Schema version 9 adds two optional Evidence fields that record how an item
was obtained: inconclusive (the attempt to collect it could not run) and
collectedByKind (human, deterministic, or model). See
§Evidence. The addition is additive: every bundle valid at
schemaVersion 8 remains valid. A bundle that uses either field MUST declare
schemaVersion 9, and the schema rejects them under a lower declared
version; a producer SHOULD declare 9 only when it uses one of them, because
older validators reject both the new properties and the value 9 itself. A
consequence is that one producer's bundles can carry different declared
versions, and merge.md §5 refuses to merge bundles whose
schemaVersion values differ. A consumer that merges such bundles restamps
the lower ones to 9 first; a bundle valid at 8 is valid at 9 unchanged,
and a bundle at 5–7 is valid at 9 once it meets the version 8
conclusionConfidence rules. Neither field is a status-function input, so
statusFunctionVersion did not change for it. The sf-inconclusive-evidence vector
checks, for every version, that an inconclusive item is ignored
wherever the fold reads evidence, and sf-basis-fields-inert checks that
statuses are identical with and without the new fields.
Status function version "3" makes omission fail closed: a claim with no
resolvable (or no non-empty) verification policy derives at most proposed,
check evidence satisfies a requirement only with passing: true, an
unevaluable validity rule derives stale, a blocking failure is checked before
policy requirements, and the derivation ceiling uses a reconciled status
ordering. Version "2" remains defined and the bundled implementation
evaluates it on request ({ statusFunctionVersion: "2" }, or
hachure derive --status-function-version 2). No schema change is involved.
See status-function.md §"Migrating from version 2" for
exactly which bundles change status, and the sf-v3-* conformance vectors.
Status function version "4" is the current version and the default of the
bundled implementation and the CLI. It defines a timestamp as an RFC 3339
date-time, compares instants exactly to any number of fractional digits, and makes times that cannot be evaluated fail closed in the
authority step: a dispute resolution is not honoured when its own createdAt
is not a timestamp, or when every trace for its actor has a revokedAt,
validFrom or validUntil that is present but is not one; and a blocking
failure whose observedAt is not a timestamp is not set aside by a
resolution. Under version "3" each of those failed open. Values that
Date.parse accepted but RFC 3339 does not (a date with no time, a time with
no offset, hour 24) are no longer read as times, and a leap second now is.
Validity windows are exact as well (durationDays: 0.7 is exactly
60 480 000 ms), and a now given as a string, including --now, must be a
timestamp. A schema-valid bundle derives the same under "3" and "4" if every time in
it is an RFC 3339 date-time with an offset, at most three fractional digits
and no leap second, and now is not within one millisecond of the end of a
validity window.
A status can become stronger as well as weaker under "4": refusing a
resolution to rejected lets a later verified event stand. Versions "3"
and "2" remain defined, unchanged, and selectable
({ statusFunctionVersion: "3" }, or hachure derive
--status-function-version 3). No schema change is involved. See
status-function.md §"Migrating from version 3" and the
sf-v4-* conformance vectors.
Conformance language
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT,
SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this
document and in every other normative document in this repository
(merge.md, assurance.md, verification-endpoint.md,
status-function.md, interop-in-toto.md, evidence-ingestion.md,
scitt.md, oscal.md, ai-evaluation.md, contract-claims.md, waivers.md,
basis-annotations.md, SECURITY.md) are to be
interpreted as described in RFC 2119
and clarified by RFC 8174 (BCP 14),
only when they appear in all capitals, as shown here.
Scope: core record shapes
This specification covers the following record types. Each is a first-class concept in the format; none requires a specific producer or product to instantiate.
TrustBundle
The central wire record. A portable, point-in-time package of trust state from a
single producer: claims, evidence, policies, verification events, and optional identity
links, claim groups, authority traces, and a proof block (signing anchors — see
assurance.md).
Plain-language definition:
A Trust Bundle is a portable, point-in-time package of trust state from a single producer — claims, the evidence and verification events behind them, and the policies the producer played by — packed so it can cross a product boundary without the receiver needing access to the producer's internals.
The source field identifies the producer (free-text, may vary per run); an optional
producerId field carries a stable, unsigned identifier for the producing system,
consistent across every bundle it emits. When present, producerId MUST be a
non-empty string. Bundles from multiple producers can be merged
into one ledger without last-write-wins and without deleting losing evidence; conflicts
between claims are surfaced as contradiction transparency gaps, never silently
resolved or used to flip a claim's status. The full specification of identifier
conventions and the merge algorithm is in merge.md.
An optional identityLinks array declares co-referent subjects — real-world entities
known under more than one identifier. Each link carries a stable optional id, a
subjects array (two or more { subjectType, subjectId } refs), and an optional
relation field: "equivalent" (default — the subjects denote the same entity),
"subsumes" (the first subject is a superset of the others), or "converts" (the
subjects are related by a unit or scale transformation, parameterised by an optional
conversion: { factor, offset, note } object). A link may additionally carry a
mappingClaimId pointing to the Claim that evidences the mapping assertion itself;
when set, inquiry resolution through that link is subject to a weakest-link status cap —
a disputed mapping claim cannot yield a verified answer.
Claim
An assertion about a real-world subject. A claim has a stable id, a subjectType
and subjectId pair identifying what is being asserted, an optional facet (a
producer-defined grouping or namespace for the claim — see merge.md §4 for why
it's excluded from cross-producer claim identity), a claimType, a
fieldOrBehavior, and a value. Claims carry optional impactLevel, integrity
anchors, policy references, derivation edges, and confidence basis metadata, and
an optional conclusionConfidence whose value is written only by a calibrator
and names its calibration table in calibration (see
ai-evaluation.md).
Derived trust status is never stored on the claim itself as source of truth; it is computed from the surrounding bundle at evaluation time.
Claims also carry two optional round-trip fields, tolerated but never producer-authored:
producerStatus (the producer's own declared status, present when a TrustReport's
derived claims are re-fed as bundle input) and freshness ({ asOf, expiresAt?,
stale }, a freshness stamp on derived/report claims).
Evidence
An item of support for a claim. Evidence is linked to a claim via claimId. Each
item carries evidenceType, method, sourceRef, an excerpt or summary, and
observedAt. Evidence can declare a passing boolean and a blocking flag; a
non-passing, non-blocked evidence item is a soft signal; a non-passing blocking
item can cause a disputed status outcome.
supportStrength (default "entails") distinguishes full entailment from citation:
only "entails" evidence feeds policy requirement checks. "cited" evidence is
contextual but does not satisfy required-evidence policies.
Use test_output for results produced in an isolated or pre-deployment test
context, runtime_observation for behavior observed from a running system (in
any environment — record which via execution.environment), and
crawl_observation for facts extracted by crawling an external resource. When
evidence has an execution block, its
optional environment (test, staging, or production) records where that
execution occurred; policies that specifically require live/deployed evidence
SHOULD require runtime_observation rather than inferring it from method or
execution metadata.
Inconclusive evidence
An attempt to collect evidence that could not run is recorded with the optional
inconclusive object (schema version 9):
{
"id": "ev-9", "claimId": "claim.api.p95",
"evidenceType": "runtime_observation", "method": "monitoring",
"supportStrength": "cited",
"sourceRef": "https://metrics.example/p95",
"excerptOrSummary": "Metrics endpoint returned 503",
"observedAt": "2026-09-26T10:00:00Z", "collectedBy": "ci-probe",
"inconclusive": { "reason": "unreachable", "detail": "HTTP 503 after 3 retries" }
}reason is one of unreachable, tool_error, permission_denied, timeout,
or other; detail is an optional string containing at least one
non-whitespace character (whitespace as the ECMAScript regular expression
class \s defines it, so a no-break space alone does not count) and is
REQUIRED when reason is other. The set is closed: a cause it does not name (a rate limit,
for example) is recorded as other with a detail.
- An inconclusive item MUST set
supportStrength: "cited"and MUST NOT carrypassing. The Evidence schema enforces both. Because cited evidence is dropped before the fold, an inconclusive item satisfies no policy requirement, does not corroborate, and cannot dispute a claim: every claim derives the status it would have if the item were absent. This holds for schema-valid bundles only: status derivation does not validate its input, so a caller MUST validate a bundle against the schemas before deriving status from it. - Only an explicit
inconclusiveobject means "could not run".execution.isError: trueon its own, andpassing: false, both mean the check ran and failed. A producer MUST NOT write a check that could not run aspassing: false: an absentblockingcounts as blocking, so that would turn a verified claimdisputed.execution.isErrorMAY accompanyinconclusive;inconclusivethen takes precedence in display. - A consumer MAY show a claim-level "could not check" label when a claim has at least one inconclusive item and no entailing evidence. That is distinct from "never checked" (no evidence) and from "failed".
Collector kind
The optional collectedByKind (schema version 9) records what kind of
collector produced an item: human, deterministic (a parser, test runner, or
probe), or model. collectedBy remains the collector's identity; the kind is
never encoded in it.
- The field is descriptive only. The status function does not read it and no policy field refers to it; a policy that excludes evidence by collector kind would be a status-function change and is not defined.
- When the value is
"model", the producer SHOULD also setmetadata.collectorModelto{ "name": "<model>", "version": "<version>" }. This names the model that collected the item, and is separate from the AI-evaluation profile'smetadata.model, which names the model under evaluation. - A person's review of model output is separate
attestationevidence withcollectedByKind: "human". There is no combined value. - Absent means not declared. A consumer MUST NOT infer
humanordeterministicfrom a missing field.
VerificationPolicy
A policy declares what evidence and methods are required to reach verified status
for a given claimType, and how long verification remains valid. Core fields:
requiredEvidence (array of evidence types), requiredMethods, requiresCorroboration,
validityRule (one of duration, commit, historical, manual), and
acceptanceCriteria.
Policies are resolved against claims by verificationPolicyId first, then by
claimType exact match, then by walking the parentType chain from most-specific
to most-general. See Status Derivation for how the resolved
policy feeds the derivation.
VerificationEvent
An append-only event representing a status decision for a claim. Events carry
claimId, status, actor, method, evidenceIds, and timestamps. Events are
never updated; they accumulate as a ledger. The most recent event of a given kind
shapes the derived status via the fold described in Status Derivation.
A verification event may carry resolvesDispute: true and an authorityRef to
indicate it is an authority-gated dispute-resolution decision (see
status-function.md Step 1).
AuthorityTrace
A record establishing that a named actor held a named authority over a subject during
a time window. Authority traces are the credential that makes a dispute-resolution
event binding: the fold checks that the resolution event's actor has an active trace
at the decision timestamp. Fields: actorRef, authorityType, authorityRef,
validFrom, validUntil, revokedAt, and optional integrity anchors.
InquiryRecord
An append-only record capturing the resolution of a consumer-side question (Inquiry)
against the ledger. An InquiryRecord carries the original question, the
resolution path (matched claim or named derivation rule plus input claims), the answer
with its status at evaluation time, a frozen snapshot of input claim statuses, the
statusFunctionVersion used, and the resolvedAt timestamp.
Records never go stale because they never assert present-tense truth; they assert what
was knowable at a specific moment. The statusFunctionVersion field enables
re-evaluation if the derivation algorithm changes.
DerivationRule
A named, versioned rule that derives a boolean answer from existing claims.
Rules compose claims using value predicates (eq, neq, gt, gte, lt, lte, in, exists)
and status predicates (acceptedStatuses), combined with "all" or "any" — the
portable expression of gate-style checks ("proceed only if these claims hold these
statuses"). The weakest-link confidence ceiling propagates through rule evaluation
unchanged.
Status semantics
Status is a pure, versioned function of the bundle data and a now timestamp. The
full specification of the derivation algorithm is in status-function.md.
The nine possible statuses:
| Status | Meaning |
|---|---|
| unknown | No supporting evidence or events; the claim cannot be evaluated. |
| proposed | Evidence exists or a verification event indicates proposed, but policy requirements are not fully met. |
| assumed | The claim is treated as true for operational purposes without full verification evidence. |
| verified | A verification event asserts verified, required policy evidence is present, and the verification is still fresh. |
| stale | The most recent verified event has expired under the policy's validity rule. |
| disputed | A verified claim has blocking contradicting evidence, or a terminal dispute event exists. |
| superseded | A terminal event marks the claim as superseded. |
| rejected | A terminal event marks the claim as rejected. |
| revoked | An explicit invalidation event has revoked the claim's verification. For single-claim status derivation this folds to stale (see status-function.md, Step 2) unless a later verification event re-asserts the claim; the reference implementation still tracks revoked as a distinct, weakest-ranked raw status for Claim.status/VerificationEvent.status, claim-group rollups, and weakest-link ordering. |
Normative schemas
The JSON schemas at schemas/ are the normative wire contracts for
all core record shapes. The following schema files are part of this format:
| Schema file | Record type(s) |
|---|---|
| trust-bundle.schema.json | TrustBundle (top-level container) |
| claim.schema.json | Claim |
| evidence.schema.json | Evidence |
| verification-policy.schema.json | VerificationPolicy |
| verification-event.schema.json | VerificationEvent |
| trust-report.schema.json | TrustReport (derived, not emitted by producers) |
| derivation-rule.schema.json | DerivationRule |
| inquiry-record.schema.json | InquiryRecord |
Schemas are not duplicated in this directory. The reference implementation
validates TrustBundle input against these schemas via validateTrustBundle().
Profiles
The core specification covers record shapes and status semantics. Profiles are optional, independently adoptable conventions for interop and transport. Adopting a profile requires no changes to core record shapes or the status function.
| Profile | File | What it covers |
|---|---|---|
| in-toto interop | interop-in-toto.md | Wrapping a TrustBundle as a signed in-toto Statement v1 / DSSE envelope (export direction). |
| Evidence ingestion | evidence-ingestion.md | Importing existing attestations — in-toto/SLSA, EAT, SCITT statements, VCs — as Evidence on Hachure claims (import direction). |
| SCITT | scitt.md | Registering bundles on transparency services (RFC 9943): receipts as proof anchors, and the status function as a published registration/appraisal policy. |
| OSCAL | oscal.md | Assessment-results mapping: observation↔Evidence, finding↔Claim, result-expiry↔expiresAt; projection of derived state into OSCAL AR documents. |
| Verification endpoint | verification-endpoint.md | Producer-served HTTP endpoint for receivers to fetch post-export event deltas. |
| Assurance | assurance.md | Signing as a dial: L0/L1/L2 assurance levels, identity presentation, consumer policy, and human signing ceremony. |
| AI evaluation | ai-evaluation.md | Eval results, model claims, and agent outcomes as recomputable trust: eval evidence that survives the boundary, calibrated conclusionConfidence + comfort-zone, conclusion-freshness vs signature-freshness, composing with model-signing / AI-BOMs / DID-VC as evidence. |
| Contract claims | contract-claims.md | End-to-end contracts between providers and consumers: a contract claim qualifier convention plus live runtime_observation receipts that keep test-only integration assertions at a gap. |
| Waivers | waivers.md | Typed claim.metadata.waiver shape for documenting an accepted assumed gap (reason/approver/timestamp), with no status-function or schema change. |
| Basis annotations | basis-annotations.md | How a status was established, for display: evidence.metadata.sourceOfRecord (a reference to an AuthorityTrace, with resolution rules) and claim.metadata.estimate (basis and optional bounds), with no status-function or schema change. |
Relationship to W3C Verifiable Credentials
Hachure and the W3C Verifiable Credentials data model both represent claims-with-evidence, and it is a fair question why this format does not simply build on VC instead of defining its own record shapes.
The short answer: DID-based issuer identity is the dominant convention
in the VC ecosystem — a resolvable, typically key-based identifier scheme —
though the VC data model itself permits any URL as an issuer identifier.
Hachure treats signing as an opt-in Assurance dial (L0
unsigned by default, L1/L2 signed on request), not a precondition for a
record to exist.
Requiring DIDs for producerId (merge.md §2) would collapse that
layered design into "every producer needs key infrastructure just to be
namespaced for merge" — a strictly higher bar than merge, or basic claim
authorship, actually needs. producerId is deliberately at the same trust
level as the existing source field: free-text, unsigned, always available,
upgradable to a cryptographically verifiable identity only when a consumer's
policy requires it.
This is not a rejection of DIDs or VCs — an implementation that wants
DID-backed producer identity can express it today via Assurance L1/L2's OIDC-
or held-key-backed signing (assurance.md §"Identity presentation"), and
nothing here prevents a future profile from mapping Hachure records into a VC
envelope for interop with VC-native ecosystems. It is a statement that Hachure
does not require DID infrastructure just to produce a valid, useful record —
consistent with the "signing is a dial, not a gate" principle that runs
through the whole Assurance profile.
See merge.md §10 "Prior art" for the fuller technical rationale, and assurance.md for how signed identity is layered on top when a consumer needs it.
Relationship to IETF SCITT
SCITT (Supply Chain Integrity, Transparency, and Trust — RFC 9943, June 2026) is the closest prior art to Hachure's problem space: issuers sign statements about artifacts (COSE-signed), register them on append-only transparency services, and receive receipts proving registration. It is worth being precise about the split, because the two compose rather than compete:
- SCITT answers "who said this, and is it on the record?" It provides non-repudiable, tamper-evident registration of frozen signed statements. It deliberately does not define what a statement means, how evidence relates to a claim, what policy governs verification, or how a statement's standing changes as new facts arrive.
- Hachure answers "what is the standing of this claim right now?" Claims
travel with evidence, policy, and an append-only event ledger, and status
is recomputed —
verifieddecays tostale, getsdisputedby blocking evidence, or isrevokedby a later event — without ever editing the original record.
Composition is the natural shape: a TrustBundle (or its DSSE envelope per
interop-in-toto.md) can be registered as a SCITT signed
statement, and the resulting receipt belongs in the bundle's proof block as
a transparency_log anchor. SCITT then guarantees the bundle existed and who
registered it; Hachure keeps answering what its claims are worth as time
passes. As with DIDs above, none of this is required: SCITT registration is
an Assurance-layer dial, not a precondition for a valid record.
Worth stating plainly: SCITT deliberately left registration policy and
relying-party status decisions unstandardized (an earlier standardized policy
mechanism was dropped from the final architecture). Hachure's versioned status
function is a natural candidate to fill exactly that vacated niche — a
published, recomputable policy a transparency service or relying party can
adopt instead of writing bespoke local rules. The SCITT profile
makes this concrete: registration mapping, receipts as proof anchors, and
the status function as a declared appraisal policy.
Relationship to policy engines (OPA/Rego, Cedar, CEL)
General-purpose policy engines — Open Policy Agent's Rego, AWS Cedar, Google's
CEL — do recompute decisions deterministically as decision = f(input, policy),
which makes "isn't Hachure's status function just a Rego policy?" a fair question.
It is the closest challenger by mechanism, so the distinction is worth stating
precisely:
- A policy engine is a language and a runtime. It evaluates whatever policy an author writes, deterministically. There is no canonical trust-status policy in Rego or Cedar; "portable and reproducible" means the engine is side-effect-free, not that two parties compute the same appraisal. The policy is the author's, private, and unversioned across organizations.
- Hachure ships the function, not the engine.
status = f(claim, evidence, events, policy, authorityTrace, now)is a specific, published, versioned algorithm — pinned bystatusFunctionVersionand held to conformance vectors that prove independent implementations derive byte-identical statuses. Any consumer recomputes the same standing from the same inputs; that cross-party agreement is the point, and it is exactly what a general engine does not provide.
The two compose cleanly: the engine is substrate, the status function is the standardized artifact. A conforming Hachure deriver could be implemented on OPA, Cedar, or CEL — the engine is plumbing; the published, versioned function is what travels with the record and makes the verdict reproducible by everyone.
Out of scope: future extension profiles
The following producer domains are explicitly out of scope for this core specification. Each is a candidate for a future extension profile that imports the core record shapes and adds domain-specific vocabulary:
- Extraction/review provenance chains — the source → extraction → candidate → review → claim pipeline a producer runs before a claim lands in a bundle. The bundle is the output boundary; the review trail above it is producer-scoped.
- Repository/codebase standards — per-repo claim vocabularies, per-run evidence collection conventions, and merge-gate integration records.
- Gate/run records — gate-expectation vocabularies, run-scoped views, and
gate-result record shapes built on top of
DerivationRuleandInquiryRecord.
Extension profiles reference this spec as their foundation and declare any additional fields or constraints. They do not modify the core record shapes. (Kontour AI's product suite defines profiles in each of these domains; they carry product-scoped namespaces and no special status in this specification.)
Executable conformance
conformance/ contains test vector bundles and expected per-claim
statuses at a fixed now. The specification is executable in-repo: this package's
suite runs every status-derivation vector against the bundled implementation
(test/derive.conformance.test.mjs) and every merge vector — under every
permutation of the input bundles — against the bundled merge
(test/merge.conformance.test.mjs). npx hachure vectors runs the same check
from the command line. Independent implementations prove conformance by running
the same vectors via the testVectors export.
See conformance/README.md for the test vector inventory, and
conformance/manifest.json (also exported as
conformanceManifest) for a machine-readable index of what an implementation
must pass to claim conformance at each level (L1 schema-valid records, L2
status-derivation vectors, L3 merge vectors).
Project documents
- SECURITY.md — the format's honest trust boundaries: source/producerId spoofing, verification-endpoint replay risk, and whole-bundle substitution, and which Assurance level mitigates each.
- CONTRIBUTING.md — how to propose a change, when a design writeup is expected, and the conformance-vector requirement for behavior changes. (Draft — see the banner in that file.)
- GOVERNANCE.md — who currently has decision authority, and what "neutral governance" is expected to mean when the project moves toward it. Expands the "Governance intent" paragraph above; does not contradict it. (Draft — see the banner in that file.)
- ROADMAP.md — what "1.0" will mean and the explicit exit criteria for declaring it; near-term profile candidates.
- LICENSE — MIT, matching
package.json's"license"field.
Canonical home
This repository (hachure-org/spec) is the canonical home of the Hachure
specification: prose, normative JSON Schemas, conformance test vectors, and the
bundled implementation. On any conflict between an implementation and this
repository, this repository wins.
Implementations
Conformance is claimed by passing the conformance vectors (manifest), not by appearing in this list. Known implementations:
| Implementation | Maintainer | Notes |
|---|---|---|
| hachure (this package, lib/) | hachure-org | Bundled with the spec; dependency-free; runs all vectors in-repo. |
| @kontourai/surface | Kontour AI | Separate (first-party) implementation, maintained by Kontour AI; runs these vectors in its own suite. Does not yet satisfy ROADMAP.md's exit criterion 1, which requires an implementation not maintained by Kontour AI or hachure-org. |
To add an implementation, open a PR that links to your public conformance-vector run.
