hachure 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +79 -3
- package/SECURITY.md +180 -0
- package/assurance.md +1 -0
- package/conformance/README.md +48 -0
- package/conformance/manifest.json +71 -0
- package/conformance/merge/merge-agree-values.json +59 -0
- package/conformance/merge/merge-collision-order-independence.json +85 -0
- package/conformance/merge/merge-conflict-status.json +149 -0
- package/conformance/merge/merge-conflict-value.json +100 -0
- package/index.mjs +22 -1
- package/interop-in-toto.md +1 -0
- package/merge.md +394 -0
- package/package.json +4 -2
- package/schemas/claim.schema.json +2 -2
- package/schemas/derivation-rule.schema.json +2 -2
- package/schemas/evidence.schema.json +2 -2
- package/schemas/inquiry-record.schema.json +2 -2
- package/schemas/trust-bundle.schema.json +7 -2
- package/schemas/trust-report.schema.json +2 -2
- package/schemas/verification-event.schema.json +2 -2
- package/schemas/verification-policy.schema.json +2 -2
- package/status-function.md +1 -0
- package/verification-endpoint.md +1 -0
package/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.
|
|
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://
|
|
4
|
-
"title": "
|
|
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://
|
|
4
|
-
"title": "
|
|
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://
|
|
4
|
-
"title": "
|
|
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://
|
|
4
|
-
"title": "
|
|
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://
|
|
4
|
-
"title": "
|
|
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://
|
|
4
|
-
"title": "
|
|
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://
|
|
4
|
-
"title": "
|
|
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://
|
|
4
|
-
"title": "
|
|
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": {
|
package/status-function.md
CHANGED
|
@@ -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
|
|
package/verification-endpoint.md
CHANGED
|
@@ -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
|
|