hachure 0.5.1 → 0.7.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/README.md +16 -4
- package/conformance/README.md +36 -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/merge.md +364 -0
- package/package.json +6 -2
- package/schemas/claim.schema.json +16 -2
- package/schemas/derivation-rule.schema.json +1 -1
- package/schemas/trust-bundle.schema.json +34 -24
- package/schemas/trust-report.schema.json +86 -0
- package/schemas/verification-policy.schema.json +2 -2
package/README.md
CHANGED
|
@@ -104,8 +104,14 @@ Plain-language definition (ADR 0002):
|
|
|
104
104
|
> the producer played by — packed so it can cross a product boundary without the
|
|
105
105
|
> receiver needing access to the producer's internals.
|
|
106
106
|
|
|
107
|
-
The `source` field identifies the producer
|
|
108
|
-
|
|
107
|
+
The `source` field identifies the producer (free-text, may vary per run); an optional
|
|
108
|
+
`producerId` field carries a stable, unsigned identifier for the producing system,
|
|
109
|
+
consistent across every bundle it emits. When present, `producerId` MUST be a
|
|
110
|
+
non-empty string. Bundles from multiple producers can be merged
|
|
111
|
+
into one ledger without last-write-wins and without deleting losing evidence; conflicts
|
|
112
|
+
between claims are surfaced as `contradiction` transparency gaps, never silently
|
|
113
|
+
resolved or used to flip a claim's status. The full specification of identifier
|
|
114
|
+
conventions and the merge algorithm is in [merge.md](merge.md).
|
|
109
115
|
|
|
110
116
|
An optional `identityLinks` array declares co-referent subjects — real-world entities
|
|
111
117
|
known under more than one identifier. Each link carries a stable optional `id`, a
|
|
@@ -128,6 +134,11 @@ anchors, policy references, derivation edges, and confidence basis metadata.
|
|
|
128
134
|
Derived trust status is never stored on the claim itself as source of truth; it is
|
|
129
135
|
computed from the surrounding bundle at evaluation time.
|
|
130
136
|
|
|
137
|
+
Claims also carry two optional round-trip fields, tolerated but never producer-authored:
|
|
138
|
+
`producerStatus` (the producer's own declared status, present when a TrustReport's
|
|
139
|
+
derived claims are re-fed as bundle input) and `freshness` ({ `asOf`, `expiresAt`?,
|
|
140
|
+
`stale` }, a freshness stamp on derived/report claims).
|
|
141
|
+
|
|
131
142
|
### Evidence
|
|
132
143
|
|
|
133
144
|
An item of support for a claim. Evidence is linked to a claim via `claimId`. Each
|
|
@@ -186,7 +197,7 @@ re-evaluation if the derivation algorithm changes.
|
|
|
186
197
|
### DerivationRule
|
|
187
198
|
|
|
188
199
|
A named, versioned rule that derives a boolean answer from existing claims (ADR 0003 §5).
|
|
189
|
-
Rules compose claims using value predicates (`eq`, `gt`, `gte`, `lte`, `in`, `exists`)
|
|
200
|
+
Rules compose claims using value predicates (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `exists`)
|
|
190
201
|
and status predicates (`acceptedStatuses`), combined with `"all"` or `"any"`. Rules
|
|
191
202
|
are promoted from Flow's gate-expectation language. The weakest-link confidence ceiling
|
|
192
203
|
propagates through rule evaluation unchanged.
|
|
@@ -198,7 +209,7 @@ propagates through rule evaluation unchanged.
|
|
|
198
209
|
Status is a pure, versioned function of the bundle data and a `now` timestamp. The
|
|
199
210
|
full specification of the derivation algorithm is in [status-function.md](status-function.md).
|
|
200
211
|
|
|
201
|
-
The
|
|
212
|
+
The nine possible statuses:
|
|
202
213
|
|
|
203
214
|
| Status | Meaning |
|
|
204
215
|
|---|---|
|
|
@@ -210,6 +221,7 @@ The eight possible statuses:
|
|
|
210
221
|
| `disputed` | A verified claim has blocking contradicting evidence, or a terminal dispute event exists. |
|
|
211
222
|
| `superseded` | A terminal event marks the claim as superseded. |
|
|
212
223
|
| `rejected` | A terminal event marks the claim as rejected. |
|
|
224
|
+
| `revoked` | An explicit invalidation event has revoked the claim's verification. For single-claim status derivation this folds to `stale` (see [status-function.md](status-function.md), Step 2) unless a later verification event re-asserts the claim; the reference implementation still tracks `revoked` as a distinct, weakest-ranked raw status for `Claim.status`/`VerificationEvent.status`, claim-group rollups, and weakest-link ordering. |
|
|
213
225
|
|
|
214
226
|
---
|
|
215
227
|
|
package/conformance/README.md
CHANGED
|
@@ -32,3 +32,39 @@ implementation derives the expected statuses.
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
|
+
|
|
36
|
+
## Merge conformance vectors
|
|
37
|
+
|
|
38
|
+
`conformance/merge/` contains a second, distinct family of vectors that make
|
|
39
|
+
the [Identifier & Multi-Producer Merge Semantics specification](../merge.md)
|
|
40
|
+
executable. Each vector merges two or more input `TrustBundle`s and asserts
|
|
41
|
+
the merged claim-id set, any id collisions, and the per-claim status derived
|
|
42
|
+
independently on the merged bundle. This repo's `npm test` (`test/merge.test.mjs`)
|
|
43
|
+
validates vector *shape* and Ajv-validates every `inputs[]` entry against
|
|
44
|
+
`trust-bundle.schema.json`; it does not execute `mergeBundles`/
|
|
45
|
+
`mergeBundlesDetailed`/`deriveClaimStatus` (this repo carries no
|
|
46
|
+
`@kontourai/surface` dependency) — see `merge.md`'s "Reference implementation
|
|
47
|
+
notes" for the implementation-side conformance loop.
|
|
48
|
+
|
|
49
|
+
### Merge test vector inventory
|
|
50
|
+
|
|
51
|
+
| File | Scenario | Now |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `merge-agree-values.json` | Two producers' claims agree on the same canonical subject+field; both retained as distinct records, both derive their own status independently | 2026-06-10T00:00:00.000Z |
|
|
54
|
+
| `merge-conflict-value.json` | Two producers' claims disagree on value, governed by a shared `incompatibleValues` policy; both retained, statuses computed independently | 2026-06-10T00:00:00.000Z |
|
|
55
|
+
| `merge-conflict-status.json` | Producer A's claim reaches `disputed` via its own blocking evidence; producer B's claim independently reaches `verified` — merge does not let one overwrite or suppress the other | 2026-06-10T00:00:00.000Z |
|
|
56
|
+
| `merge-collision-order-independence.json` | Three bundles; one `Claim.id` shared by two with genuinely different content (accidental collision) plus one unrelated bundle; asserts the merge result (kept content + collisions) is identical for every permutation of `inputs` | 2026-06-10T00:00:00.000Z |
|
|
57
|
+
|
|
58
|
+
### Merge test vector format
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"now": "<ISO 8601 string>",
|
|
63
|
+
"inputs": [ /* TrustBundle, TrustBundle, ... */ ],
|
|
64
|
+
"expect": {
|
|
65
|
+
"mergedClaimIds": ["<id>", "..."],
|
|
66
|
+
"collisions": [{ "collection": "claims", "id": "<id>" }],
|
|
67
|
+
"statusByClaimId": { "<claimId>": "<TrustStatus>" }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Hand-derivation (merge.md §4/§5/§7a; status-function.md Step 6). Bundle A (producer-a) and Bundle B (producer-b) each carry one claim with the same subjectType/subjectId ('repo'/'repo-1') and the same fieldOrBehavior ('coverage'), no qualifiers on either side -> canonicalClaimKey equal (§4) -> same logical claim. Values are deep-equal (91 === 91) -> agreement (§7a): both claims MUST be retained as distinct records (never collapsed), so mergedClaimIds contains both distinct ids. Neither claims/evidence/policies/events share an id across the two bundles, so collisions is empty. Per-claim status: neither bundle attaches any policy or evidence/events for its claim, so status-function.md Step 6 ('No policy') applies for both -> policy is undefined and evidence.length === 0 -> 'unknown' for both, independently of one another and of the agreement itself (merge does not synthesize a stronger status from agreement, §7a).",
|
|
3
|
+
"now": "2026-06-10T00:00:00.000Z",
|
|
4
|
+
"inputs": [
|
|
5
|
+
{
|
|
6
|
+
"schemaVersion": 4,
|
|
7
|
+
"source": "producer-a:run-1",
|
|
8
|
+
"producerId": "producer-a",
|
|
9
|
+
"claims": [
|
|
10
|
+
{
|
|
11
|
+
"id": "producer-a.claim.readiness.coverage",
|
|
12
|
+
"subjectType": "repo",
|
|
13
|
+
"subjectId": "repo-1",
|
|
14
|
+
"surface": "readiness",
|
|
15
|
+
"claimType": "coverage",
|
|
16
|
+
"fieldOrBehavior": "coverage",
|
|
17
|
+
"value": 91,
|
|
18
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
19
|
+
"updatedAt": "2026-06-01T00:00:00.000Z"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"evidence": [],
|
|
23
|
+
"policies": [],
|
|
24
|
+
"events": []
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"schemaVersion": 4,
|
|
28
|
+
"source": "producer-b:run-7",
|
|
29
|
+
"producerId": "producer-b",
|
|
30
|
+
"claims": [
|
|
31
|
+
{
|
|
32
|
+
"id": "producer-b.claim.readiness.coverage",
|
|
33
|
+
"subjectType": "repo",
|
|
34
|
+
"subjectId": "repo-1",
|
|
35
|
+
"surface": "readiness",
|
|
36
|
+
"claimType": "coverage",
|
|
37
|
+
"fieldOrBehavior": "coverage",
|
|
38
|
+
"value": 91,
|
|
39
|
+
"createdAt": "2026-06-02T00:00:00.000Z",
|
|
40
|
+
"updatedAt": "2026-06-02T00:00:00.000Z"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"evidence": [],
|
|
44
|
+
"policies": [],
|
|
45
|
+
"events": []
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"expect": {
|
|
49
|
+
"mergedClaimIds": [
|
|
50
|
+
"producer-a.claim.readiness.coverage",
|
|
51
|
+
"producer-b.claim.readiness.coverage"
|
|
52
|
+
],
|
|
53
|
+
"collisions": [],
|
|
54
|
+
"statusByClaimId": {
|
|
55
|
+
"producer-a.claim.readiness.coverage": "unknown",
|
|
56
|
+
"producer-b.claim.readiness.coverage": "unknown"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Hand-derivation (merge.md §5/§6/§8). This vector's inputs/expect are STRUCTURALLY identical to .kontourai/flow-agents/hachure-identifier-merge/design.md §11 'Worked example + independent hand-derivation' -- the bundle-level source/producerId values were genericized from design.md §11's worked example (survey -> producer-a, veritas -> producer-b, flow -> producer-c; mapping recorded in this session's deliver.md History) to keep this shipped, machine-checked conformance fixture vendor-neutral, consistent with the rest of this repo's conformance vectors and merge.md's own examples (plan.md AC7). This rename does NOT change expect: source/producerId are bundle-level fields, not part of the Claim record being compared, so the tie-break outcome below is unaffected -- re-derived by hand against the renamed inputs. shared.claim.x appears in Bundle A (subjectId r1, surface readiness, claimType coverage, value 91) and Bundle B (subjectId r2, surface governance, claimType policy-check, value true) -- content differs under the same id, so per merge.md §5 rule 2 this is a collision, reported as {collection: 'claims', id: 'shared.claim.x'}. Per merge.md §6's tie-break, compute a canonical (sorted-key) serialization of each variant: A's serialization starts '{\\\"claimType\\\":\\\"coverage\\\",...}', B's starts '{\\\"claimType\\\":\\\"policy-check\\\",...}' -- lexicographically 'coverage' < 'policy-check' ('c' < 'p'), so A's record is the deterministically-kept content regardless of whether the implementation is handed [A,B,C], [B,A,C], [C,B,A], or any other permutation of the 3 inputs -- this is the vector that exercises the order-independence MUST (test/merge.test.mjs asserts identical output across every permutation of inputs). Bundle C's claim ('unrelated.claim.y') never collides with anything, and its id is a distinct string never repeated. Status-function.md Step 6 ('no policy, no evidence') applies to both surviving claim ids ('shared.claim.x' from kept content A, and 'unrelated.claim.y') since no bundle attaches any policy or evidence/events -> both derive 'unknown'.",
|
|
3
|
+
"now": "2026-06-10T00:00:00.000Z",
|
|
4
|
+
"inputs": [
|
|
5
|
+
{
|
|
6
|
+
"schemaVersion": 4,
|
|
7
|
+
"source": "producer-a",
|
|
8
|
+
"producerId": "producer-a",
|
|
9
|
+
"claims": [
|
|
10
|
+
{
|
|
11
|
+
"id": "shared.claim.x",
|
|
12
|
+
"subjectType": "repo",
|
|
13
|
+
"subjectId": "r1",
|
|
14
|
+
"surface": "readiness",
|
|
15
|
+
"claimType": "coverage",
|
|
16
|
+
"fieldOrBehavior": "coverage",
|
|
17
|
+
"value": 91,
|
|
18
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
19
|
+
"updatedAt": "2026-06-01T00:00:00.000Z"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"evidence": [],
|
|
23
|
+
"policies": [],
|
|
24
|
+
"events": []
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"schemaVersion": 4,
|
|
28
|
+
"source": "producer-b",
|
|
29
|
+
"producerId": "producer-b",
|
|
30
|
+
"claims": [
|
|
31
|
+
{
|
|
32
|
+
"id": "shared.claim.x",
|
|
33
|
+
"subjectType": "repo",
|
|
34
|
+
"subjectId": "r2",
|
|
35
|
+
"surface": "governance",
|
|
36
|
+
"claimType": "policy-check",
|
|
37
|
+
"fieldOrBehavior": "signed-off",
|
|
38
|
+
"value": true,
|
|
39
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
40
|
+
"updatedAt": "2026-06-01T00:00:00.000Z"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"evidence": [],
|
|
44
|
+
"policies": [],
|
|
45
|
+
"events": []
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"schemaVersion": 4,
|
|
49
|
+
"source": "producer-c",
|
|
50
|
+
"producerId": "producer-c",
|
|
51
|
+
"claims": [
|
|
52
|
+
{
|
|
53
|
+
"id": "unrelated.claim.y",
|
|
54
|
+
"subjectType": "gate",
|
|
55
|
+
"subjectId": "g1",
|
|
56
|
+
"surface": "gates",
|
|
57
|
+
"claimType": "gate-status",
|
|
58
|
+
"fieldOrBehavior": "passed",
|
|
59
|
+
"value": true,
|
|
60
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
61
|
+
"updatedAt": "2026-06-01T00:00:00.000Z"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"evidence": [],
|
|
65
|
+
"policies": [],
|
|
66
|
+
"events": []
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"expect": {
|
|
70
|
+
"mergedClaimIds": [
|
|
71
|
+
"shared.claim.x",
|
|
72
|
+
"unrelated.claim.y"
|
|
73
|
+
],
|
|
74
|
+
"collisions": [
|
|
75
|
+
{
|
|
76
|
+
"collection": "claims",
|
|
77
|
+
"id": "shared.claim.x"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"statusByClaimId": {
|
|
81
|
+
"shared.claim.x": "unknown",
|
|
82
|
+
"unrelated.claim.y": "unknown"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Hand-derivation (merge.md §7c; status-function.md Steps 1-4). Both claims share subjectType/subjectId ('resource'/'svc-1') and fieldOrBehavior ('granted') -> same canonical claim (§4). Both bundles carry the identical policy 'policy.access-check.basic' (byte-identical in both -> unioning it is not a collision), which additionally declares incompatibleStatuses covering ['verified','disputed'] purely as an illustration that this does NOT by itself flip either claim's status (§7c) -- neither this vector's expect block nor status-function.md's fold reads incompatibleStatuses at all; it only affects the report layer, out of scope here (§9). No ids collide across the two bundles -> collisions is empty. Fold for producer-a's claim: latestEvent is 'producer-a.event.access.verified' (status 'verified', not terminal, skip Steps 2-3) -> Step 4 applies. 4a: claim has no expiresAt/ttlSeconds, policy validityRule.kind='duration', durationDays=30, verifiedAt=2026-06-01T00:00:00Z, now=2026-06-10T00:00:00Z -> 9 days elapsed < 30 -> not stale. 4b: entailing evidence (supportStrength defaults to 'entails' on both evidence items) = [producer-a.evidence.access.pass, producer-a.evidence.access.fail]; their evidenceType set {source_excerpt} and method set {observation} both satisfy policy.requiredEvidence=['source_excerpt']/requiredMethods=['observation']; requiresCorroboration=false -> no gap. 4c: 'producer-a.evidence.access.fail' has passing=false and blocking=true, observedAt=2026-06-05 (after the verified event) -> blocking failure found -> return 'disputed'. Fold for producer-b's claim: latestEvent is 'producer-b.event.access.verified' (status 'verified') -> Step 4. 4a: verifiedAt=2026-06-02T00:00:00Z, now=2026-06-10T00:00:00Z -> 8 days < 30 -> not stale. 4b: entailing evidence = [producer-b.evidence.access.pass], evidenceType {source_excerpt}, method {observation} -> satisfies policy -> no gap. 4c: the one evidence item has no 'passing:false' entry (passing is absent) -> no blocking failure -> 4d: return 'verified'. This demonstrates merge does not let producer-b's 'verified' overwrite or suppress producer-a's independently-derived 'disputed', and vice versa.",
|
|
3
|
+
"now": "2026-06-10T00:00:00.000Z",
|
|
4
|
+
"inputs": [
|
|
5
|
+
{
|
|
6
|
+
"schemaVersion": 4,
|
|
7
|
+
"source": "producer-a:run-3",
|
|
8
|
+
"producerId": "producer-a",
|
|
9
|
+
"claims": [
|
|
10
|
+
{
|
|
11
|
+
"id": "producer-a.claim.access.grant",
|
|
12
|
+
"subjectType": "resource",
|
|
13
|
+
"subjectId": "svc-1",
|
|
14
|
+
"surface": "access",
|
|
15
|
+
"claimType": "access-check",
|
|
16
|
+
"fieldOrBehavior": "granted",
|
|
17
|
+
"value": true,
|
|
18
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
19
|
+
"updatedAt": "2026-06-01T00:00:00.000Z"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"evidence": [
|
|
23
|
+
{
|
|
24
|
+
"id": "producer-a.evidence.access.pass",
|
|
25
|
+
"claimId": "producer-a.claim.access.grant",
|
|
26
|
+
"evidenceType": "source_excerpt",
|
|
27
|
+
"method": "observation",
|
|
28
|
+
"sourceRef": "source A: access log",
|
|
29
|
+
"excerptOrSummary": "Access granted.",
|
|
30
|
+
"observedAt": "2026-06-01T00:00:00.000Z",
|
|
31
|
+
"collectedBy": "agent-a"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "producer-a.evidence.access.fail",
|
|
35
|
+
"claimId": "producer-a.claim.access.grant",
|
|
36
|
+
"evidenceType": "source_excerpt",
|
|
37
|
+
"method": "observation",
|
|
38
|
+
"sourceRef": "source A2: revocation log",
|
|
39
|
+
"excerptOrSummary": "Access revocation observed after original grant.",
|
|
40
|
+
"observedAt": "2026-06-05T00:00:00.000Z",
|
|
41
|
+
"collectedBy": "agent-a",
|
|
42
|
+
"passing": false,
|
|
43
|
+
"blocking": true
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"policies": [
|
|
47
|
+
{
|
|
48
|
+
"id": "policy.access-check.basic",
|
|
49
|
+
"claimType": "access-check",
|
|
50
|
+
"requiredEvidence": ["source_excerpt"],
|
|
51
|
+
"requiredMethods": ["observation"],
|
|
52
|
+
"requiresCorroboration": false,
|
|
53
|
+
"acceptanceCriteria": ["access confirmed from source"],
|
|
54
|
+
"reviewAuthority": "operator",
|
|
55
|
+
"validityRule": { "kind": "duration", "durationDays": 30 },
|
|
56
|
+
"stalenessTriggers": [],
|
|
57
|
+
"conflictRules": [],
|
|
58
|
+
"impactLevel": "medium",
|
|
59
|
+
"incompatibleStatuses": [
|
|
60
|
+
{ "statuses": ["verified", "disputed"], "message": "conflicting access status across producers" }
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"events": [
|
|
65
|
+
{
|
|
66
|
+
"id": "producer-a.event.access.verified",
|
|
67
|
+
"claimId": "producer-a.claim.access.grant",
|
|
68
|
+
"status": "verified",
|
|
69
|
+
"actor": "operator-a",
|
|
70
|
+
"method": "attestation",
|
|
71
|
+
"evidenceIds": ["producer-a.evidence.access.pass"],
|
|
72
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
73
|
+
"verifiedAt": "2026-06-01T00:00:00.000Z"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"schemaVersion": 4,
|
|
79
|
+
"source": "producer-b:run-4",
|
|
80
|
+
"producerId": "producer-b",
|
|
81
|
+
"claims": [
|
|
82
|
+
{
|
|
83
|
+
"id": "producer-b.claim.access.grant",
|
|
84
|
+
"subjectType": "resource",
|
|
85
|
+
"subjectId": "svc-1",
|
|
86
|
+
"surface": "access",
|
|
87
|
+
"claimType": "access-check",
|
|
88
|
+
"fieldOrBehavior": "granted",
|
|
89
|
+
"value": true,
|
|
90
|
+
"createdAt": "2026-06-02T00:00:00.000Z",
|
|
91
|
+
"updatedAt": "2026-06-02T00:00:00.000Z"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"evidence": [
|
|
95
|
+
{
|
|
96
|
+
"id": "producer-b.evidence.access.pass",
|
|
97
|
+
"claimId": "producer-b.claim.access.grant",
|
|
98
|
+
"evidenceType": "source_excerpt",
|
|
99
|
+
"method": "observation",
|
|
100
|
+
"sourceRef": "source B: access log",
|
|
101
|
+
"excerptOrSummary": "Access granted, confirmed.",
|
|
102
|
+
"observedAt": "2026-06-02T00:00:00.000Z",
|
|
103
|
+
"collectedBy": "agent-b"
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"policies": [
|
|
107
|
+
{
|
|
108
|
+
"id": "policy.access-check.basic",
|
|
109
|
+
"claimType": "access-check",
|
|
110
|
+
"requiredEvidence": ["source_excerpt"],
|
|
111
|
+
"requiredMethods": ["observation"],
|
|
112
|
+
"requiresCorroboration": false,
|
|
113
|
+
"acceptanceCriteria": ["access confirmed from source"],
|
|
114
|
+
"reviewAuthority": "operator",
|
|
115
|
+
"validityRule": { "kind": "duration", "durationDays": 30 },
|
|
116
|
+
"stalenessTriggers": [],
|
|
117
|
+
"conflictRules": [],
|
|
118
|
+
"impactLevel": "medium",
|
|
119
|
+
"incompatibleStatuses": [
|
|
120
|
+
{ "statuses": ["verified", "disputed"], "message": "conflicting access status across producers" }
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"events": [
|
|
125
|
+
{
|
|
126
|
+
"id": "producer-b.event.access.verified",
|
|
127
|
+
"claimId": "producer-b.claim.access.grant",
|
|
128
|
+
"status": "verified",
|
|
129
|
+
"actor": "operator-b",
|
|
130
|
+
"method": "attestation",
|
|
131
|
+
"evidenceIds": ["producer-b.evidence.access.pass"],
|
|
132
|
+
"createdAt": "2026-06-02T00:00:00.000Z",
|
|
133
|
+
"verifiedAt": "2026-06-02T00:00:00.000Z"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
"expect": {
|
|
139
|
+
"mergedClaimIds": [
|
|
140
|
+
"producer-a.claim.access.grant",
|
|
141
|
+
"producer-b.claim.access.grant"
|
|
142
|
+
],
|
|
143
|
+
"collisions": [],
|
|
144
|
+
"statusByClaimId": {
|
|
145
|
+
"producer-a.claim.access.grant": "disputed",
|
|
146
|
+
"producer-b.claim.access.grant": "verified"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Hand-derivation (merge.md §4/§5/§7b; status-function.md Step 7). Both claims share subjectType/subjectId ('repo'/'repo-2') and fieldOrBehavior ('tier'), no qualifiers -> same canonical claim key (§4). Values 'gold' (producer-a) and 'silver' (producer-b) are not deep-equal, and both bundles reference the identical policy 'policy.pricing-field.tier-conflict', which declares incompatibleValues covering ['gold','silver'] -> this is a value conflict (§7b): both claims MUST be retained (mergedClaimIds has both distinct ids); the conflict itself surfaces only as a report-layer contradiction transparency gap, which is out of this vector format's scope (merge.md §9) and does NOT affect either claim's own status computation. The policy object is byte-identical in both bundles under the same id, so unioning it is not a collision (merge.md §5 rule 2) -> collisions is empty. Status-function.md fold, per claim: no verification event in either bundle (skip Steps 1-5); a policy IS resolved (claimType 'pricing-field' matches) so Step 6 ('no policy') does not apply; Step 7 applies ('policy present, no verification event'): producer-a's claim has one entailing evidence item with evidenceType 'source_excerpt', so its evidence-type set is {source_excerpt}, which is a superset of policy.requiredEvidence=['source_excerpt'] -> 'proposed'. producer-b's claim has no evidence at all, so its evidence-type set is {} which does NOT contain 'source_excerpt' -> 'unknown'. The two claims resolve to different statuses purely from their own attached evidence, independent of the value conflict between them.",
|
|
3
|
+
"now": "2026-06-10T00:00:00.000Z",
|
|
4
|
+
"inputs": [
|
|
5
|
+
{
|
|
6
|
+
"schemaVersion": 4,
|
|
7
|
+
"source": "producer-a:run-2",
|
|
8
|
+
"producerId": "producer-a",
|
|
9
|
+
"claims": [
|
|
10
|
+
{
|
|
11
|
+
"id": "producer-a.claim.pricing.tier",
|
|
12
|
+
"subjectType": "repo",
|
|
13
|
+
"subjectId": "repo-2",
|
|
14
|
+
"surface": "pricing",
|
|
15
|
+
"claimType": "pricing-field",
|
|
16
|
+
"fieldOrBehavior": "tier",
|
|
17
|
+
"value": "gold",
|
|
18
|
+
"createdAt": "2026-06-01T00:00:00.000Z",
|
|
19
|
+
"updatedAt": "2026-06-01T00:00:00.000Z"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"evidence": [
|
|
23
|
+
{
|
|
24
|
+
"id": "producer-a.evidence.tier.source",
|
|
25
|
+
"claimId": "producer-a.claim.pricing.tier",
|
|
26
|
+
"evidenceType": "source_excerpt",
|
|
27
|
+
"method": "observation",
|
|
28
|
+
"sourceRef": "source A: internal pricing sheet",
|
|
29
|
+
"excerptOrSummary": "Tier is gold.",
|
|
30
|
+
"observedAt": "2026-06-01T00:00:00.000Z",
|
|
31
|
+
"collectedBy": "crawler-a"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"policies": [
|
|
35
|
+
{
|
|
36
|
+
"id": "policy.pricing-field.tier-conflict",
|
|
37
|
+
"claimType": "pricing-field",
|
|
38
|
+
"requiredEvidence": ["source_excerpt"],
|
|
39
|
+
"acceptanceCriteria": ["tier confirmed by source"],
|
|
40
|
+
"reviewAuthority": "operator",
|
|
41
|
+
"validityRule": { "kind": "historical" },
|
|
42
|
+
"stalenessTriggers": [],
|
|
43
|
+
"conflictRules": [],
|
|
44
|
+
"impactLevel": "medium",
|
|
45
|
+
"incompatibleValues": [
|
|
46
|
+
{ "values": ["gold", "silver"], "message": "tier values conflict across producers" }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"events": []
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"schemaVersion": 4,
|
|
54
|
+
"source": "producer-b:run-9",
|
|
55
|
+
"producerId": "producer-b",
|
|
56
|
+
"claims": [
|
|
57
|
+
{
|
|
58
|
+
"id": "producer-b.claim.pricing.tier",
|
|
59
|
+
"subjectType": "repo",
|
|
60
|
+
"subjectId": "repo-2",
|
|
61
|
+
"surface": "pricing",
|
|
62
|
+
"claimType": "pricing-field",
|
|
63
|
+
"fieldOrBehavior": "tier",
|
|
64
|
+
"value": "silver",
|
|
65
|
+
"createdAt": "2026-06-02T00:00:00.000Z",
|
|
66
|
+
"updatedAt": "2026-06-02T00:00:00.000Z"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"evidence": [],
|
|
70
|
+
"policies": [
|
|
71
|
+
{
|
|
72
|
+
"id": "policy.pricing-field.tier-conflict",
|
|
73
|
+
"claimType": "pricing-field",
|
|
74
|
+
"requiredEvidence": ["source_excerpt"],
|
|
75
|
+
"acceptanceCriteria": ["tier confirmed by source"],
|
|
76
|
+
"reviewAuthority": "operator",
|
|
77
|
+
"validityRule": { "kind": "historical" },
|
|
78
|
+
"stalenessTriggers": [],
|
|
79
|
+
"conflictRules": [],
|
|
80
|
+
"impactLevel": "medium",
|
|
81
|
+
"incompatibleValues": [
|
|
82
|
+
{ "values": ["gold", "silver"], "message": "tier values conflict across producers" }
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"events": []
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"expect": {
|
|
90
|
+
"mergedClaimIds": [
|
|
91
|
+
"producer-a.claim.pricing.tier",
|
|
92
|
+
"producer-b.claim.pricing.tier"
|
|
93
|
+
],
|
|
94
|
+
"collisions": [],
|
|
95
|
+
"statusByClaimId": {
|
|
96
|
+
"producer-a.claim.pricing.tier": "proposed",
|
|
97
|
+
"producer-b.claim.pricing.tier": "unknown"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/merge.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
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
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Principle
|
|
10
|
+
|
|
11
|
+
A Trust Bundle (README §"TrustBundle") is the supply side of the ledger, from
|
|
12
|
+
a single producer (ADR 0002). Multiple producers' bundles about overlapping
|
|
13
|
+
subjects MUST be combinable into one ledger without:
|
|
14
|
+
|
|
15
|
+
- silently overwriting one producer's claim with another's (never
|
|
16
|
+
last-write-wins),
|
|
17
|
+
- deleting losing evidence,
|
|
18
|
+
- requiring a shared identifier authority, key infrastructure, or
|
|
19
|
+
pre-registration between producers.
|
|
20
|
+
|
|
21
|
+
This document specifies: how a claim's identity is compared across producers
|
|
22
|
+
(§4), how bundles fold into one ledger (§5), the determinism guarantee that
|
|
23
|
+
folding MUST satisfy (§6), how agreement/conflict/dispute are represented
|
|
24
|
+
(§7), and how accidental id collisions between *unrelated* records are
|
|
25
|
+
detected (§8).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Producer identity
|
|
30
|
+
|
|
31
|
+
`TrustBundle.source` (`schemas/trust-bundle.schema.json`) is a free-text
|
|
32
|
+
string. Real producers use it inconsistently as a human-readable label, a
|
|
33
|
+
run-scoped value, or both (e.g. `source: 'producer-b:${run_id}'`,
|
|
34
|
+
`source: 'session-log'`, `source: 'filesystem-inferred'`). `source`
|
|
35
|
+
alone is not a stable, comparable producer identity — it changes per
|
|
36
|
+
run/session for the same producer.
|
|
37
|
+
|
|
38
|
+
`TrustBundle` carries one OPTIONAL field, `producerId` (string), a stable
|
|
39
|
+
identifier for the *system* that produced the bundle, distinct from
|
|
40
|
+
`source`'s run-scoped free text:
|
|
41
|
+
|
|
42
|
+
```jsonc
|
|
43
|
+
{
|
|
44
|
+
"schemaVersion": 4,
|
|
45
|
+
"source": "producer-a:run-48213", // unchanged: free text, may vary per run
|
|
46
|
+
"producerId": "producer-a", // OPTIONAL, new: stable across runs
|
|
47
|
+
"claims": [ /* ... */ ]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Rules:
|
|
52
|
+
|
|
53
|
+
- `producerId` is OPTIONAL. A bundle without it is exactly as valid as a
|
|
54
|
+
bundle that predates this field (additive; `trust-bundle.schema.json`'s
|
|
55
|
+
`required` array is unchanged).
|
|
56
|
+
- When present, `producerId` MUST be a non-empty string
|
|
57
|
+
(`trust-bundle.schema.json`'s `producerId` property carries `minLength: 1`)
|
|
58
|
+
— an empty string carries no identifying information, so it is
|
|
59
|
+
schema-invalid rather than treated as equivalent to omitting the field.
|
|
60
|
+
- When present, `producerId` SHOULD be stable across every bundle the same
|
|
61
|
+
system emits, and SHOULD be used (§3) as the leading segment of that
|
|
62
|
+
producer's record ids.
|
|
63
|
+
- `producerId` carries no cryptographic weight. It is an L0
|
|
64
|
+
(producer-asserted) fact in Assurance-profile terms (`assurance.md`).
|
|
65
|
+
Producers wanting a verifiable producer identity SHOULD present that
|
|
66
|
+
identity via the existing Assurance L1 (OIDC-backed) or L2 (held-key)
|
|
67
|
+
presentation (`assurance.md` §"Identity presentation"). This document does
|
|
68
|
+
not define, and MUST NOT be read to require, any DID or key-resolution
|
|
69
|
+
mechanism. Cryptographic identity is Assurance-profile territory;
|
|
70
|
+
`producerId` is the plain, unsigned, always-available floor underneath it.
|
|
71
|
+
- On merge (§5), a merged bundle represents more than one producer, so a
|
|
72
|
+
merged bundle's `producerId` MUST be omitted — it MUST NOT be synthesized
|
|
73
|
+
the way `source` is (`source` becomes `merged:<a>+<b>`; `producerId` has no
|
|
74
|
+
analogous synthesized form). Per-record producer attribution across a merge
|
|
75
|
+
is best-effort via the id convention in §3, not a schema-enforced field on
|
|
76
|
+
every record; `Claim`, `Evidence`, `VerificationPolicy`, and
|
|
77
|
+
`VerificationEvent` do not each carry their own `producerId` — the
|
|
78
|
+
bundle-level field plus the id convention is the complete mechanism.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 3. Identifier format
|
|
83
|
+
|
|
84
|
+
`id` fields (`Claim.id`, `Evidence.id`, `VerificationPolicy.id`,
|
|
85
|
+
`VerificationEvent.id`, etc.) remain `{ "type": "string" }` with no `pattern`
|
|
86
|
+
constraint. This document introduces no schema change to any `id` field.
|
|
87
|
+
|
|
88
|
+
- Producers SHOULD mint ids as dot-separated, lowercase, URL-safe segments
|
|
89
|
+
(a stable helper that lowercases, collapses non-alphanumeric runs to `-`,
|
|
90
|
+
and joins segments with `.` is the recommended shape).
|
|
91
|
+
- Producers that set `producerId` (§2) SHOULD make the id's leading segment
|
|
92
|
+
equal to `producerId` (or a short slug derived from it), e.g.
|
|
93
|
+
`producerId: "producer-a"` → ids like `producer-a.recommendation.upgrade-node`.
|
|
94
|
+
- This is a SHOULD, not a MUST, and is never schema-enforced. A conforming
|
|
95
|
+
bundle with un-prefixed ids remains fully conformant.
|
|
96
|
+
- Rationale for SHOULD over MUST: enforcing a producer prefix would need a
|
|
97
|
+
`pattern` regex, which cannot be written today without either rejecting
|
|
98
|
+
real existing ids or being so permissive it adds no safety. The prefix
|
|
99
|
+
convention earns its value from making *accidental* id collisions between
|
|
100
|
+
unrelated producers vanishingly unlikely (§8), not from schema enforcement.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 4. Claim identity across producers
|
|
105
|
+
|
|
106
|
+
Two claims from different producers are the same logical claim (candidates
|
|
107
|
+
for agreement/conflict comparison, §7) **if and only if:**
|
|
108
|
+
|
|
109
|
+
1. Their subjects resolve to the same canonical key under the merged bundle's
|
|
110
|
+
identity index (`IdentityIndex.canonicalKeyForClaim`) — i.e. same subject,
|
|
111
|
+
or subjects declared co-referent via `identityLinks`/`subjectAliases`.
|
|
112
|
+
2. `canonicalClaimKey({ subjectType, subjectId, fieldOrBehavior, qualifiers })`
|
|
113
|
+
is equal once (1) is applied (same `fieldOrBehavior`, same `qualifiers`
|
|
114
|
+
after the existing trim/lowercase/sort normalization).
|
|
115
|
+
|
|
116
|
+
**`claimType` and `surface` are explicitly excluded from the identity key —
|
|
117
|
+
this is a deliberate design decision, not an oversight:**
|
|
118
|
+
|
|
119
|
+
- `claimType` is excluded because the canonical claim key is defined over
|
|
120
|
+
*subject, predicate, value, qualifiers* — `fieldOrBehavior` is the
|
|
121
|
+
predicate; `claimType` is a taxonomy tag, not part of the matching grammar.
|
|
122
|
+
Two producers describing the same subject+field under different
|
|
123
|
+
`claimType` taxonomies are still the same logical claim for merge
|
|
124
|
+
purposes; reusing the canonical key means merge and Inquiry matching never
|
|
125
|
+
diverge on this point.
|
|
126
|
+
- `surface` is excluded because it is a producer-defined grouping or
|
|
127
|
+
namespace for related claims, not the primary thing users evaluate. Two
|
|
128
|
+
producers will pick unrelated `surface` values for logically identical
|
|
129
|
+
claims — there is no shared `surface` vocabulary across producers;
|
|
130
|
+
including it in the identity key would make cross-producer matches
|
|
131
|
+
essentially never fire. `surface` remains meaningful *within* one
|
|
132
|
+
producer's bundle (grouping, reporting `bySurface` counts) but plays no
|
|
133
|
+
role in cross-producer identity.
|
|
134
|
+
|
|
135
|
+
This means: **claims are never collapsed into one record by claim identity.**
|
|
136
|
+
Two producers' claims about the same canonical subject+field, even when they
|
|
137
|
+
fully agree, remain two distinct `Claim` records with two distinct ids in the
|
|
138
|
+
merged bundle (§5 unions by `id`, not by claim identity) — claim identity is
|
|
139
|
+
used only to decide *how to interpret* the pair (§7), never to deduplicate
|
|
140
|
+
them into one.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 5. The merge algorithm
|
|
145
|
+
|
|
146
|
+
Given `bundles: TrustBundle[]` (all sharing one `schemaVersion` —
|
|
147
|
+
implementations MUST reject a merge across differing `schemaVersion` values
|
|
148
|
+
rather than guessing a coercion):
|
|
149
|
+
|
|
150
|
+
1. **Union every collection by `id`**: `claims`, `evidence`, `policies`,
|
|
151
|
+
`events` (each item has a required `id`); `claimGroups`, `authorityTrace`
|
|
152
|
+
(each item has an optional `id`; items without an `id` are always kept,
|
|
153
|
+
never deduped). `identityLinks` are concatenated in full (they may omit
|
|
154
|
+
`id`; a union-find-based identity index dedupes them harmlessly even when
|
|
155
|
+
duplicated).
|
|
156
|
+
2. **First-occurrence wins content, subject to the determinism rule in §6** —
|
|
157
|
+
when two records share an `id`:
|
|
158
|
+
- If their content is structurally identical (deep-equal), keep it; this
|
|
159
|
+
is not a collision (the same fact was reported by two bundles, e.g.
|
|
160
|
+
after a re-export round-trip).
|
|
161
|
+
- If their content differs, this is a **collision** (§8): the
|
|
162
|
+
implementation MUST record it (`MergeCollision`: `collection`, `id`, and
|
|
163
|
+
enough information to identify the contributing bundles) rather than
|
|
164
|
+
silently picking one. The throwing entry point (`mergeBundles`) MUST
|
|
165
|
+
throw when any **claim** collision (differing content, same `Claim.id`)
|
|
166
|
+
is detected — silent claim corruption is the one thing merge MUST NOT
|
|
167
|
+
ever do. The non-throwing entry point (`mergeBundlesDetailed`) MUST
|
|
168
|
+
return the collisions for the caller to inspect/reconcile instead of
|
|
169
|
+
throwing.
|
|
170
|
+
3. **`source` becomes a synthesized combination** of the distinct `source`
|
|
171
|
+
values across the merged bundles (`merged:<a>+<b>`); **`producerId` MUST
|
|
172
|
+
be omitted** on a merged bundle (§2).
|
|
173
|
+
4. The merged bundle is not itself a new producer assertion — it MUST be
|
|
174
|
+
accepted as input to the same, unmodified status derivation
|
|
175
|
+
(`status-function.md`) and to the merge function again (merge MUST be
|
|
176
|
+
re-appliable to an already-merged bundle, since a bundle is a bundle
|
|
177
|
+
regardless of how many producers contributed to it — no special "already
|
|
178
|
+
merged" flag is introduced).
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 6. Determinism (order independence)
|
|
183
|
+
|
|
184
|
+
**MUST:** for any fixed *set* of input bundles, the merge function's output
|
|
185
|
+
(both the retained record content and the `collisions[]` set, modulo list
|
|
186
|
+
ordering) MUST be identical regardless of the order the bundles are supplied
|
|
187
|
+
in. `merge([A, B, C])`, `merge([C, A, B])`, and every other permutation of the
|
|
188
|
+
same set MUST produce the same merged bundle.
|
|
189
|
+
|
|
190
|
+
**Normative tie-break rule:** when N ≥ 2 records share an id and are not all
|
|
191
|
+
content-identical, an implementation MUST:
|
|
192
|
+
|
|
193
|
+
1. Compare **every** colliding record's content against every other's (not
|
|
194
|
+
just against the first-seen one), and report a collision for every
|
|
195
|
+
distinct-content pair.
|
|
196
|
+
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.)
|
|
205
|
+
|
|
206
|
+
This makes the merged bundle a pure, order-independent function of the *set*
|
|
207
|
+
of input bundles — the same guarantee `status-function.md` already gives for
|
|
208
|
+
`now`-parameterized status derivation, extended to the merge step that
|
|
209
|
+
precedes it.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 7. Agreement, conflict, and dispute mechanics
|
|
214
|
+
|
|
215
|
+
Given two claims that are the same logical claim under §4:
|
|
216
|
+
|
|
217
|
+
### 7a. Agreement
|
|
218
|
+
|
|
219
|
+
If `deepEqual(a.value, b.value)`: the claims agree. They MUST NOT be
|
|
220
|
+
collapsed into one record (§4). Agreement is informational at the merge
|
|
221
|
+
layer; agreement alone does not synthesize a stronger status. A consumer that
|
|
222
|
+
wants "N producers agree" as an input to a decision already has the tool for
|
|
223
|
+
it without a new mechanism: an authored `DerivationRule` (ADR 0003 §5,
|
|
224
|
+
`derivation-rule.schema.json`) can require `acceptedStatuses` across both
|
|
225
|
+
claim ids explicitly. This document does not add corroboration-across-producers
|
|
226
|
+
as an automatic status input.
|
|
227
|
+
|
|
228
|
+
### 7b. Value conflict
|
|
229
|
+
|
|
230
|
+
If the claims are governed by a `VerificationPolicy` with `incompatibleValues`
|
|
231
|
+
covering the pair (`verification-policy.schema.json`) and the values match an
|
|
232
|
+
`incompatibleValues` pair: **both claims MUST be retained** (never
|
|
233
|
+
last-write-wins) and the conflict is surfaced as a `contradiction`
|
|
234
|
+
transparency gap. This document does not add a normative JSON Schema for
|
|
235
|
+
`TransparencyGap` — that remains explicitly out of scope
|
|
236
|
+
(`schemas/trust-report.schema.json`'s own `$comment` already documents this;
|
|
237
|
+
see §9). The merge-layer guarantee this document DOES make is
|
|
238
|
+
schema-checkable without a `TransparencyGap` schema: neither claim is
|
|
239
|
+
dropped, mutated, or status-overridden by the presence of the other (§5 rule
|
|
240
|
+
2).
|
|
241
|
+
|
|
242
|
+
### 7c. Status conflict
|
|
243
|
+
|
|
244
|
+
A cross-producer `incompatibleStatuses` policy match (like a value conflict,
|
|
245
|
+
§7b) produces a `contradiction` transparency gap. It does not, by itself,
|
|
246
|
+
flip either claim's `status`.
|
|
247
|
+
|
|
248
|
+
A claim's `status` becomes `disputed` **only** through the existing,
|
|
249
|
+
single-claim mechanisms already in `status-function.md`: blocking
|
|
250
|
+
non-passing evidence (Step 4c), a terminal event with `status: "disputed"`
|
|
251
|
+
(Step 2), or an authority-gated resolution that is itself overridden by newer
|
|
252
|
+
blocking evidence (Step 1). Nothing in the cross-producer conflict path sets
|
|
253
|
+
a claim's status to `disputed`; `TrustReport.summary.disputedClaims` is
|
|
254
|
+
populated purely by scanning `claim.status === "disputed"` from each claim's
|
|
255
|
+
own single-claim fold output.
|
|
256
|
+
|
|
257
|
+
### 7d. Dispute resolution — no new record type
|
|
258
|
+
|
|
259
|
+
When a human/authority needs to resolve a `disputed` status (from 7c's
|
|
260
|
+
existing mechanisms), the spec already has the shape (ADR 0003 §8): a
|
|
261
|
+
`VerificationEvent` with `resolvesDispute: true` and an optional
|
|
262
|
+
`authorityRef`, gated by an active `AuthorityTrace` at decision time
|
|
263
|
+
(`status-function.md` Step 1). This document does not introduce a new
|
|
264
|
+
"Dispute" resource. Reusing `VerificationEvent` + `AuthorityTrace` means a
|
|
265
|
+
cross-producer dispute is resolved exactly the same way a single-producer one
|
|
266
|
+
is — the resolving event just needs `claimId` pointed at whichever specific
|
|
267
|
+
claim the authority is ruling on (the fold is per-claim; resolving "the
|
|
268
|
+
subject+field disagreement" in general means issuing a resolution event on
|
|
269
|
+
each affected claim id, or issuing one and letting a `DerivationRule` compose
|
|
270
|
+
the pair — no new bulk-resolution primitive is added by this document).
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 8. Id collision handling for records that are NOT the same logical claim
|
|
275
|
+
|
|
276
|
+
This is the case where two producers, without coordinating, mint the
|
|
277
|
+
identical `id` string for two *unrelated* records (accidental collision —
|
|
278
|
+
distinct from §4's "same logical claim, different ids" case, and distinct
|
|
279
|
+
from §7's value/status conflict between claims that *are* the same logical
|
|
280
|
+
claim).
|
|
281
|
+
|
|
282
|
+
- **Detection:** compare content; identical content is not a problem
|
|
283
|
+
(idempotent re-merge); differing content under the same id is a collision
|
|
284
|
+
that MUST be surfaced (`mergeBundles` throws for claims;
|
|
285
|
+
`mergeBundlesDetailed` reports for every collection).
|
|
286
|
+
- **Mitigation is the id convention (§3), not a new mechanism.** A collision
|
|
287
|
+
between two truly unrelated records is only possible if both producers
|
|
288
|
+
independently chose the same opaque string. The producer-prefixed dotted
|
|
289
|
+
convention (e.g. `producer-a.recommendation.upgrade-node` vs.
|
|
290
|
+
`producer-b.candidate.upgrade-node`) makes this vanishingly unlikely
|
|
291
|
+
without any schema enforcement. This document does not add a registry,
|
|
292
|
+
reservation scheme, or uniqueness authority — that would introduce the
|
|
293
|
+
kind of cross-producer coordination infrastructure the "stand-alone,
|
|
294
|
+
vendor-neutral format" goal explicitly rules out.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 9. Explicitly out of scope
|
|
299
|
+
|
|
300
|
+
- **`TransparencyGap` / `EvidenceRequirement` normative JSON Schemas.**
|
|
301
|
+
Referenced descriptively in §7b/§7c (the `contradiction` gap type already
|
|
302
|
+
exists informally, per `schemas/trust-report.schema.json`'s own
|
|
303
|
+
`$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.
|
|
309
|
+
- **Cryptographic producer identity (DIDs, keys, transparency-log-anchored
|
|
310
|
+
identity).** `producerId` (§2) is deliberately unsigned and unverified.
|
|
311
|
+
Where verifiable producer identity is needed, use Assurance L1/L2
|
|
312
|
+
(`assurance.md`) — this document adds no new identity/signing mechanism.
|
|
313
|
+
- **Survey chains, Veritas standards, Flow gates** — unchanged; still
|
|
314
|
+
extension-profile territory per README's existing "Out of scope" section.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Prior art
|
|
319
|
+
|
|
320
|
+
- **W3C Verifiable Credentials.** VC issuer identity is built on DIDs — a
|
|
321
|
+
resolvable, typically key-based identifier scheme. Requiring DIDs for
|
|
322
|
+
`producerId` would collapse the existing layered design (Assurance
|
|
323
|
+
L0/producer-asserted is the default; L1/L2 signing is opt-in) into "every
|
|
324
|
+
producer needs key infrastructure just to be namespaced for merge," a
|
|
325
|
+
strictly higher bar than merge needs. `producerId` is deliberately at the
|
|
326
|
+
same trust level as the existing `source` field (L0, free-text, unsigned).
|
|
327
|
+
- **in-toto.** `interop-in-toto.md` already wraps a whole `TrustBundle` as one
|
|
328
|
+
in-toto `Statement`'s `predicate` — in-toto's subject/predicate model is
|
|
329
|
+
single-attestation by design and defines no multi-producer merge algorithm
|
|
330
|
+
at the claim level. This document is compatible with that profile
|
|
331
|
+
unchanged: merge happens on `TrustBundle`s *before* DSSE wrapping, or a
|
|
332
|
+
verifier can independently wrap several signed Statements' predicates and
|
|
333
|
+
merge them after unwrapping — either order works because merge is a pure
|
|
334
|
+
function over `TrustBundle` values, not over signed envelopes.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Reference implementation notes (for implementers)
|
|
339
|
+
|
|
340
|
+
| Normative rule (this document) | Where it lands in `@kontourai/surface` `src/` | Status |
|
|
341
|
+
|---|---|---|
|
|
342
|
+
| §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) |
|
|
343
|
+
| §3 id convention | Prose-only; no code change (SHOULD, unenforced) | N/A |
|
|
344
|
+
| §4 claim identity across producers | `src/canonical.ts` `canonicalClaimKey` + `src/identity.ts` `buildIdentityIndex` | Reused unchanged |
|
|
345
|
+
| §5 rule 1–2 (union by id, first-occurrence-wins-if-identical, collision on differing content) | `src/merge.ts` `unionById` / `unionOptionalById` | Implemented |
|
|
346
|
+
| §5 rule 3 (`producerId` omitted on merge) | `src/merge.ts` `mergeBundlesDetailed` (the `source` synthesis block) | Implementer TODO: add omission of `producerId` |
|
|
347
|
+
| §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. |
|
|
348
|
+
| §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 |
|
|
349
|
+
| §7b value conflict → `contradiction` gap | `src/conflict-derivation.ts` `deriveConflictTransparencyGaps` | Implemented |
|
|
350
|
+
| §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 |
|
|
351
|
+
| §7d dispute resolution | `src/dispute.ts` `buildDisputeResolutionEvent`; `status-function.md` Step 1 | Implemented |
|
|
352
|
+
| §8 collision detection | `src/merge.ts` `MergeCollision` (a TS type; not currently a normative wire schema, and stays that way per §9) | Implemented |
|
|
353
|
+
| 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 |
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Versioning
|
|
358
|
+
|
|
359
|
+
This document introduces no change to `statusFunctionVersion` (stays `"2"`)
|
|
360
|
+
and no change to `schemaVersion`'s meaning (stays `4` — the new
|
|
361
|
+
`TrustBundle.producerId` field is optional and ignored by the unchanged
|
|
362
|
+
status-derivation fold). A bundle merged under this document and fed to the
|
|
363
|
+
unchanged fold produces identical per-claim results to a bundle that was
|
|
364
|
+
never merged.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hachure",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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,12 +16,16 @@
|
|
|
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
22
|
"assurance.md"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
|
-
"test": "node --test test
|
|
25
|
+
"test": "node --test test/*.test.mjs"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"ajv": "^8"
|
|
25
29
|
},
|
|
26
30
|
"keywords": [
|
|
27
31
|
"trust",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"claimType": { "type": "string" },
|
|
13
13
|
"fieldOrBehavior": { "type": "string" },
|
|
14
14
|
"value": {},
|
|
15
|
-
"status": { "
|
|
15
|
+
"status": { "$ref": "#/$defs/trustStatus" },
|
|
16
16
|
"createdAt": { "type": "string", "format": "date-time" },
|
|
17
17
|
"updatedAt": { "type": "string", "format": "date-time" },
|
|
18
18
|
"expiresAt": {
|
|
@@ -67,9 +67,23 @@
|
|
|
67
67
|
"type": "object",
|
|
68
68
|
"additionalProperties": { "type": "string" }
|
|
69
69
|
},
|
|
70
|
-
"metadata": { "type": "object" }
|
|
70
|
+
"metadata": { "type": "object" },
|
|
71
|
+
"producerStatus": { "$ref": "#/$defs/trustStatus" },
|
|
72
|
+
"freshness": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"required": ["asOf", "stale"],
|
|
75
|
+
"properties": {
|
|
76
|
+
"asOf": { "type": "string", "format": "date-time" },
|
|
77
|
+
"expiresAt": { "type": "string", "format": "date-time" },
|
|
78
|
+
"stale": { "type": "boolean" }
|
|
79
|
+
},
|
|
80
|
+
"additionalProperties": false
|
|
81
|
+
}
|
|
71
82
|
},
|
|
72
83
|
"$defs": {
|
|
84
|
+
"trustStatus": {
|
|
85
|
+
"enum": ["unknown", "proposed", "assumed", "verified", "stale", "disputed", "superseded", "rejected", "revoked"]
|
|
86
|
+
},
|
|
73
87
|
"integrityAnchor": {
|
|
74
88
|
"type": "object",
|
|
75
89
|
"required": ["id", "kind", "algorithm", "value", "sourceRef"],
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"target": { "$ref": "#/$defs/canonicalClaimTarget" },
|
|
41
41
|
"acceptedStatuses": {
|
|
42
42
|
"type": "array",
|
|
43
|
-
"items": { "enum": ["unknown", "proposed", "assumed", "verified", "stale", "disputed", "superseded", "rejected"] }
|
|
43
|
+
"items": { "enum": ["unknown", "proposed", "assumed", "verified", "stale", "disputed", "superseded", "rejected", "revoked"] }
|
|
44
44
|
},
|
|
45
45
|
"predicate": {
|
|
46
46
|
"type": "object",
|
|
@@ -8,6 +8,11 @@
|
|
|
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" }
|
|
@@ -18,36 +23,15 @@
|
|
|
18
23
|
},
|
|
19
24
|
"policies": {
|
|
20
25
|
"type": "array",
|
|
21
|
-
"items": { "
|
|
26
|
+
"items": { "$ref": "verification-policy.schema.json" }
|
|
22
27
|
},
|
|
23
28
|
"events": {
|
|
24
29
|
"type": "array",
|
|
25
|
-
"items": { "
|
|
30
|
+
"items": { "$ref": "verification-event.schema.json" }
|
|
26
31
|
},
|
|
27
32
|
"identityLinks": {
|
|
28
33
|
"type": "array",
|
|
29
|
-
"items": {
|
|
30
|
-
"type": "object",
|
|
31
|
-
"required": ["subjects"],
|
|
32
|
-
"properties": {
|
|
33
|
-
"subjects": {
|
|
34
|
-
"type": "array",
|
|
35
|
-
"minItems": 2,
|
|
36
|
-
"items": {
|
|
37
|
-
"type": "object",
|
|
38
|
-
"required": ["subjectType", "subjectId"],
|
|
39
|
-
"properties": {
|
|
40
|
-
"subjectType": { "type": "string" },
|
|
41
|
-
"subjectId": { "type": "string" }
|
|
42
|
-
},
|
|
43
|
-
"additionalProperties": false
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"reason": { "type": "string" },
|
|
47
|
-
"attestedBy": { "type": "string" }
|
|
48
|
-
},
|
|
49
|
-
"additionalProperties": false
|
|
50
|
-
}
|
|
34
|
+
"items": { "$ref": "#/$defs/identityLink" }
|
|
51
35
|
},
|
|
52
36
|
"claimGroups": {
|
|
53
37
|
"type": "array",
|
|
@@ -68,6 +52,32 @@
|
|
|
68
52
|
},
|
|
69
53
|
"additionalProperties": false
|
|
70
54
|
},
|
|
55
|
+
"identityLink": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"required": ["subjects"],
|
|
58
|
+
"properties": {
|
|
59
|
+
"id": { "type": "string" },
|
|
60
|
+
"subjects": {
|
|
61
|
+
"type": "array",
|
|
62
|
+
"minItems": 2,
|
|
63
|
+
"items": { "$ref": "#/$defs/subjectRef" }
|
|
64
|
+
},
|
|
65
|
+
"reason": { "type": "string" },
|
|
66
|
+
"attestedBy": { "type": "string" },
|
|
67
|
+
"relation": { "enum": ["equivalent", "subsumes", "converts"] },
|
|
68
|
+
"conversion": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {
|
|
71
|
+
"factor": { "type": "number" },
|
|
72
|
+
"offset": { "type": "number" },
|
|
73
|
+
"note": { "type": "string" }
|
|
74
|
+
},
|
|
75
|
+
"additionalProperties": false
|
|
76
|
+
},
|
|
77
|
+
"mappingClaimId": { "type": "string" }
|
|
78
|
+
},
|
|
79
|
+
"additionalProperties": false
|
|
80
|
+
},
|
|
71
81
|
"authorityTrace": {
|
|
72
82
|
"type": "object",
|
|
73
83
|
"required": ["id", "subject", "actorRef", "authorityType", "authorityRef", "sourceRef", "observedAt"],
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
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",
|
|
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
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"schemaVersion",
|
|
10
|
+
"id",
|
|
11
|
+
"generatedAt",
|
|
12
|
+
"source",
|
|
13
|
+
"claims",
|
|
14
|
+
"evidence",
|
|
15
|
+
"policies",
|
|
16
|
+
"events",
|
|
17
|
+
"evidenceRequirementsByClaimId",
|
|
18
|
+
"transparencyGaps",
|
|
19
|
+
"changeRecords",
|
|
20
|
+
"subjectGroups",
|
|
21
|
+
"claimGroupRollups",
|
|
22
|
+
"summary",
|
|
23
|
+
"statusFunctionVersion"
|
|
24
|
+
],
|
|
25
|
+
"properties": {
|
|
26
|
+
"schemaVersion": { "enum": [2, 3, 4] },
|
|
27
|
+
"id": { "type": "string" },
|
|
28
|
+
"generatedAt": { "type": "string", "format": "date-time" },
|
|
29
|
+
"source": { "type": "string" },
|
|
30
|
+
"claims": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": {
|
|
33
|
+
"allOf": [
|
|
34
|
+
{ "$ref": "claim.schema.json" },
|
|
35
|
+
{ "type": "object", "required": ["status"] }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"evidence": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": { "$ref": "evidence.schema.json" }
|
|
42
|
+
},
|
|
43
|
+
"policies": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"items": { "$ref": "verification-policy.schema.json" }
|
|
46
|
+
},
|
|
47
|
+
"events": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": { "$ref": "verification-event.schema.json" }
|
|
50
|
+
},
|
|
51
|
+
"identityLinks": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": { "$ref": "trust-bundle.schema.json#/$defs/identityLink" }
|
|
54
|
+
},
|
|
55
|
+
"claimGroups": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": { "$ref": "trust-bundle.schema.json#/$defs/claimGroup" }
|
|
58
|
+
},
|
|
59
|
+
"authorityTrace": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"items": { "$ref": "trust-bundle.schema.json#/$defs/authorityTrace" }
|
|
62
|
+
},
|
|
63
|
+
"evidenceRequirementsByClaimId": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"additionalProperties": { "type": "object" }
|
|
66
|
+
},
|
|
67
|
+
"transparencyGaps": {
|
|
68
|
+
"type": "array",
|
|
69
|
+
"items": { "type": "object" }
|
|
70
|
+
},
|
|
71
|
+
"changeRecords": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"items": { "type": "object" }
|
|
74
|
+
},
|
|
75
|
+
"subjectGroups": {
|
|
76
|
+
"type": "array",
|
|
77
|
+
"items": { "type": "object" }
|
|
78
|
+
},
|
|
79
|
+
"claimGroupRollups": {
|
|
80
|
+
"type": "array",
|
|
81
|
+
"items": { "type": "object" }
|
|
82
|
+
},
|
|
83
|
+
"summary": { "type": "object" },
|
|
84
|
+
"statusFunctionVersion": { "type": "string" }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"parentType": { "type": "string" },
|
|
11
11
|
"requiredEvidence": {
|
|
12
12
|
"type": "array",
|
|
13
|
-
"items": { "enum": ["source_excerpt", "test_output", "human_attestation", "calculation_trace", "document_citation", "crawl_observation", "policy_rule"] }
|
|
13
|
+
"items": { "enum": ["source_excerpt", "test_output", "human_attestation", "attestation", "calculation_trace", "document_citation", "crawl_observation", "policy_rule"] }
|
|
14
14
|
},
|
|
15
15
|
"requiredMethods": {
|
|
16
16
|
"type": "array",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"type": "array",
|
|
54
54
|
"minItems": 2,
|
|
55
55
|
"maxItems": 2,
|
|
56
|
-
"items": { "enum": ["unknown", "proposed", "assumed", "verified", "stale", "disputed", "superseded", "rejected"] }
|
|
56
|
+
"items": { "enum": ["unknown", "proposed", "assumed", "verified", "stale", "disputed", "superseded", "rejected", "revoked"] }
|
|
57
57
|
},
|
|
58
58
|
"message": { "type": "string" }
|
|
59
59
|
},
|