forked from ChelseaKR/habitable
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpacket-bundle.schema.json
More file actions
444 lines (444 loc) · 24.6 KB
/
Copy pathpacket-bundle.schema.json
File metadata and controls
444 lines (444 loc) · 24.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://chelseakr.github.io/habitable/schema/packet-bundle-v4.schema.json",
"title": "habitable evidence packet — bundle.json (v1–v4)",
"description": "The machine-readable contract for bundle.json through packet_version 4. Version 4 adds custody-bound artifacts, typed evidence relationships, versioned workflow profiles, and signed presentation-only handoff manifests. The signed sibling file bundle.sig.json is described by $defs/signature.",
"type": "object",
"required": [
"packet_version",
"case_id",
"generated_at",
"producer_fingerprint",
"hash_algorithm",
"items",
"custody_proof",
"appendix"
],
"additionalProperties": true,
"properties": {
"packet_version": {
"type": "integer",
"minimum": 1,
"maximum": 4,
"description": "Packet format version. A verifier accepts 1..SUPPORTED_PACKET_VERSION and rejects newer-than-supported packets rather than mis-verifying."
},
"case_id": { "type": "string" },
"unit": { "type": "string", "description": "Unit label; may be empty." },
"scope": {
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["issue", "unit"], "description": "Current packet-v3 construction emits 'unit'; 'issue' remains accepted for historical packet compatibility." },
"issue_id": { "type": "string", "description": "Historical scoped-packet field; empty on current whole-unit exports." },
"since": { "type": "string", "description": "Historical lower bound on capture time; empty on current whole-unit exports." },
"statement": {
"type": "string",
"description": "Human-readable scope summary. Current packet-v3 construction emits 'Scope: the whole unit …'; issue-only statements are historical compatibility data, not an available export mode. Also rendered, localized, in packet.html/packet.pdf; see docs/legal/minimal-disclosure.md."
},
"exclusions": {
"type": "array",
"items": { "type": "string" },
"description": "Historical scoped packets may state exclusions here. Current packet-v3 construction is whole-unit only and emits no issue/date exclusions; optional originals and metadata handling are described in disclosures. See item R-35."
}
},
"additionalProperties": true
},
"generated_at": {
"type": "string",
"description": "Packet generation time, ISO 8601 UTC, e.g. 2026-01-02T00:00:00Z."
},
"producer_fingerprint": {
"type": "string",
"description": "Producing device's identity fingerprint (xxxx-xxxx-xxxx-xxxx)."
},
"hash_algorithm": { "type": "string", "const": "sha256" },
"language": { "type": "string", "description": "BCP 47-ish language tag of the rendered packet, e.g. en, es." },
"template": {
"type": "object",
"properties": {
"header": { "type": "string" },
"footer": { "type": "string" }
},
"additionalProperties": true
},
"issues": { "type": "array", "items": { "$ref": "#/$defs/issue" } },
"timeline": { "type": "array", "items": { "$ref": "#/$defs/timelineEntry" } },
"items": { "type": "array", "items": { "$ref": "#/$defs/item" } },
"relationships": { "type": "array", "items": { "$ref": "#/$defs/relationship" } },
"use_case_profile": {
"oneOf": [{ "$ref": "#/$defs/useCaseProfile" }, { "type": "null" }]
},
"use_case_profile_fallback": {
"oneOf": [{ "$ref": "#/$defs/useCaseProfileFallback" }, { "type": "null" }]
},
"handoff_views": {
"type": "array",
"items": { "$ref": "#/$defs/handoffManifest" }
},
"custody_proof": { "$ref": "#/$defs/custodyProof" },
"disclosures": {
"type": "array",
"items": { "type": "string" },
"description": "Human-readable notes of what the packet reveals (e.g. shared-copy metadata handling; custody identities not exported; sealed originals embedded with full metadata). Also rendered, localized, in packet.html/packet.pdf."
},
"appendix": {
"type": "object",
"required": ["item_count", "timestamped_count", "includes_originals"],
"properties": {
"item_count": { "type": "integer", "minimum": 0 },
"timestamped_count": { "type": "integer", "minimum": 0 },
"includes_originals": { "type": "boolean" },
"timeline_count": { "type": "integer", "minimum": 0 },
"custody_bound_timeline_count": { "type": "integer", "minimum": 0 },
"artifact_count": { "type": "integer", "minimum": 0 },
"relationship_count": { "type": "integer", "minimum": 0 }
},
"additionalProperties": true
}
},
"allOf": [
{
"if": { "properties": { "packet_version": { "minimum": 3 } } },
"then": {
"properties": {
"timeline": { "type": "array", "items": { "$ref": "#/$defs/timelineEntryV3" } }
}
},
"else": {
"properties": {
"timeline": { "type": "array", "items": { "$ref": "#/$defs/timelineEntryLegacy" } }
}
}
}
],
"$defs": {
"hexSha256": {
"type": "string",
"pattern": "^[0-9a-f]{64}$",
"description": "Lowercase hex SHA-256 digest (64 chars). The custody genesis prev_hash is 64 zeros."
},
"issue": {
"type": "object",
"required": ["issue_id", "category", "room", "title", "status", "severity", "description"],
"properties": {
"issue_id": { "type": "string" },
"category": { "type": "string", "description": "e.g. heat, mold, pests, water, electrical, structural." },
"room": { "type": "string" },
"title": { "type": "string" },
"status": { "type": "string" },
"severity": { "type": "string" },
"description": { "type": "string" }
},
"additionalProperties": true
},
"timelineEntry": {
"oneOf": [
{ "$ref": "#/$defs/timelineEntryLegacy" },
{ "$ref": "#/$defs/timelineEntryV3" }
]
},
"timelineEntryLegacy": {
"type": "object",
"required": ["entry_id", "issue_id", "kind", "text", "hlc"],
"properties": {
"entry_id": { "type": "string" },
"issue_id": { "type": "string" },
"kind": { "type": "string" },
"text": { "type": "string" },
"hlc": { "type": "string", "description": "Historical field: raw wall_ms.counter.node_id HLC in packet v1; opaque ordering token in packet v2. It is never treated as occurred_at or recorded_at in packet v3." }
},
"additionalProperties": true
},
"timelineEntryV3": {
"type": "object",
"not": {
"anyOf": [
{ "required": ["kind"] },
{ "required": ["hlc"] }
]
},
"required": ["timeline_schema", "entry_id", "issue_id", "event_type", "other_label", "text", "occurred_at", "source", "source_detail", "recorded_at", "links", "order_token", "integrity"],
"properties": {
"timeline_schema": { "type": "integer", "const": 2 },
"entry_id": { "type": "string" },
"issue_id": { "type": "string" },
"event_type": {
"type": "string",
"enum": ["condition_observed", "notice_sent", "delivery_confirmed", "response_received", "inspection", "repair", "recurrence", "impact", "other"]
},
"other_label": { "type": "string", "description": "Non-empty only when event_type is other." },
"text": { "type": "string", "minLength": 1 },
"occurred_at": { "type": "string", "description": "Date/time the recorder says the event happened. ISO date or offset-normalized UTC timestamp. Empty only for an explicit legacy migration; it never defaults to recorded_at." },
"source": { "type": "string", "enum": ["firsthand", "message", "document", "official_record", "other", "unspecified"] },
"source_detail": { "type": "string", "description": "Non-empty only when source is other." },
"recorded_at": { "type": "string", "description": "UTC device time at which the append-only case entry was created; not a claim about occurrence time and not an RFC 3161 timestamp." },
"links": {
"type": "object",
"required": ["capture_ids", "notice_entry_id", "receipt_entry_id", "response_entry_id"],
"properties": {
"capture_ids": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
"notice_entry_id": { "type": "string", "description": "Related notice_sent timeline entry; empty when absent." },
"receipt_entry_id": { "type": "string", "description": "Related delivery_confirmed timeline entry; empty when absent." },
"response_entry_id": { "type": "string", "description": "Related response_received timeline entry; empty when absent." }
},
"additionalProperties": false
},
"order_token": { "type": "string", "description": "Opaque, per-case-salted CRDT ordering token. It is not a date." },
"integrity": {
"type": "object",
"required": ["algorithm", "commitment", "custody_action", "binding_stage"],
"properties": {
"algorithm": { "type": "string", "const": "sha256" },
"commitment": { "$ref": "#/$defs/hexSha256" },
"custody_action": { "type": "string", "const": "note_added" },
"binding_stage": { "type": "string", "enum": ["recorded", "backfill", "migration"] }
},
"additionalProperties": false
},
"migration": {
"type": "object",
"description": "Present only when a case-schema-v1 free-form timeline entry is exported into packet v3. Unknown occurrence/source facts remain explicitly unknown.",
"additionalProperties": true
}
},
"additionalProperties": true
},
"item": {
"type": "object",
"required": ["capture_id", "content_hash", "media_type", "captured_at", "shared_name", "shared_hash", "has_original", "archive_timestamps"],
"properties": {
"record_kind": { "type": "string", "enum": ["capture", "artifact"] },
"capture_id": { "type": "string" },
"issue_id": { "type": "string" },
"content_hash": { "$ref": "#/$defs/hexSha256", "description": "SHA-256 of the sealed original; the value the RFC 3161 token is taken over." },
"media_type": { "type": "string", "description": "MIME type, e.g. image/jpeg." },
"captured_at": { "type": "string" },
"shared_name": { "type": "string", "description": "Filename under media/ of the shared copy processed under the configured metadata policy; empty if none." },
"shared_hash": {
"oneOf": [{ "$ref": "#/$defs/hexSha256" }, { "type": "string", "maxLength": 0 }],
"description": "SHA-256 of the shared copy; empty when no shared media for this item."
},
"stripped": { "type": "string", "description": "Human-readable note of which metadata was removed from the shared copy (e.g. 'gps', 'none', 'skipped')." },
"has_original": { "type": "boolean", "description": "Whether the sealed original is embedded under originals/." },
"timestamp": {
"oneOf": [{ "$ref": "#/$defs/timestampToken" }, { "type": "null" }],
"description": "RFC 3161 (or dev) token over content_hash; null while awaiting timestamp."
},
"archive_timestamps": {
"type": "array",
"items": { "$ref": "#/$defs/timestampToken" },
"description": "Ordered archive (re-)timestamps chaining back to the primary token."
},
"additional_timestamps": {
"type": "array",
"items": { "$ref": "#/$defs/timestampToken" },
"description": "Independent redundant timestamps from other authorities over the SAME content_hash (not a chain). Provides multiple-authority redundancy: the verifier treats the item as timestamped if at least one authority (primary or additional) verifies. Absent in single-authority packets."
},
"sensor": {
"oneOf": [{ "$ref": "#/$defs/sensorSeries" }, { "type": "null" }],
"description": "Present (non-null) only for instrument data-file captures (EXP-09, e.g. a temperature-logger or moisture-meter CSV). The readings interpreted from the sealed original for accessible chart+table rendering; the CSV bytes themselves remain the hash-anchored evidence under content_hash. null (or absent) for photo/video captures."
},
"artifact": { "$ref": "#/$defs/artifact" },
"integrity": { "$ref": "#/$defs/workflowIntegrity" }
},
"additionalProperties": true
},
"artifact": {
"type": "object",
"required": ["artifact_schema", "artifact_id", "issue_id", "artifact_type", "title", "source", "issuer", "occurred_at", "recorded_at", "content_hash", "media_type", "accessible_description"],
"properties": {
"artifact_schema": { "type": "integer", "const": 1 },
"artifact_id": { "type": "string" },
"issue_id": { "type": "string" },
"artifact_type": { "type": "string", "enum": ["repair_request", "delivery_receipt", "landlord_response", "inspection_report", "utility_notice", "accommodation_request", "supporting_letter", "clinician_letter", "expense_receipt", "relocation_record", "partner_export", "other_document"] },
"title": { "type": "string", "minLength": 1 },
"source": { "type": "string", "minLength": 1 },
"issuer": { "type": "string" },
"occurred_at": { "type": "string" },
"recorded_at": { "type": "string" },
"content_hash": { "$ref": "#/$defs/hexSha256" },
"media_type": { "type": "string", "minLength": 1 },
"accessible_description": { "type": "string" }
},
"additionalProperties": false
},
"relationship": {
"type": "object",
"required": ["relationship_schema", "relationship_id", "issue_id", "relationship_type", "source_id", "target_id", "assertion", "recorded_at", "order_token", "integrity"],
"properties": {
"relationship_schema": { "type": "integer", "const": 1 },
"relationship_id": { "type": "string" },
"issue_id": { "type": "string" },
"relationship_type": { "type": "string", "enum": ["documents_condition", "sent_via", "delivery_receipt_for", "response_to", "before_of", "after_of", "inspection_finding_for", "repair_claim_for", "expense_caused_by", "supports"] },
"source_id": { "type": "string" },
"target_id": { "type": "string" },
"assertion": { "type": "string" },
"recorded_at": { "type": "string" },
"order_token": { "type": "string" },
"integrity": { "$ref": "#/$defs/workflowIntegrity" }
},
"additionalProperties": false
},
"workflowIntegrity": {
"type": "object",
"required": ["algorithm", "commitment", "custody_action", "binding_stage"],
"properties": {
"algorithm": { "type": "string", "const": "sha256" },
"commitment": { "$ref": "#/$defs/hexSha256" },
"custody_action": { "type": "string", "enum": ["artifact_added", "relationship_added"] },
"binding_stage": { "type": "string", "enum": ["recorded", "semantic_binding", "import_binding"] }
},
"additionalProperties": false
},
"useCaseProfile": {
"type": "object",
"required": ["profile_schema", "profile_id", "version", "name", "summary", "artifact_types", "relationship_types", "handoff_sections", "disclosures", "review_state", "external_review_required"],
"properties": {
"profile_schema": { "type": "integer", "const": 1 },
"profile_id": { "type": "string" },
"version": { "type": "integer", "minimum": 1 },
"review_state": { "type": "string", "enum": ["maintainer_reviewed", "external_review_required"] },
"external_review_required": { "type": "boolean" }
},
"additionalProperties": true
},
"useCaseProfileFallback": {
"type": "object",
"description": "Present only when a selected/named profile's review had expired by export time (ADR 0012); export then carries no use_case_profile.",
"required": ["requested_profile_id", "requested_profile_version", "reason", "expires_at"],
"properties": {
"requested_profile_id": { "type": "string" },
"requested_profile_version": { "type": "integer", "minimum": 1 },
"reason": { "type": "string", "const": "expired" },
"expires_at": { "type": "string", "description": "YYYY-MM-DD; the review-expiry date that triggered the fallback." }
},
"additionalProperties": true
},
"handoffManifest": {
"type": "object",
"required": ["handoff_manifest_version", "profile_id", "profile", "scope", "sections", "counts", "disclosures", "source_of_truth", "presentation_only"],
"properties": {
"handoff_manifest_version": { "type": "integer", "enum": [1, 2], "description": "1 gave every section the whole bundle's id lists, so a section with no member records still rendered a non-zero count (issue #181). 2 drops the per-section id lists; sections carry section_id only and 'counts' is the one bundle-wide figure." },
"section_membership": { "type": "string", "const": "not_recorded", "description": "Present from manifest version 2. Nothing in the case model records which record belongs to which handoff section, so no section claims membership. Section-scoped counts return when routing does." },
"profile_id": { "type": "string" },
"profile": { "$ref": "#/$defs/useCaseProfile" },
"sections": { "type": "array", "items": { "type": "object", "required": ["section_id"], "properties": { "section_id": { "type": "string" } } } },
"disclosures": { "type": "array", "items": { "type": "string" } },
"source_of_truth": { "type": "string", "const": "bundle.json" },
"presentation_only": { "type": "boolean", "const": true }
},
"additionalProperties": true
},
"sensorSeries": {
"type": "object",
"description": "An instrument CSV interpreted into a renderable series (EXP-09). Corroboration only, never proof of cause; the chart is a visual convenience over the readings table, which is the accessible source of truth.",
"required": ["label_header", "value_header", "readings", "total_rows", "truncated", "minimum", "maximum", "mean"],
"properties": {
"label_header": { "type": "string", "description": "Column name for each reading's label (often a timestamp); 'Reading' when the CSV had no header." },
"value_header": { "type": "string", "description": "Column name for the numeric value; 'Value' when the CSV had no header. Any trailing '(unit)' is split out into 'unit'." },
"unit": { "oneOf": [{ "type": "string" }, { "type": "null" }], "description": "Unit parsed from the value header (e.g. 'F', '%RH'); null if none." },
"readings": {
"type": "array",
"description": "Rendered readings, in file order. Capped at 500 rows; see 'truncated'/'total_rows'. Full data remains in the sealed original.",
"items": {
"type": "object",
"required": ["label", "value"],
"properties": {
"label": { "type": "string" },
"value": { "type": "number" }
}
}
},
"total_rows": { "type": "integer", "description": "Total numeric readings parsed (before any truncation)." },
"truncated": { "type": "boolean", "description": "Whether 'readings' is a truncated prefix of the full series." },
"minimum": { "type": "number" },
"maximum": { "type": "number" },
"mean": { "type": "number" },
"warnings": {
"type": "array",
"items": { "type": "string" },
"description": "Non-fatal parse notes (rows skipped, truncation), surfaced verbatim in the packet."
}
},
"additionalProperties": false
},
"timestampToken": {
"type": "object",
"required": ["kind", "tsa_name", "token_b64"],
"properties": {
"kind": { "type": "string", "enum": ["rfc3161", "dev"], "description": "'dev' is a non-production offline authority; production packets use 'rfc3161'." },
"tsa_name": { "type": "string" },
"token_b64": { "type": "string", "contentEncoding": "base64", "description": "DER (rfc3161) or canonical-JSON (dev) token, base64-encoded." }
},
"additionalProperties": true
},
"custodyProof": {
"type": "object",
"required": ["algorithm", "length", "head_hash", "entries"],
"description": "Identity-stripped, independently checkable proof the chain of custody is intact. Actor identities, salts, signatures, and private_details are NOT present (they stay in the union's vault).",
"properties": {
"algorithm": { "type": "string", "const": "sha256" },
"length": { "type": "integer", "minimum": 0 },
"head_hash": {
"oneOf": [{ "$ref": "#/$defs/hexSha256" }, { "type": "string", "pattern": "^0{64}$" }],
"description": "entry_hash of the last entry, or 64 zeros for an empty chain."
},
"items": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"entries": { "type": "integer", "minimum": 0 },
"last_action": { "type": "string" },
"head_hash": { "$ref": "#/$defs/hexSha256" }
},
"additionalProperties": true
}
},
"entries": { "type": "array", "items": { "$ref": "#/$defs/custodyEntry" } }
},
"additionalProperties": true
},
"custodyEntry": {
"type": "object",
"description": "Exported (redacted) custody entry. entry_hash = SHA-256(canonical_json of {seq, action, item_id, hlc, actor_commitment, details(sorted), prev_hash}).",
"required": ["seq", "action", "item_id", "hlc", "actor_commitment", "details", "prev_hash", "entry_hash"],
"properties": {
"seq": { "type": "integer", "minimum": 1, "description": "1-based, strictly increasing position in the chain." },
"action": {
"type": "string",
"enum": ["captured", "imported", "fixity_checked", "timestamped", "viewed", "copied_for_sharing", "included_in_packet", "note_added"]
},
"item_id": { "type": "string" },
"hlc": { "type": "string", "description": "Ordering token. Opaque, per-case-salted digest in packet_version >= 2 (no wall-clock/node metadata); the raw HLC stamp in v1. The exported chain is re-hashed over this value so it still verifies standalone." },
"actor_commitment": { "$ref": "#/$defs/hexSha256", "description": "Salted commitment SHA-256(salt:actor); the clear actor and salt are vault-only." },
"details": {
"type": "object",
"additionalProperties": { "type": "string" },
"description": "Verification-relevant, non-identifying facts only (hashes, media_type, tsa, etc.). A copied_for_sharing entry carries content_hash + shared_hash + stripped."
},
"prev_hash": {
"oneOf": [{ "$ref": "#/$defs/hexSha256" }, { "type": "string", "pattern": "^0{64}$" }],
"description": "entry_hash of the previous entry; 64 zeros for the genesis link."
},
"entry_hash": { "$ref": "#/$defs/hexSha256" }
},
"additionalProperties": false
},
"signature": {
"type": "object",
"description": "Contents of the sibling file bundle.sig.json. The producer's Ed25519 key signs the ASCII hex of bundle_sha256. That key is carried here too, so the signature alone proves internal consistency, not producer identity — packet_seal is the part an attacker cannot manufacture.",
"required": ["sign_public", "bundle_sha256", "signature"],
"properties": {
"producer_fingerprint": { "type": "string" },
"sign_public": { "type": "string", "contentEncoding": "base64", "description": "Raw Ed25519 public key, base64." },
"bundle_sha256": { "$ref": "#/$defs/hexSha256", "description": "SHA-256 of the exact bundle.json bytes." },
"signature": { "type": "string", "contentEncoding": "base64", "description": "Ed25519 signature over the ASCII of bundle_sha256." },
"packet_seal": {
"$ref": "#/$defs/timestampToken",
"description": "OPTIONAL. An RFC 3161 token whose imprint is bundle_sha256, so it covers every field of bundle.json at once — including every shared_hash and the custody head_hash. Absent for a packet exported offline or with sealing switched off; that is a weaker packet, not an invalid one. Not versioned with the bundle: it lives in this sidecar, which no packet_version has ever covered. See docs/crypto-spec.md §6.5 and docs/adr/0011-authority-seal-over-the-whole-packet.md."
}
},
"additionalProperties": true
}
}
}