hachure 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +79 -3
- package/SECURITY.md +180 -0
- package/assurance.md +1 -0
- package/conformance/README.md +48 -0
- package/conformance/manifest.json +71 -0
- package/conformance/merge/merge-agree-values.json +59 -0
- package/conformance/merge/merge-collision-order-independence.json +85 -0
- package/conformance/merge/merge-conflict-status.json +149 -0
- package/conformance/merge/merge-conflict-value.json +100 -0
- package/index.mjs +22 -1
- package/interop-in-toto.md +1 -0
- package/merge.md +394 -0
- package/package.json +4 -2
- package/schemas/claim.schema.json +2 -2
- package/schemas/derivation-rule.schema.json +2 -2
- package/schemas/evidence.schema.json +2 -2
- package/schemas/inquiry-record.schema.json +2 -2
- package/schemas/trust-bundle.schema.json +7 -2
- package/schemas/trust-report.schema.json +2 -2
- package/schemas/verification-event.schema.json +2 -2
- package/schemas/verification-policy.schema.json +2 -2
- package/status-function.md +1 -0
- package/verification-endpoint.md +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kontour AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -86,6 +86,19 @@ every bundle valid at `schemaVersion` `3` remains valid; only the deriver
|
|
|
86
86
|
|
|
87
87
|
---
|
|
88
88
|
|
|
89
|
+
## Conformance language
|
|
90
|
+
|
|
91
|
+
The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
|
|
92
|
+
**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this
|
|
93
|
+
document and in every other normative document in this repository
|
|
94
|
+
(`merge.md`, `assurance.md`, `verification-endpoint.md`,
|
|
95
|
+
`status-function.md`, `interop-in-toto.md`, `SECURITY.md`) are to be
|
|
96
|
+
interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119)
|
|
97
|
+
and clarified by [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) (BCP 14),
|
|
98
|
+
only when they appear in all capitals, as shown here.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
89
102
|
## Scope: core record shapes
|
|
90
103
|
|
|
91
104
|
This specification covers the following record types. Each is a first-class concept
|
|
@@ -104,8 +117,14 @@ Plain-language definition (ADR 0002):
|
|
|
104
117
|
> the producer played by — packed so it can cross a product boundary without the
|
|
105
118
|
> receiver needing access to the producer's internals.
|
|
106
119
|
|
|
107
|
-
The `source` field identifies the producer
|
|
108
|
-
|
|
120
|
+
The `source` field identifies the producer (free-text, may vary per run); an optional
|
|
121
|
+
`producerId` field carries a stable, unsigned identifier for the producing system,
|
|
122
|
+
consistent across every bundle it emits. When present, `producerId` MUST be a
|
|
123
|
+
non-empty string. Bundles from multiple producers can be merged
|
|
124
|
+
into one ledger without last-write-wins and without deleting losing evidence; conflicts
|
|
125
|
+
between claims are surfaced as `contradiction` transparency gaps, never silently
|
|
126
|
+
resolved or used to flip a claim's status. The full specification of identifier
|
|
127
|
+
conventions and the merge algorithm is in [merge.md](merge.md).
|
|
109
128
|
|
|
110
129
|
An optional `identityLinks` array declares co-referent subjects — real-world entities
|
|
111
130
|
known under more than one identifier. Each link carries a stable optional `id`, a
|
|
@@ -254,6 +273,42 @@ a profile requires no changes to core record shapes or the status function.
|
|
|
254
273
|
|
|
255
274
|
---
|
|
256
275
|
|
|
276
|
+
## Relationship to W3C Verifiable Credentials
|
|
277
|
+
|
|
278
|
+
Hachure and the [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/)
|
|
279
|
+
data model both represent claims-with-evidence, and it is a fair question why
|
|
280
|
+
this format does not simply build on VC instead of defining its own record
|
|
281
|
+
shapes.
|
|
282
|
+
|
|
283
|
+
**The short answer:** DID-based issuer identity is the dominant convention
|
|
284
|
+
in the VC ecosystem — a resolvable, typically key-based identifier scheme —
|
|
285
|
+
though the VC data model itself permits any URL as an issuer identifier.
|
|
286
|
+
Hachure treats signing as an opt-in [Assurance](assurance.md) dial (L0
|
|
287
|
+
unsigned by default, L1/L2 signed on request), not a precondition for a
|
|
288
|
+
record to exist.
|
|
289
|
+
Requiring DIDs for `producerId` ([merge.md](merge.md) §2) would collapse that
|
|
290
|
+
layered design into "every producer needs key infrastructure just to be
|
|
291
|
+
namespaced for merge" — a strictly higher bar than merge, or basic claim
|
|
292
|
+
authorship, actually needs. `producerId` is deliberately at the same trust
|
|
293
|
+
level as the existing `source` field: free-text, unsigned, always available,
|
|
294
|
+
upgradable to a cryptographically verifiable identity only when a consumer's
|
|
295
|
+
policy requires it.
|
|
296
|
+
|
|
297
|
+
This is not a rejection of DIDs or VCs — an implementation that wants
|
|
298
|
+
DID-backed producer identity can express it today via Assurance L1/L2's OIDC-
|
|
299
|
+
or held-key-backed signing (`assurance.md` §"Identity presentation"), and
|
|
300
|
+
nothing here prevents a future profile from mapping Hachure records into a VC
|
|
301
|
+
envelope for interop with VC-native ecosystems. It is a statement that Hachure
|
|
302
|
+
does not *require* DID infrastructure just to produce a valid, useful record —
|
|
303
|
+
consistent with the "signing is a dial, not a gate" principle that runs
|
|
304
|
+
through the whole Assurance profile.
|
|
305
|
+
|
|
306
|
+
See [merge.md](merge.md) §10 "Prior art" for the fuller technical rationale,
|
|
307
|
+
and [assurance.md](assurance.md) for how signed identity is layered on top
|
|
308
|
+
when a consumer needs it.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
257
312
|
## Out of scope: future extension profiles
|
|
258
313
|
|
|
259
314
|
The following producer domains are explicitly out of scope for this core specification.
|
|
@@ -280,7 +335,28 @@ fixed `now`. The test at `tests/spec-conformance.test.ts` loads every test vecto
|
|
|
280
335
|
asserts that the reference implementation derives the expected statuses, making this
|
|
281
336
|
specification executable.
|
|
282
337
|
|
|
283
|
-
See [conformance/README.md](conformance/README.md) for the test vector inventory
|
|
338
|
+
See [conformance/README.md](conformance/README.md) for the test vector inventory, and
|
|
339
|
+
[conformance/manifest.json](conformance/manifest.json) (also exported as
|
|
340
|
+
`conformanceManifest`) for a machine-readable index of what an implementation
|
|
341
|
+
must pass to claim conformance at each level (L1 schema-valid records, L2
|
|
342
|
+
status-derivation vectors, L3 merge vectors).
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Project documents
|
|
347
|
+
|
|
348
|
+
- **[SECURITY.md](SECURITY.md)** — the format's honest trust boundaries:
|
|
349
|
+
source/producerId spoofing, verification-endpoint replay risk, and
|
|
350
|
+
whole-bundle substitution, and which [Assurance](assurance.md) level
|
|
351
|
+
mitigates each.
|
|
352
|
+
- **[CONTRIBUTING.md](CONTRIBUTING.md)** — how to propose a change, when a
|
|
353
|
+
design writeup is expected, and the conformance-vector requirement for
|
|
354
|
+
behavior changes. *(Draft — see the banner in that file.)*
|
|
355
|
+
- **[GOVERNANCE.md](GOVERNANCE.md)** — who currently has decision authority,
|
|
356
|
+
and what "neutral governance" is expected to mean when the project moves
|
|
357
|
+
toward it. Expands the "Governance intent" paragraph above; does not
|
|
358
|
+
contradict it. *(Draft — see the banner in that file.)*
|
|
359
|
+
- **[LICENSE](LICENSE)** — MIT, matching `package.json`'s `"license"` field.
|
|
284
360
|
|
|
285
361
|
---
|
|
286
362
|
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Security Considerations
|
|
2
|
+
|
|
3
|
+
This document describes the trust boundaries of the Hachure core format and
|
|
4
|
+
its optional profiles, and points at the mechanism each risk is mitigated by.
|
|
5
|
+
It is a description of the format's own honest limits, not a claim that these
|
|
6
|
+
risks are solved by default — most of them are solved only when a consumer
|
|
7
|
+
opts into a higher [Assurance](assurance.md) level.
|
|
8
|
+
|
|
9
|
+
Reporting a vulnerability in the schemas, conformance vectors, or prose in
|
|
10
|
+
this repository: open an issue at
|
|
11
|
+
[hachure-org/spec](https://github.com/hachure-org/spec/issues). This repo
|
|
12
|
+
ships specification artifacts (JSON Schemas, test vectors, markdown), not
|
|
13
|
+
runtime code; there is no server or service to report an exploit against
|
|
14
|
+
here. Vulnerabilities in the reference implementation (`@kontourai/surface`)
|
|
15
|
+
should be reported in that repository.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Threat model summary
|
|
20
|
+
|
|
21
|
+
The core format (schemas, `merge.md`, `status-function.md`) makes **no
|
|
22
|
+
cryptographic guarantee by default.** Every TrustBundle is valid — and fully
|
|
23
|
+
conformant — with zero signatures. This is a deliberate design choice
|
|
24
|
+
(`assurance.md` §"Principle: signing is a dial, not a gate"), not an
|
|
25
|
+
oversight, but it means a receiver's default trust posture for an unsigned
|
|
26
|
+
bundle is "as trustworthy as the channel it arrived over," never higher.
|
|
27
|
+
|
|
28
|
+
The three risk classes below are the direct, honest consequences of that
|
|
29
|
+
default. All three share the same mitigation dial: [assurance.md](assurance.md)'s
|
|
30
|
+
L0/L1/L2 levels. **Signing is opt-in, not a precondition for validity** — a
|
|
31
|
+
consumer that needs a mitigation below must pull the dial itself, by
|
|
32
|
+
requiring L1/L2 in its `VerificationPolicy`; the format does not do it for
|
|
33
|
+
them.
|
|
34
|
+
|
|
35
|
+
### 1. Source / producerId spoofing (L0)
|
|
36
|
+
|
|
37
|
+
`TrustBundle.source` (`schemas/trust-bundle.schema.json`) is free text, and
|
|
38
|
+
the optional `TrustBundle.producerId` (`merge.md` §2) is an unsigned string.
|
|
39
|
+
Both are **self-asserted**: nothing in the core format cryptographically
|
|
40
|
+
binds either field to the system that actually produced the bundle. A
|
|
41
|
+
receiver of an unsigned bundle has no guarantee that the `source`/`producerId`
|
|
42
|
+
values are accurate — only that they are what the sender chose to write down.
|
|
43
|
+
Below Assurance L1, this is trust-on-first-use: the receiver's confidence in
|
|
44
|
+
producer identity comes entirely from the trustworthiness of the transport
|
|
45
|
+
channel (e.g. an authenticated HTTPS connection to a known host), not from
|
|
46
|
+
anything inside the bundle itself.
|
|
47
|
+
|
|
48
|
+
`producerId` was introduced (`merge.md` §2) specifically to give merge a
|
|
49
|
+
stable, comparable identity across runs — it is explicitly documented there
|
|
50
|
+
as carrying "no cryptographic weight," an L0 fact in Assurance-profile terms.
|
|
51
|
+
Reusing it as an authorization or access-control signal without an L1/L2
|
|
52
|
+
signature over it is a misuse of the field, not a supported use case.
|
|
53
|
+
|
|
54
|
+
**Mitigation:** [assurance.md](assurance.md) L1 (OIDC-backed identity-signed)
|
|
55
|
+
or L2 (org-held-key-signed) records bind a verifiable identity to the record
|
|
56
|
+
via a DSSE envelope. A consumer that needs verified producer identity should
|
|
57
|
+
require L1 or L2 in its `VerificationPolicy`'s acceptance criteria and treat
|
|
58
|
+
a below-threshold bundle as a transparency gap (`assurance.md` §"Consumer
|
|
59
|
+
policy"), not silently accept the self-asserted `source`/`producerId` values
|
|
60
|
+
as authoritative.
|
|
61
|
+
|
|
62
|
+
### 2. Replay in the verification-endpoint profile
|
|
63
|
+
|
|
64
|
+
[verification-endpoint.md](verification-endpoint.md) defines a pull-based
|
|
65
|
+
delta-fetch channel (`GET/POST .well-known/hachure/verify`) for a receiver to
|
|
66
|
+
ask a producer "what has changed since this bundle was issued?" As of this
|
|
67
|
+
writing, that profile defines no nonce, freshness token, or monotonic cursor
|
|
68
|
+
in either the request or the response. A response is authenticated only by
|
|
69
|
+
whatever transport-level authentication the producer chooses to require
|
|
70
|
+
(profile text: "Producers MAY require authentication before serving a
|
|
71
|
+
response") and, optionally, by a DSSE signature over the response bundle
|
|
72
|
+
(`verification-endpoint.md` §"Assurance levels").
|
|
73
|
+
|
|
74
|
+
This means: an unsigned verification-endpoint response carries no
|
|
75
|
+
cryptographic freshness guarantee. A response captured and replayed later by
|
|
76
|
+
a party sitting on the transport path (or by a compromised intermediate
|
|
77
|
+
cache) is not distinguishable, at the profile level, from a fresh response —
|
|
78
|
+
the profile's own `respondedAt` metadata field is producer-asserted, not
|
|
79
|
+
verified. This is named here as an explicit, currently-unmitigated gap in the
|
|
80
|
+
profile, not as a solved problem being restated: no existing
|
|
81
|
+
freshness-handling language exists in `verification-endpoint.md` today, and
|
|
82
|
+
this document does not silently invent one.
|
|
83
|
+
|
|
84
|
+
**Mitigation:** treat every verification-endpoint response — signed or not —
|
|
85
|
+
as advisory testimony, per the profile's own framing ("It is testimony with a
|
|
86
|
+
timestamp... It is never a verdict the receiver is expected to obey,"
|
|
87
|
+
`verification-endpoint.md` §"Problem"). Receivers that need replay resistance
|
|
88
|
+
should (a) require a signed response (Assurance L1/L2) *and* (b) layer their
|
|
89
|
+
own freshness handling on top — for example, rejecting a response whose
|
|
90
|
+
`respondedAt` is older than a receiver-chosen staleness bound, or pinning
|
|
91
|
+
expected `respondedAt` monotonicity per producer host — since the core
|
|
92
|
+
profile does not provide either mechanism itself.
|
|
93
|
+
|
|
94
|
+
### 3. Whole-bundle substitution
|
|
95
|
+
|
|
96
|
+
A TrustBundle transmitted without a DSSE envelope (Assurance L0, the default)
|
|
97
|
+
has no integrity protection against wholesale substitution in transit or at
|
|
98
|
+
rest. An attacker or compromised intermediary who can replace the bytes of an
|
|
99
|
+
unsigned bundle — swap it for a different bundle, an older bundle, or a
|
|
100
|
+
bundle from a different producer — is undetectable by the receiver using
|
|
101
|
+
only the core format's own mechanisms. Nothing in `schemas/*.schema.json` or
|
|
102
|
+
`status-function.md` provides tamper-evidence; those layers assume the bytes
|
|
103
|
+
they operate on are already the intended bytes.
|
|
104
|
+
|
|
105
|
+
**Mitigation:** Assurance L1/L2 DSSE signing
|
|
106
|
+
([interop-in-toto.md](interop-in-toto.md)) wraps the serialized bundle in a
|
|
107
|
+
signed envelope; a receiver that verifies the signature before parsing the
|
|
108
|
+
bundle detects substitution (the signature will not verify over substituted
|
|
109
|
+
bytes). This is the same mechanism item 1's mitigation depends on — signing
|
|
110
|
+
the bundle simultaneously binds its producer identity and its content
|
|
111
|
+
integrity. A receiver that only checks `source`/`producerId` (item 1) without
|
|
112
|
+
also verifying the envelope (item 3) has closed neither gap: an attacker who
|
|
113
|
+
can substitute the bundle can substitute the self-asserted `source` field
|
|
114
|
+
along with it.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Integrity anchors and canonicalization
|
|
119
|
+
|
|
120
|
+
Bundle and claim records carry optional `integrityAnchor` objects
|
|
121
|
+
(`schemas/claim.schema.json` `$defs/integrityAnchor`,
|
|
122
|
+
`schemas/trust-bundle.schema.json` `$defs/integrityAnchor`) with a `kind`
|
|
123
|
+
enum that includes `"hash"`. When an implementation computes a `"hash"`-kind
|
|
124
|
+
integrity anchor over a bundle or record — or independently verifies one — it
|
|
125
|
+
**MUST** canonicalize the JSON with [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785)
|
|
126
|
+
(the JSON Canonicalization Scheme, JCS) before hashing. This is the same
|
|
127
|
+
canonicalization primitive `merge.md` §6 now ratifies as normative for its
|
|
128
|
+
deterministic tie-break rule (see `merge.md` §6/§9) — one canonicalization
|
|
129
|
+
decision for the whole format, not two independently-evolving ones.
|
|
130
|
+
Canonicalization MUST be RFC 8785 (JCS) — full stop, with no
|
|
131
|
+
sorted-key-`JSON.stringify` shortcut carve-out. Two implementations that both
|
|
132
|
+
claim RFC 8785 compliance and hash the same logical content MUST produce the
|
|
133
|
+
same digest.
|
|
134
|
+
|
|
135
|
+
> **Note (informative, not normative):** producing byte-identical RFC 8785
|
|
136
|
+
> output across languages has real cross-language pitfalls implementers
|
|
137
|
+
> should verify against, not assume away. (a) Non-ASCII string escaping —
|
|
138
|
+
> RFC 8785 (via RFC 8259) requires strings to contain literal UTF-8
|
|
139
|
+
> characters, never `\uXXXX` escapes, but several languages' default JSON
|
|
140
|
+
> serializers `\u`-escape non-ASCII by default (e.g. Python's `json.dumps`'s
|
|
141
|
+
> `ensure_ascii=True` default) and must be reconfigured to emit literal
|
|
142
|
+
> UTF-8. (b) Number serialization MUST follow the ECMAScript
|
|
143
|
+
> `Number::toString` algorithm (RFC 8785 §3.2.2.3) — native to JavaScript
|
|
144
|
+
> engines, but other languages need a compliant implementation of that exact
|
|
145
|
+
> algorithm, not their own default float-to-string routine. (c) Property
|
|
146
|
+
> sort order is by UTF-16 code unit value (RFC 8785 §3.2.3), not codepoint,
|
|
147
|
+
> byte, or locale-collation order. RFC 8785 §3.1 also does not apply Unicode
|
|
148
|
+
> normalization — strings are canonicalized exactly as they already are in
|
|
149
|
+
> memory, so two visually-identical strings that differ only in Unicode
|
|
150
|
+
> normalization form produce different canonical bytes. A hand-rolled
|
|
151
|
+
> sorted-key `JSON.stringify` can silently diverge from RFC 8785 on any of
|
|
152
|
+
> these points; there is no shortcut that is safe to assume equivalent
|
|
153
|
+
> without verifying against a conformant JCS implementation.
|
|
154
|
+
|
|
155
|
+
An integrity anchor is only as trustworthy as the channel it arrived over
|
|
156
|
+
unless it is itself covered by an Assurance L1/L2 signature (item 3 above) —
|
|
157
|
+
a `"hash"` anchor on an otherwise-unsigned L0 bundle is a self-asserted
|
|
158
|
+
checksum, useful for detecting accidental corruption, not for detecting a
|
|
159
|
+
deliberate adversary who can also recompute the hash over substituted
|
|
160
|
+
content.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Summary: the mitigation is one dial, not three
|
|
165
|
+
|
|
166
|
+
All three risk classes above, and the integrity-anchor guidance, resolve to
|
|
167
|
+
the same answer: **[assurance.md](assurance.md)'s L0/L1/L2 levels are the
|
|
168
|
+
dial a consumer pulls when any of this matters.** L0 (the default, unsigned)
|
|
169
|
+
carries none of these guarantees and is not pretending to. L1/L2 (DSSE
|
|
170
|
+
signing, per [interop-in-toto.md](interop-in-toto.md)) closes the producer-identity
|
|
171
|
+
and bundle-substitution gaps together, because they are the same mechanism.
|
|
172
|
+
The verification-endpoint replay gap additionally needs receiver-side
|
|
173
|
+
freshness handling that the format does not currently specify — named here
|
|
174
|
+
explicitly rather than left implicit.
|
|
175
|
+
|
|
176
|
+
A consumer that needs any of these properties expresses that need as a
|
|
177
|
+
`VerificationPolicy` acceptance criterion requiring a minimum assurance
|
|
178
|
+
level; a bundle that falls short is a transparency gap for a human or policy
|
|
179
|
+
layer to act on (`assurance.md` §"Consumer policy"), never a silent
|
|
180
|
+
downgrade.
|
package/assurance.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
**Status:** draft
|
|
5
5
|
**Namespace:** `hachure.org/v1`
|
|
6
6
|
**Depends on:** core record shapes, [interop-in-toto.md](interop-in-toto.md), [verification-endpoint.md](verification-endpoint.md)
|
|
7
|
+
**Conformance language:** MUST/SHOULD/MAY keywords in this document are to be interpreted per RFC 2119/BCP 14, as defined in [README.md's Conformance language section](README.md#conformance-language).
|
|
7
8
|
|
|
8
9
|
---
|
|
9
10
|
|
package/conformance/README.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
This directory contains input bundles and expected per-claim statuses that make
|
|
4
4
|
the [Status Derivation specification](../status-function.md) executable.
|
|
5
5
|
|
|
6
|
+
**Machine-readable conformance manifest:** [`manifest.json`](manifest.json)
|
|
7
|
+
(also exported as `conformanceManifest` from the package root) is a
|
|
8
|
+
structured index of what an implementation must pass to claim conformance at
|
|
9
|
+
each level (L1 schema-valid records, L2 status-derivation vectors, L3 merge
|
|
10
|
+
vectors) — distinct from, and more structured than, the raw vector inventory
|
|
11
|
+
below. Read it, or run:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { conformanceManifest } from 'hachure';
|
|
15
|
+
console.log(conformanceManifest.levels);
|
|
16
|
+
```
|
|
17
|
+
|
|
6
18
|
Each test vector is a JSON file with an `input` (a valid TrustBundle) and an `expect`
|
|
7
19
|
object listing expected per-claim statuses at a fixed `now` timestamp. The test at
|
|
8
20
|
`tests/spec-conformance.test.ts` loads every test vector and asserts that the reference
|
|
@@ -32,3 +44,39 @@ implementation derives the expected statuses.
|
|
|
32
44
|
}
|
|
33
45
|
}
|
|
34
46
|
```
|
|
47
|
+
|
|
48
|
+
## Merge conformance vectors
|
|
49
|
+
|
|
50
|
+
`conformance/merge/` contains a second, distinct family of vectors that make
|
|
51
|
+
the [Identifier & Multi-Producer Merge Semantics specification](../merge.md)
|
|
52
|
+
executable. Each vector merges two or more input `TrustBundle`s and asserts
|
|
53
|
+
the merged claim-id set, any id collisions, and the per-claim status derived
|
|
54
|
+
independently on the merged bundle. This repo's `npm test` (`test/merge.test.mjs`)
|
|
55
|
+
validates vector *shape* and Ajv-validates every `inputs[]` entry against
|
|
56
|
+
`trust-bundle.schema.json`; it does not execute `mergeBundles`/
|
|
57
|
+
`mergeBundlesDetailed`/`deriveClaimStatus` (this repo carries no
|
|
58
|
+
`@kontourai/surface` dependency) — see `merge.md`'s "Reference implementation
|
|
59
|
+
notes" for the implementation-side conformance loop.
|
|
60
|
+
|
|
61
|
+
### Merge test vector inventory
|
|
62
|
+
|
|
63
|
+
| File | Scenario | Now |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `merge-agree-values.json` | Two producers' claims agree on the same canonical subject+field; both retained as distinct records, both derive their own status independently | 2026-06-10T00:00:00.000Z |
|
|
66
|
+
| `merge-conflict-value.json` | Two producers' claims disagree on value, governed by a shared `incompatibleValues` policy; both retained, statuses computed independently | 2026-06-10T00:00:00.000Z |
|
|
67
|
+
| `merge-conflict-status.json` | Producer A's claim reaches `disputed` via its own blocking evidence; producer B's claim independently reaches `verified` — merge does not let one overwrite or suppress the other | 2026-06-10T00:00:00.000Z |
|
|
68
|
+
| `merge-collision-order-independence.json` | Three bundles; one `Claim.id` shared by two with genuinely different content (accidental collision) plus one unrelated bundle; asserts the merge result (kept content + collisions) is identical for every permutation of `inputs` | 2026-06-10T00:00:00.000Z |
|
|
69
|
+
|
|
70
|
+
### Merge test vector format
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"now": "<ISO 8601 string>",
|
|
75
|
+
"inputs": [ /* TrustBundle, TrustBundle, ... */ ],
|
|
76
|
+
"expect": {
|
|
77
|
+
"mergedClaimIds": ["<id>", "..."],
|
|
78
|
+
"collisions": [{ "collection": "claims", "id": "<id>" }],
|
|
79
|
+
"statusByClaimId": { "<claimId>": "<TrustStatus>" }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schemaComment": "Machine-readable conformance manifest for the Hachure trust format. Not a JSON Schema itself — a structured index of what an implementation must pass to claim conformance at each level. Additive to, not a replacement for, index.mjs's `testVectors` export (which lists raw vectors, not levels/pass-criteria).",
|
|
3
|
+
"manifestVersion": 1,
|
|
4
|
+
"appliesTo": {
|
|
5
|
+
"schemaVersion": [2, 3, 4],
|
|
6
|
+
"statusFunctionVersion": "2"
|
|
7
|
+
},
|
|
8
|
+
"levels": [
|
|
9
|
+
{
|
|
10
|
+
"level": "L1",
|
|
11
|
+
"name": "Schema-valid records",
|
|
12
|
+
"description": "Every core record type an implementation emits or accepts validates against the corresponding normative JSON Schema in schemas/. This is the floor: an implementation that cannot produce or consume schema-valid records cannot claim any higher level.",
|
|
13
|
+
"requires": [],
|
|
14
|
+
"satisfiedBy": {
|
|
15
|
+
"kind": "schema-validation",
|
|
16
|
+
"schemaDir": "schemas",
|
|
17
|
+
"schemaFiles": [
|
|
18
|
+
"claim.schema.json",
|
|
19
|
+
"derivation-rule.schema.json",
|
|
20
|
+
"evidence.schema.json",
|
|
21
|
+
"inquiry-record.schema.json",
|
|
22
|
+
"trust-bundle.schema.json",
|
|
23
|
+
"trust-report.schema.json",
|
|
24
|
+
"verification-event.schema.json",
|
|
25
|
+
"verification-policy.schema.json"
|
|
26
|
+
],
|
|
27
|
+
"howToRun": "Validate your implementation's own TrustBundle output (and any other emitted record types) against schemas/trust-bundle.schema.json with all sibling schemas registered for $ref resolution, using an Ajv (draft 2020-12) instance or equivalent."
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"level": "L2",
|
|
32
|
+
"name": "Status derivation vectors",
|
|
33
|
+
"description": "The implementation's status-derivation function (status-function.md) produces the expected per-claim status for every single-bundle conformance vector in this directory, at the vector's fixed `now`.",
|
|
34
|
+
"requires": ["L1"],
|
|
35
|
+
"satisfiedBy": {
|
|
36
|
+
"kind": "test-vectors",
|
|
37
|
+
"vectorDir": "conformance",
|
|
38
|
+
"vectorFilePattern": "sf-*.json",
|
|
39
|
+
"vectorCount": 8,
|
|
40
|
+
"specRef": "status-function.md",
|
|
41
|
+
"howToRun": "For each vector file, call your status-derivation function with vector.input and new Date(vector.now), then assert the derived status for every claim id matches vector.expect.statusByClaimId. All vectors must pass for the statusFunctionVersion declared in appliesTo.statusFunctionVersion."
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"level": "L3",
|
|
46
|
+
"name": "Multi-producer merge vectors",
|
|
47
|
+
"description": "The implementation's merge function (merge.md) produces the expected merged claim-id set, collision set, and per-claim statuses on the merged bundle for every merge conformance vector, and the merge result is identical regardless of input bundle order (merge.md §6 determinism rule).",
|
|
48
|
+
"requires": ["L1", "L2"],
|
|
49
|
+
"satisfiedBy": {
|
|
50
|
+
"kind": "test-vectors",
|
|
51
|
+
"vectorDir": "conformance/merge",
|
|
52
|
+
"vectorFilePattern": "merge-*.json",
|
|
53
|
+
"vectorCount": 4,
|
|
54
|
+
"specRef": "merge.md",
|
|
55
|
+
"howToRun": "For each vector file, call mergeBundles/mergeBundlesDetailed on vector.inputs (and on every permutation of vector.inputs, to prove order-independence per merge.md §6), then assert the merged claim ids, collisions, and per-claim statuses (via your L2 status-derivation function on the merged bundle) match vector.expect."
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"canonicalization": {
|
|
60
|
+
"primitive": "RFC 8785 (JCS)",
|
|
61
|
+
"appliesTo": [
|
|
62
|
+
"merge.md §6 deterministic tie-break rule",
|
|
63
|
+
"\"hash\"-kind integrityAnchor computation (SECURITY.md)"
|
|
64
|
+
],
|
|
65
|
+
"status": "ratified"
|
|
66
|
+
},
|
|
67
|
+
"notes": [
|
|
68
|
+
"This manifest describes what passing looks like; it does not itself execute anything. hachure-org/spec ships schemas, prose, and vector fixtures — it does not depend on @kontourai/surface, so this repo's own `npm test` validates vector shape and Ajv-validates vector `input`/`inputs[]` fields against trust-bundle.schema.json, but does not run status/merge derivation itself. Algorithmic conformance (L2/L3) is proven by an implementation actually running its own deriveClaimStatus/mergeBundles against these vectors, per merge.md's \"Reference implementation notes\" table.",
|
|
69
|
+
"L2 and L3 are independent axes in principle (an implementation could theoretically support merge without an L1-conformant single-bundle deriver), but requires:['L1','L2'] on L3 reflects that merge.md's own conformance loop (merge.md's Reference implementation notes table) evaluates merged-bundle status via the same deriveClaimStatus function L2 exercises — there is no separate merge-only status function to test independently."
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Hand-derivation (merge.md §4/§5/§7a; status-function.md Step 6). Bundle A (producer-a) and Bundle B (producer-b) each carry one claim with the same subjectType/subjectId ('repo'/'repo-1') and the same fieldOrBehavior ('coverage'), no qualifiers on either side -> canonicalClaimKey equal (§4) -> same logical claim. Values are deep-equal (91 === 91) -> agreement (§7a): both claims MUST be retained as distinct records (never collapsed), so mergedClaimIds contains both distinct ids. Neither claims/evidence/policies/events share an id across the two bundles, so collisions is empty. Per-claim status: neither bundle attaches any policy or evidence/events for its claim, so status-function.md Step 6 ('No policy') applies for both -> policy is undefined and evidence.length === 0 -> 'unknown' for both, independently of one another and of the agreement itself (merge does not synthesize a stronger status from agreement, §7a).",
|
|
3
|
+
"now": "2026-06-10T00:00:00.000Z",
|
|
4
|
+
"inputs": [
|
|
5
|
+
{
|
|
6
|
+
"schemaVersion": 4,
|
|
7
|
+
"source": "producer-a:run-1",
|
|
8
|
+
"producerId": "producer-a",
|
|
9
|
+
"claims": [
|
|
10
|
+
{
|
|
11
|
+
"id": "producer-a.claim.readiness.coverage",
|
|
12
|
+
"subjectType": "repo",
|
|
13
|
+
"subjectId": "repo-1",
|
|
14
|
+
"surface": "readiness",
|
|
15
|
+
"claimType": "coverage",
|
|
16
|
+
"fieldOrBehavior": "coverage",
|
|
17
|
+
"value": 91,
|
|
18
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
19
|
+
"updatedAt": "2026-06-01T00:00:00.000Z"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"evidence": [],
|
|
23
|
+
"policies": [],
|
|
24
|
+
"events": []
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"schemaVersion": 4,
|
|
28
|
+
"source": "producer-b:run-7",
|
|
29
|
+
"producerId": "producer-b",
|
|
30
|
+
"claims": [
|
|
31
|
+
{
|
|
32
|
+
"id": "producer-b.claim.readiness.coverage",
|
|
33
|
+
"subjectType": "repo",
|
|
34
|
+
"subjectId": "repo-1",
|
|
35
|
+
"surface": "readiness",
|
|
36
|
+
"claimType": "coverage",
|
|
37
|
+
"fieldOrBehavior": "coverage",
|
|
38
|
+
"value": 91,
|
|
39
|
+
"createdAt": "2026-06-02T00:00:00.000Z",
|
|
40
|
+
"updatedAt": "2026-06-02T00:00:00.000Z"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"evidence": [],
|
|
44
|
+
"policies": [],
|
|
45
|
+
"events": []
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"expect": {
|
|
49
|
+
"mergedClaimIds": [
|
|
50
|
+
"producer-a.claim.readiness.coverage",
|
|
51
|
+
"producer-b.claim.readiness.coverage"
|
|
52
|
+
],
|
|
53
|
+
"collisions": [],
|
|
54
|
+
"statusByClaimId": {
|
|
55
|
+
"producer-a.claim.readiness.coverage": "unknown",
|
|
56
|
+
"producer-b.claim.readiness.coverage": "unknown"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Hand-derivation (merge.md §5/§6/§8). This vector's inputs/expect are STRUCTURALLY identical to .kontourai/flow-agents/hachure-identifier-merge/design.md §11 'Worked example + independent hand-derivation' -- the bundle-level source/producerId values were genericized from design.md §11's worked example (survey -> producer-a, veritas -> producer-b, flow -> producer-c; mapping recorded in this session's deliver.md History) to keep this shipped, machine-checked conformance fixture vendor-neutral, consistent with the rest of this repo's conformance vectors and merge.md's own examples (plan.md AC7). This rename does NOT change expect: source/producerId are bundle-level fields, not part of the Claim record being compared, so the tie-break outcome below is unaffected -- re-derived by hand against the renamed inputs. shared.claim.x appears in Bundle A (subjectId r1, surface readiness, claimType coverage, value 91) and Bundle B (subjectId r2, surface governance, claimType policy-check, value true) -- content differs under the same id, so per merge.md §5 rule 2 this is a collision, reported as {collection: 'claims', id: 'shared.claim.x'}. Per merge.md §6's tie-break, compute a canonical (sorted-key) serialization of each variant: A's serialization starts '{\\\"claimType\\\":\\\"coverage\\\",...}', B's starts '{\\\"claimType\\\":\\\"policy-check\\\",...}' -- lexicographically 'coverage' < 'policy-check' ('c' < 'p'), so A's record is the deterministically-kept content regardless of whether the implementation is handed [A,B,C], [B,A,C], [C,B,A], or any other permutation of the 3 inputs -- this is the vector that exercises the order-independence MUST (test/merge.test.mjs asserts identical output across every permutation of inputs). Bundle C's claim ('unrelated.claim.y') never collides with anything, and its id is a distinct string never repeated. Status-function.md Step 6 ('no policy, no evidence') applies to both surviving claim ids ('shared.claim.x' from kept content A, and 'unrelated.claim.y') since no bundle attaches any policy or evidence/events -> both derive 'unknown'.",
|
|
3
|
+
"now": "2026-06-10T00:00:00.000Z",
|
|
4
|
+
"inputs": [
|
|
5
|
+
{
|
|
6
|
+
"schemaVersion": 4,
|
|
7
|
+
"source": "producer-a",
|
|
8
|
+
"producerId": "producer-a",
|
|
9
|
+
"claims": [
|
|
10
|
+
{
|
|
11
|
+
"id": "shared.claim.x",
|
|
12
|
+
"subjectType": "repo",
|
|
13
|
+
"subjectId": "r1",
|
|
14
|
+
"surface": "readiness",
|
|
15
|
+
"claimType": "coverage",
|
|
16
|
+
"fieldOrBehavior": "coverage",
|
|
17
|
+
"value": 91,
|
|
18
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
19
|
+
"updatedAt": "2026-06-01T00:00:00.000Z"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"evidence": [],
|
|
23
|
+
"policies": [],
|
|
24
|
+
"events": []
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"schemaVersion": 4,
|
|
28
|
+
"source": "producer-b",
|
|
29
|
+
"producerId": "producer-b",
|
|
30
|
+
"claims": [
|
|
31
|
+
{
|
|
32
|
+
"id": "shared.claim.x",
|
|
33
|
+
"subjectType": "repo",
|
|
34
|
+
"subjectId": "r2",
|
|
35
|
+
"surface": "governance",
|
|
36
|
+
"claimType": "policy-check",
|
|
37
|
+
"fieldOrBehavior": "signed-off",
|
|
38
|
+
"value": true,
|
|
39
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
40
|
+
"updatedAt": "2026-06-01T00:00:00.000Z"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"evidence": [],
|
|
44
|
+
"policies": [],
|
|
45
|
+
"events": []
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"schemaVersion": 4,
|
|
49
|
+
"source": "producer-c",
|
|
50
|
+
"producerId": "producer-c",
|
|
51
|
+
"claims": [
|
|
52
|
+
{
|
|
53
|
+
"id": "unrelated.claim.y",
|
|
54
|
+
"subjectType": "gate",
|
|
55
|
+
"subjectId": "g1",
|
|
56
|
+
"surface": "gates",
|
|
57
|
+
"claimType": "gate-status",
|
|
58
|
+
"fieldOrBehavior": "passed",
|
|
59
|
+
"value": true,
|
|
60
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
61
|
+
"updatedAt": "2026-06-01T00:00:00.000Z"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"evidence": [],
|
|
65
|
+
"policies": [],
|
|
66
|
+
"events": []
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"expect": {
|
|
70
|
+
"mergedClaimIds": [
|
|
71
|
+
"shared.claim.x",
|
|
72
|
+
"unrelated.claim.y"
|
|
73
|
+
],
|
|
74
|
+
"collisions": [
|
|
75
|
+
{
|
|
76
|
+
"collection": "claims",
|
|
77
|
+
"id": "shared.claim.x"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"statusByClaimId": {
|
|
81
|
+
"shared.claim.x": "unknown",
|
|
82
|
+
"unrelated.claim.y": "unknown"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|