hachure 0.7.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
@@ -260,6 +273,42 @@ a profile requires no changes to core record shapes or the status function.
260
273
 
261
274
  ---
262
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
+
263
312
  ## Out of scope: future extension profiles
264
313
 
265
314
  The following producer domains are explicitly out of scope for this core specification.
@@ -286,7 +335,28 @@ fixed `now`. The test at `tests/spec-conformance.test.ts` loads every test vecto
286
335
  asserts that the reference implementation derives the expected statuses, making this
287
336
  specification executable.
288
337
 
289
- 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.
290
360
 
291
361
  ---
292
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
@@ -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
+ }
package/index.mjs CHANGED
@@ -11,6 +11,13 @@
11
11
  * vectors. Each vector has `input`, `expect`, and
12
12
  * `now` fields; run them against your implementation
13
13
  * to claim conformance.
14
+ * conformanceManifest — structured object describing conformance levels
15
+ * (L1 schema-valid, L2 status vectors, L3 merge
16
+ * vectors), which files satisfy each, and the
17
+ * schemaVersion/statusFunctionVersion it applies
18
+ * to. Parsed from conformance/manifest.json.
19
+ * Distinct from testVectors: this describes what
20
+ * passing means, testVectors is the raw fixtures.
14
21
  */
15
22
 
16
23
  import { readFileSync, readdirSync } from 'node:fs';
@@ -50,7 +57,9 @@ function loadTestVectors() {
50
57
  const conformanceDir = join(__dirname, 'conformance');
51
58
  const vectors = [];
52
59
  for (const file of readdirSync(conformanceDir).sort()) {
53
- if (!file.endsWith('.json')) continue;
60
+ // manifest.json is structured metadata (see conformanceManifest below),
61
+ // not a { now, input, expect } test vector — excluded here.
62
+ if (!file.endsWith('.json') || file === 'manifest.json') continue;
54
63
  const name = basename(file, '.json');
55
64
  const vector = JSON.parse(readFileSync(join(conformanceDir, file), 'utf8'));
56
65
  vectors.push({ name, vector });
@@ -59,3 +68,15 @@ function loadTestVectors() {
59
68
  }
60
69
 
61
70
  export const testVectors = loadTestVectors();
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Conformance manifest — structured levels/requirements, distinct from the
74
+ // raw testVectors list above. See conformance/manifest.json and
75
+ // conformance/README.md for the human-readable pointer.
76
+ // ---------------------------------------------------------------------------
77
+ function loadConformanceManifest() {
78
+ const manifestPath = join(__dirname, 'conformance', 'manifest.json');
79
+ return JSON.parse(readFileSync(manifestPath, 'utf8'));
80
+ }
81
+
82
+ export const conformanceManifest = loadConformanceManifest();
@@ -2,6 +2,7 @@
2
2
 
3
3
  **Module:** `@kontourai/surface` — `src/interop/in-toto.ts`
4
4
  **Public exports:** `toInTotoStatement`, `toDsseEnvelope`, `buildPaeBytes`, `parseDssePayload`
5
+ **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).
5
6
 
6
7
  ---
7
8
 
package/merge.md CHANGED
@@ -3,6 +3,7 @@
3
3
  **Function:** `mergeBundles(bundles: TrustBundle[]) → TrustBundle` /
4
4
  `mergeBundlesDetailed(bundles: TrustBundle[]) → { bundle: TrustBundle; collisions: MergeCollision[] }`
5
5
  **Source of truth:** `src/merge.ts`, `src/identity.ts`, `src/canonical.ts` in `@kontourai/surface`
6
+ **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).
6
7
 
7
8
  ---
8
9
 
@@ -194,14 +195,40 @@ content-identical, an implementation MUST:
194
195
  just against the first-seen one), and report a collision for every
195
196
  distinct-content pair.
196
197
  2. Choose the *kept* record deterministically from content alone — not from
197
- array position — using the record whose RFC 8785 (JSON Canonicalization
198
- Scheme) serialization sorts lexicographically first among the distinct
199
- contents. (RFC 8785/JCS is the target canonicalization primitive; until it
200
- is adopted bundle-wide, an implementation MAY substitute `JSON.stringify`
201
- of each object with its keys sorted recursively, which is
202
- order-independent for this purpose even though it is not RFC 8785-compliant
203
- in general this rule asks for convergence-under-permutation of *this*
204
- function, not full JCS compliance.)
198
+ array position — using the record whose [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785)
199
+ (JSON Canonicalization Scheme, JCS) serialization sorts lexicographically
200
+ first among the distinct contents.
201
+
202
+ **Canonicalization decision (ratified):** RFC 8785/JCS is the normative
203
+ canonicalization primitive for this tie-break rule, and for `"hash"`-kind
204
+ `integrityAnchor` computation bundle-wide (`SECURITY.md` §"Integrity anchors
205
+ and canonicalization" states the identical rule for hashing — one decision,
206
+ cited from both places). This was previously a "target primitive... until
207
+ adopted bundle-wide" hedge; it is now ratified as MUST, not a future
208
+ aspiration: canonicalization MUST be RFC 8785 (JCS) — full stop, with no
209
+ sorted-key-`JSON.stringify` shortcut carve-out. Two implementations that both
210
+ claim RFC 8785 compliance and compare the same distinct contents MUST agree
211
+ on which one sorts first.
212
+
213
+ > **Note (informative, not normative):** producing byte-identical RFC 8785
214
+ > output across languages has real cross-language pitfalls implementers
215
+ > should verify against, not assume away. (a) Non-ASCII string escaping —
216
+ > RFC 8785 (via RFC 8259) requires strings to contain literal UTF-8
217
+ > characters, never `\uXXXX` escapes, but several languages' default JSON
218
+ > serializers `\u`-escape non-ASCII by default (e.g. Python's `json.dumps`'s
219
+ > `ensure_ascii=True` default) and must be reconfigured to emit literal
220
+ > UTF-8. (b) Number serialization MUST follow the ECMAScript
221
+ > `Number::toString` algorithm (RFC 8785 §3.2.2.3) — native to JavaScript
222
+ > engines, but other languages need a compliant implementation of that exact
223
+ > algorithm, not their own default float-to-string routine. (c) Property
224
+ > sort order is by UTF-16 code unit value (RFC 8785 §3.2.3), not codepoint,
225
+ > byte, or locale-collation order. RFC 8785 §3.1 also does not apply Unicode
226
+ > normalization — strings are canonicalized exactly as they already are in
227
+ > memory, so two visually-identical strings that differ only in Unicode
228
+ > normalization form produce different canonical bytes. A hand-rolled
229
+ > sorted-key `JSON.stringify` can silently diverge from RFC 8785 on any of
230
+ > these points; there is no shortcut that is safe to assume equivalent
231
+ > without verifying against a conformant JCS implementation.
205
232
 
206
233
  This makes the merged bundle a pure, order-independent function of the *set*
207
234
  of input bundles — the same guarantee `status-function.md` already gives for
@@ -301,11 +328,14 @@ claim).
301
328
  Referenced descriptively in §7b/§7c (the `contradiction` gap type already
302
329
  exists informally, per `schemas/trust-report.schema.json`'s own
303
330
  `$comment`), but this document does not add a schema for them.
304
- - **RFC 8785 canonicalization as a general bundle-hashing primitive.** §6
305
- depends on *a* canonicalization function existing for its tie-break rule
306
- and names RFC 8785 (JCS) as the target, with a documented interim fallback
307
- (sorted-key `JSON.stringify`) — it does not itself specify RFC 8785
308
- adoption bundle-wide.
331
+ - **A canonicalization *library* shipped by this repo.** RFC 8785/JCS is
332
+ ratified (§6) as the normative canonicalization primitive for both the §6
333
+ tie-break rule and `"hash"`-kind `integrityAnchor` computation
334
+ (`SECURITY.md`) — that decision is now settled, not deferred. What remains
335
+ out of scope here is providing a canonicalization *implementation*:
336
+ `hachure-org/spec` ships schemas and prose, not runtime code: it is the
337
+ reference implementation's (`@kontourai/surface`'s) responsibility to
338
+ implement RFC 8785, not this repo's.
309
339
  - **Cryptographic producer identity (DIDs, keys, transparency-log-anchored
310
340
  identity).** `producerId` (§2) is deliberately unsigned and unverified.
311
341
  Where verifiable producer identity is needed, use Assurance L1/L2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hachure",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "statusFunctionVersion": "2",
5
5
  "description": "Hachure — canonical distribution of the open trust format: normative JSON schemas, conformance test vectors, and spec constants.",
6
6
  "type": "module",
@@ -19,7 +19,8 @@
19
19
  "merge.md",
20
20
  "interop-in-toto.md",
21
21
  "verification-endpoint.md",
22
- "assurance.md"
22
+ "assurance.md",
23
+ "SECURITY.md"
23
24
  ],
24
25
  "scripts": {
25
26
  "test": "node --test test/*.test.mjs"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://kontourai.io/schemas/surface/claim.schema.json",
4
- "title": "Kontour Surface Claim",
3
+ "$id": "https://hachure.org/schemas/claim.schema.json",
4
+ "title": "Hachure Claim",
5
5
  "type": "object",
6
6
  "required": ["id", "subjectType", "subjectId", "surface", "claimType", "fieldOrBehavior", "value", "createdAt", "updatedAt"],
7
7
  "properties": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://kontourai.io/schemas/surface/derivation-rule.schema.json",
4
- "title": "Surface DerivationRule",
3
+ "$id": "https://hachure.org/schemas/derivation-rule.schema.json",
4
+ "title": "Hachure DerivationRule",
5
5
  "description": "A named, versioned rule composing existing claims into a derived boolean answer (ADR 0003 §5).",
6
6
  "type": "object",
7
7
  "required": ["id", "version", "name", "target", "requirements", "combinator"],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://kontourai.io/schemas/surface/evidence.schema.json",
4
- "title": "Kontour Surface Evidence",
3
+ "$id": "https://hachure.org/schemas/evidence.schema.json",
4
+ "title": "Hachure Evidence",
5
5
  "type": "object",
6
6
  "required": ["id", "claimId", "evidenceType", "method", "sourceRef", "excerptOrSummary", "observedAt", "collectedBy"],
7
7
  "properties": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://kontourai.io/schemas/surface/inquiry-record.schema.json",
4
- "title": "Surface InquiryRecord",
3
+ "$id": "https://hachure.org/schemas/inquiry-record.schema.json",
4
+ "title": "Hachure InquiryRecord",
5
5
  "description": "Append-only record capturing the resolution of an Inquiry against a TrustBundle (ADR 0003 §6).",
6
6
  "type": "object",
7
7
  "required": ["id", "inquiry", "outcome", "resolutionPath", "inputSnapshot", "statusFunctionVersion", "resolvedAt"],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://kontourai.io/schemas/surface/trust-bundle.schema.json",
4
- "title": "Surface TrustBundle",
3
+ "$id": "https://hachure.org/schemas/trust-bundle.schema.json",
4
+ "title": "Hachure TrustBundle",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
7
  "required": ["schemaVersion", "source", "claims", "evidence", "policies", "events"],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://kontourai.io/schemas/surface/trust-report.schema.json",
4
- "title": "Surface TrustReport",
3
+ "$id": "https://hachure.org/schemas/trust-report.schema.json",
4
+ "title": "Hachure TrustReport",
5
5
  "$comment": "transparencyGaps, changeRecords, subjectGroups, claimGroupRollups, summary, and evidenceRequirementsByClaimId are intentionally loosely typed pending dedicated normative sub-schemas (no existing schema for TransparencyGap/DerivationChangeRecord/SubjectGroup/ClaimGroupRollup/TrustReportSummary/EvidenceRequirement exists anywhere in the ecosystem today); only the top-level report shape and the pass-through bundle fields are strictly validated.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://kontourai.io/schemas/surface/verification-event.schema.json",
4
- "title": "Kontour Surface Verification Event",
3
+ "$id": "https://hachure.org/schemas/verification-event.schema.json",
4
+ "title": "Hachure VerificationEvent",
5
5
  "type": "object",
6
6
  "required": ["id", "claimId", "status", "actor", "method", "evidenceIds", "createdAt"],
7
7
  "properties": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://kontourai.io/schemas/surface/verification-policy.schema.json",
4
- "title": "Kontour Surface Verification Policy",
3
+ "$id": "https://hachure.org/schemas/verification-policy.schema.json",
4
+ "title": "Hachure VerificationPolicy",
5
5
  "type": "object",
6
6
  "required": ["id", "claimType", "requiredEvidence", "acceptanceCriteria", "reviewAuthority", "validityRule", "stalenessTriggers", "conflictRules", "impactLevel"],
7
7
  "properties": {
@@ -3,6 +3,7 @@
3
3
  **Function:** `status = f(claim, evidence, events, policy, authorityTrace, now)`
4
4
  **Version constant:** `statusFunctionVersion` (currently `"2"`)
5
5
  **Source of truth:** `src/status.ts` in `@kontourai/surface`
6
+ **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).
6
7
 
7
8
  ---
8
9
 
@@ -4,6 +4,7 @@
4
4
  **Status:** draft
5
5
  **Namespace:** `hachure.org/v1`
6
6
  **Depends on:** core record shapes, [status-function.md](status-function.md), [interop-in-toto.md](interop-in-toto.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