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 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. Bundles from multiple producers can be
108
- merged; conflicts surface as `disputed` status (never last-write-wins).
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
 
@@ -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
+ }