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/merge.md ADDED
@@ -0,0 +1,394 @@
1
+ # Identifier & Multi-Producer Merge Semantics — Specification
2
+
3
+ **Function:** `mergeBundles(bundles: TrustBundle[]) → TrustBundle` /
4
+ `mergeBundlesDetailed(bundles: TrustBundle[]) → { bundle: TrustBundle; collisions: MergeCollision[] }`
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).
7
+
8
+ ---
9
+
10
+ ## 1. Principle
11
+
12
+ A Trust Bundle (README §"TrustBundle") is the supply side of the ledger, from
13
+ a single producer (ADR 0002). Multiple producers' bundles about overlapping
14
+ subjects MUST be combinable into one ledger without:
15
+
16
+ - silently overwriting one producer's claim with another's (never
17
+ last-write-wins),
18
+ - deleting losing evidence,
19
+ - requiring a shared identifier authority, key infrastructure, or
20
+ pre-registration between producers.
21
+
22
+ This document specifies: how a claim's identity is compared across producers
23
+ (§4), how bundles fold into one ledger (§5), the determinism guarantee that
24
+ folding MUST satisfy (§6), how agreement/conflict/dispute are represented
25
+ (§7), and how accidental id collisions between *unrelated* records are
26
+ detected (§8).
27
+
28
+ ---
29
+
30
+ ## 2. Producer identity
31
+
32
+ `TrustBundle.source` (`schemas/trust-bundle.schema.json`) is a free-text
33
+ string. Real producers use it inconsistently as a human-readable label, a
34
+ run-scoped value, or both (e.g. `source: 'producer-b:${run_id}'`,
35
+ `source: 'session-log'`, `source: 'filesystem-inferred'`). `source`
36
+ alone is not a stable, comparable producer identity — it changes per
37
+ run/session for the same producer.
38
+
39
+ `TrustBundle` carries one OPTIONAL field, `producerId` (string), a stable
40
+ identifier for the *system* that produced the bundle, distinct from
41
+ `source`'s run-scoped free text:
42
+
43
+ ```jsonc
44
+ {
45
+ "schemaVersion": 4,
46
+ "source": "producer-a:run-48213", // unchanged: free text, may vary per run
47
+ "producerId": "producer-a", // OPTIONAL, new: stable across runs
48
+ "claims": [ /* ... */ ]
49
+ }
50
+ ```
51
+
52
+ Rules:
53
+
54
+ - `producerId` is OPTIONAL. A bundle without it is exactly as valid as a
55
+ bundle that predates this field (additive; `trust-bundle.schema.json`'s
56
+ `required` array is unchanged).
57
+ - When present, `producerId` MUST be a non-empty string
58
+ (`trust-bundle.schema.json`'s `producerId` property carries `minLength: 1`)
59
+ — an empty string carries no identifying information, so it is
60
+ schema-invalid rather than treated as equivalent to omitting the field.
61
+ - When present, `producerId` SHOULD be stable across every bundle the same
62
+ system emits, and SHOULD be used (§3) as the leading segment of that
63
+ producer's record ids.
64
+ - `producerId` carries no cryptographic weight. It is an L0
65
+ (producer-asserted) fact in Assurance-profile terms (`assurance.md`).
66
+ Producers wanting a verifiable producer identity SHOULD present that
67
+ identity via the existing Assurance L1 (OIDC-backed) or L2 (held-key)
68
+ presentation (`assurance.md` §"Identity presentation"). This document does
69
+ not define, and MUST NOT be read to require, any DID or key-resolution
70
+ mechanism. Cryptographic identity is Assurance-profile territory;
71
+ `producerId` is the plain, unsigned, always-available floor underneath it.
72
+ - On merge (§5), a merged bundle represents more than one producer, so a
73
+ merged bundle's `producerId` MUST be omitted — it MUST NOT be synthesized
74
+ the way `source` is (`source` becomes `merged:<a>+<b>`; `producerId` has no
75
+ analogous synthesized form). Per-record producer attribution across a merge
76
+ is best-effort via the id convention in §3, not a schema-enforced field on
77
+ every record; `Claim`, `Evidence`, `VerificationPolicy`, and
78
+ `VerificationEvent` do not each carry their own `producerId` — the
79
+ bundle-level field plus the id convention is the complete mechanism.
80
+
81
+ ---
82
+
83
+ ## 3. Identifier format
84
+
85
+ `id` fields (`Claim.id`, `Evidence.id`, `VerificationPolicy.id`,
86
+ `VerificationEvent.id`, etc.) remain `{ "type": "string" }` with no `pattern`
87
+ constraint. This document introduces no schema change to any `id` field.
88
+
89
+ - Producers SHOULD mint ids as dot-separated, lowercase, URL-safe segments
90
+ (a stable helper that lowercases, collapses non-alphanumeric runs to `-`,
91
+ and joins segments with `.` is the recommended shape).
92
+ - Producers that set `producerId` (§2) SHOULD make the id's leading segment
93
+ equal to `producerId` (or a short slug derived from it), e.g.
94
+ `producerId: "producer-a"` → ids like `producer-a.recommendation.upgrade-node`.
95
+ - This is a SHOULD, not a MUST, and is never schema-enforced. A conforming
96
+ bundle with un-prefixed ids remains fully conformant.
97
+ - Rationale for SHOULD over MUST: enforcing a producer prefix would need a
98
+ `pattern` regex, which cannot be written today without either rejecting
99
+ real existing ids or being so permissive it adds no safety. The prefix
100
+ convention earns its value from making *accidental* id collisions between
101
+ unrelated producers vanishingly unlikely (§8), not from schema enforcement.
102
+
103
+ ---
104
+
105
+ ## 4. Claim identity across producers
106
+
107
+ Two claims from different producers are the same logical claim (candidates
108
+ for agreement/conflict comparison, §7) **if and only if:**
109
+
110
+ 1. Their subjects resolve to the same canonical key under the merged bundle's
111
+ identity index (`IdentityIndex.canonicalKeyForClaim`) — i.e. same subject,
112
+ or subjects declared co-referent via `identityLinks`/`subjectAliases`.
113
+ 2. `canonicalClaimKey({ subjectType, subjectId, fieldOrBehavior, qualifiers })`
114
+ is equal once (1) is applied (same `fieldOrBehavior`, same `qualifiers`
115
+ after the existing trim/lowercase/sort normalization).
116
+
117
+ **`claimType` and `surface` are explicitly excluded from the identity key —
118
+ this is a deliberate design decision, not an oversight:**
119
+
120
+ - `claimType` is excluded because the canonical claim key is defined over
121
+ *subject, predicate, value, qualifiers* — `fieldOrBehavior` is the
122
+ predicate; `claimType` is a taxonomy tag, not part of the matching grammar.
123
+ Two producers describing the same subject+field under different
124
+ `claimType` taxonomies are still the same logical claim for merge
125
+ purposes; reusing the canonical key means merge and Inquiry matching never
126
+ diverge on this point.
127
+ - `surface` is excluded because it is a producer-defined grouping or
128
+ namespace for related claims, not the primary thing users evaluate. Two
129
+ producers will pick unrelated `surface` values for logically identical
130
+ claims — there is no shared `surface` vocabulary across producers;
131
+ including it in the identity key would make cross-producer matches
132
+ essentially never fire. `surface` remains meaningful *within* one
133
+ producer's bundle (grouping, reporting `bySurface` counts) but plays no
134
+ role in cross-producer identity.
135
+
136
+ This means: **claims are never collapsed into one record by claim identity.**
137
+ Two producers' claims about the same canonical subject+field, even when they
138
+ fully agree, remain two distinct `Claim` records with two distinct ids in the
139
+ merged bundle (§5 unions by `id`, not by claim identity) — claim identity is
140
+ used only to decide *how to interpret* the pair (§7), never to deduplicate
141
+ them into one.
142
+
143
+ ---
144
+
145
+ ## 5. The merge algorithm
146
+
147
+ Given `bundles: TrustBundle[]` (all sharing one `schemaVersion` —
148
+ implementations MUST reject a merge across differing `schemaVersion` values
149
+ rather than guessing a coercion):
150
+
151
+ 1. **Union every collection by `id`**: `claims`, `evidence`, `policies`,
152
+ `events` (each item has a required `id`); `claimGroups`, `authorityTrace`
153
+ (each item has an optional `id`; items without an `id` are always kept,
154
+ never deduped). `identityLinks` are concatenated in full (they may omit
155
+ `id`; a union-find-based identity index dedupes them harmlessly even when
156
+ duplicated).
157
+ 2. **First-occurrence wins content, subject to the determinism rule in §6** —
158
+ when two records share an `id`:
159
+ - If their content is structurally identical (deep-equal), keep it; this
160
+ is not a collision (the same fact was reported by two bundles, e.g.
161
+ after a re-export round-trip).
162
+ - If their content differs, this is a **collision** (§8): the
163
+ implementation MUST record it (`MergeCollision`: `collection`, `id`, and
164
+ enough information to identify the contributing bundles) rather than
165
+ silently picking one. The throwing entry point (`mergeBundles`) MUST
166
+ throw when any **claim** collision (differing content, same `Claim.id`)
167
+ is detected — silent claim corruption is the one thing merge MUST NOT
168
+ ever do. The non-throwing entry point (`mergeBundlesDetailed`) MUST
169
+ return the collisions for the caller to inspect/reconcile instead of
170
+ throwing.
171
+ 3. **`source` becomes a synthesized combination** of the distinct `source`
172
+ values across the merged bundles (`merged:<a>+<b>`); **`producerId` MUST
173
+ be omitted** on a merged bundle (§2).
174
+ 4. The merged bundle is not itself a new producer assertion — it MUST be
175
+ accepted as input to the same, unmodified status derivation
176
+ (`status-function.md`) and to the merge function again (merge MUST be
177
+ re-appliable to an already-merged bundle, since a bundle is a bundle
178
+ regardless of how many producers contributed to it — no special "already
179
+ merged" flag is introduced).
180
+
181
+ ---
182
+
183
+ ## 6. Determinism (order independence)
184
+
185
+ **MUST:** for any fixed *set* of input bundles, the merge function's output
186
+ (both the retained record content and the `collisions[]` set, modulo list
187
+ ordering) MUST be identical regardless of the order the bundles are supplied
188
+ in. `merge([A, B, C])`, `merge([C, A, B])`, and every other permutation of the
189
+ same set MUST produce the same merged bundle.
190
+
191
+ **Normative tie-break rule:** when N ≥ 2 records share an id and are not all
192
+ content-identical, an implementation MUST:
193
+
194
+ 1. Compare **every** colliding record's content against every other's (not
195
+ just against the first-seen one), and report a collision for every
196
+ distinct-content pair.
197
+ 2. Choose the *kept* record deterministically from content alone — not from
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.
232
+
233
+ This makes the merged bundle a pure, order-independent function of the *set*
234
+ of input bundles — the same guarantee `status-function.md` already gives for
235
+ `now`-parameterized status derivation, extended to the merge step that
236
+ precedes it.
237
+
238
+ ---
239
+
240
+ ## 7. Agreement, conflict, and dispute mechanics
241
+
242
+ Given two claims that are the same logical claim under §4:
243
+
244
+ ### 7a. Agreement
245
+
246
+ If `deepEqual(a.value, b.value)`: the claims agree. They MUST NOT be
247
+ collapsed into one record (§4). Agreement is informational at the merge
248
+ layer; agreement alone does not synthesize a stronger status. A consumer that
249
+ wants "N producers agree" as an input to a decision already has the tool for
250
+ it without a new mechanism: an authored `DerivationRule` (ADR 0003 §5,
251
+ `derivation-rule.schema.json`) can require `acceptedStatuses` across both
252
+ claim ids explicitly. This document does not add corroboration-across-producers
253
+ as an automatic status input.
254
+
255
+ ### 7b. Value conflict
256
+
257
+ If the claims are governed by a `VerificationPolicy` with `incompatibleValues`
258
+ covering the pair (`verification-policy.schema.json`) and the values match an
259
+ `incompatibleValues` pair: **both claims MUST be retained** (never
260
+ last-write-wins) and the conflict is surfaced as a `contradiction`
261
+ transparency gap. This document does not add a normative JSON Schema for
262
+ `TransparencyGap` — that remains explicitly out of scope
263
+ (`schemas/trust-report.schema.json`'s own `$comment` already documents this;
264
+ see §9). The merge-layer guarantee this document DOES make is
265
+ schema-checkable without a `TransparencyGap` schema: neither claim is
266
+ dropped, mutated, or status-overridden by the presence of the other (§5 rule
267
+ 2).
268
+
269
+ ### 7c. Status conflict
270
+
271
+ A cross-producer `incompatibleStatuses` policy match (like a value conflict,
272
+ §7b) produces a `contradiction` transparency gap. It does not, by itself,
273
+ flip either claim's `status`.
274
+
275
+ A claim's `status` becomes `disputed` **only** through the existing,
276
+ single-claim mechanisms already in `status-function.md`: blocking
277
+ non-passing evidence (Step 4c), a terminal event with `status: "disputed"`
278
+ (Step 2), or an authority-gated resolution that is itself overridden by newer
279
+ blocking evidence (Step 1). Nothing in the cross-producer conflict path sets
280
+ a claim's status to `disputed`; `TrustReport.summary.disputedClaims` is
281
+ populated purely by scanning `claim.status === "disputed"` from each claim's
282
+ own single-claim fold output.
283
+
284
+ ### 7d. Dispute resolution — no new record type
285
+
286
+ When a human/authority needs to resolve a `disputed` status (from 7c's
287
+ existing mechanisms), the spec already has the shape (ADR 0003 §8): a
288
+ `VerificationEvent` with `resolvesDispute: true` and an optional
289
+ `authorityRef`, gated by an active `AuthorityTrace` at decision time
290
+ (`status-function.md` Step 1). This document does not introduce a new
291
+ "Dispute" resource. Reusing `VerificationEvent` + `AuthorityTrace` means a
292
+ cross-producer dispute is resolved exactly the same way a single-producer one
293
+ is — the resolving event just needs `claimId` pointed at whichever specific
294
+ claim the authority is ruling on (the fold is per-claim; resolving "the
295
+ subject+field disagreement" in general means issuing a resolution event on
296
+ each affected claim id, or issuing one and letting a `DerivationRule` compose
297
+ the pair — no new bulk-resolution primitive is added by this document).
298
+
299
+ ---
300
+
301
+ ## 8. Id collision handling for records that are NOT the same logical claim
302
+
303
+ This is the case where two producers, without coordinating, mint the
304
+ identical `id` string for two *unrelated* records (accidental collision —
305
+ distinct from §4's "same logical claim, different ids" case, and distinct
306
+ from §7's value/status conflict between claims that *are* the same logical
307
+ claim).
308
+
309
+ - **Detection:** compare content; identical content is not a problem
310
+ (idempotent re-merge); differing content under the same id is a collision
311
+ that MUST be surfaced (`mergeBundles` throws for claims;
312
+ `mergeBundlesDetailed` reports for every collection).
313
+ - **Mitigation is the id convention (§3), not a new mechanism.** A collision
314
+ between two truly unrelated records is only possible if both producers
315
+ independently chose the same opaque string. The producer-prefixed dotted
316
+ convention (e.g. `producer-a.recommendation.upgrade-node` vs.
317
+ `producer-b.candidate.upgrade-node`) makes this vanishingly unlikely
318
+ without any schema enforcement. This document does not add a registry,
319
+ reservation scheme, or uniqueness authority — that would introduce the
320
+ kind of cross-producer coordination infrastructure the "stand-alone,
321
+ vendor-neutral format" goal explicitly rules out.
322
+
323
+ ---
324
+
325
+ ## 9. Explicitly out of scope
326
+
327
+ - **`TransparencyGap` / `EvidenceRequirement` normative JSON Schemas.**
328
+ Referenced descriptively in §7b/§7c (the `contradiction` gap type already
329
+ exists informally, per `schemas/trust-report.schema.json`'s own
330
+ `$comment`), but this document does not add a schema for them.
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.
339
+ - **Cryptographic producer identity (DIDs, keys, transparency-log-anchored
340
+ identity).** `producerId` (§2) is deliberately unsigned and unverified.
341
+ Where verifiable producer identity is needed, use Assurance L1/L2
342
+ (`assurance.md`) — this document adds no new identity/signing mechanism.
343
+ - **Survey chains, Veritas standards, Flow gates** — unchanged; still
344
+ extension-profile territory per README's existing "Out of scope" section.
345
+
346
+ ---
347
+
348
+ ## Prior art
349
+
350
+ - **W3C Verifiable Credentials.** VC issuer identity is built on DIDs — a
351
+ resolvable, typically key-based identifier scheme. Requiring DIDs for
352
+ `producerId` would collapse the existing layered design (Assurance
353
+ L0/producer-asserted is the default; L1/L2 signing is opt-in) into "every
354
+ producer needs key infrastructure just to be namespaced for merge," a
355
+ strictly higher bar than merge needs. `producerId` is deliberately at the
356
+ same trust level as the existing `source` field (L0, free-text, unsigned).
357
+ - **in-toto.** `interop-in-toto.md` already wraps a whole `TrustBundle` as one
358
+ in-toto `Statement`'s `predicate` — in-toto's subject/predicate model is
359
+ single-attestation by design and defines no multi-producer merge algorithm
360
+ at the claim level. This document is compatible with that profile
361
+ unchanged: merge happens on `TrustBundle`s *before* DSSE wrapping, or a
362
+ verifier can independently wrap several signed Statements' predicates and
363
+ merge them after unwrapping — either order works because merge is a pure
364
+ function over `TrustBundle` values, not over signed envelopes.
365
+
366
+ ---
367
+
368
+ ## Reference implementation notes (for implementers)
369
+
370
+ | Normative rule (this document) | Where it lands in `@kontourai/surface` `src/` | Status |
371
+ |---|---|---|
372
+ | §2 `producerId` field | `src/types.ts` `TrustBundle` interface; `schemas/trust-bundle.schema.json` (this repo) | New — add optional field (not yet in the reference implementation) |
373
+ | §3 id convention | Prose-only; no code change (SHOULD, unenforced) | N/A |
374
+ | §4 claim identity across producers | `src/canonical.ts` `canonicalClaimKey` + `src/identity.ts` `buildIdentityIndex` | Reused unchanged |
375
+ | §5 rule 1–2 (union by id, first-occurrence-wins-if-identical, collision on differing content) | `src/merge.ts` `unionById` / `unionOptionalById` | Implemented |
376
+ | §5 rule 3 (`producerId` omitted on merge) | `src/merge.ts` `mergeBundlesDetailed` (the `source` synthesis block) | Implementer TODO: add omission of `producerId` |
377
+ | §6 determinism / order-independence | `src/merge.ts` `unionById` | **Known gap**: current `unionById` only compares against the first-seen record for a given id, not every colliding record — kept-content and the collision set are both order-dependent today for 3+-way collisions on one id. Not yet true; needs an implementation fix. |
378
+ | §6 tie-break (canonical-serialization ordering) | `src/merge.ts` `sameContent` | Implementer TODO: no multi-way tie-break exists yet, since there's no multi-way comparison yet |
379
+ | §7b value conflict → `contradiction` gap | `src/conflict-derivation.ts` `deriveConflictTransparencyGaps` | Implemented |
380
+ | §7c status conflict | `src/conflict-derivation.ts` (no code change needed — the code already matches this document's narrower rule) | Implemented; `docs/adr/0002-trust-bundle.md` in `@kontourai/surface` previously described a broader behavior and has a correction note |
381
+ | §7d dispute resolution | `src/dispute.ts` `buildDisputeResolutionEvent`; `status-function.md` Step 1 | Implemented |
382
+ | §8 collision detection | `src/merge.ts` `MergeCollision` (a TS type; not currently a normative wire schema, and stays that way per §9) | Implemented |
383
+ | Conformance vectors | `conformance/merge/*.json` (this repo) | Vector shape/schema validated by this repo's `npm test`; algorithmic correctness is proven by an implementation actually running `mergeBundlesDetailed`/`deriveClaimStatus` against them, not by this repo alone |
384
+
385
+ ---
386
+
387
+ ## Versioning
388
+
389
+ This document introduces no change to `statusFunctionVersion` (stays `"2"`)
390
+ and no change to `schemaVersion`'s meaning (stays `4` — the new
391
+ `TrustBundle.producerId` field is optional and ignored by the unchanged
392
+ status-derivation fold). A bundle merged under this document and fed to the
393
+ unchanged fold produces identical per-claim results to a bundle that was
394
+ never merged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hachure",
3
- "version": "0.6.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",
@@ -16,9 +16,11 @@
16
16
  "index.mjs",
17
17
  "README.md",
18
18
  "status-function.md",
19
+ "merge.md",
19
20
  "interop-in-toto.md",
20
21
  "verification-endpoint.md",
21
- "assurance.md"
22
+ "assurance.md",
23
+ "SECURITY.md"
22
24
  ],
23
25
  "scripts": {
24
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,13 +1,18 @@
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"],
8
8
  "properties": {
9
9
  "schemaVersion": { "enum": [2, 3, 4] },
10
10
  "source": { "type": "string" },
11
+ "producerId": {
12
+ "type": "string",
13
+ "minLength": 1,
14
+ "description": "Optional stable identifier for the producing system, distinct from source (which is free-text and may vary per run). When present, MUST be a non-empty string (minLength: 1) -- see merge.md section 2. Omitted (never synthesized) on a merged bundle."
15
+ },
11
16
  "claims": {
12
17
  "type": "array",
13
18
  "items": { "$ref": "claim.schema.json" }
@@ -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