fix: align evidence success state (#150)

This commit is contained in:
QiuSW
2026-08-31 08:57:25 +08:00
parent 2a395aa126
commit 359c553452
7 changed files with 18 additions and 11 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ Field ownership is deliberately narrow:
- `400 invalid_event`: schema, canonical form, or sensitive/unknown member violation. Terminal until the producer fixes the payload.
- `409 idempotency_conflict`: same permanent key with a different payload. Terminal and audited; never overwrite the first Event.
- `422 unsupported_schema_version`: unknown major/revision. Terminal for that payload.
- Evidence `pending`, `processing` and `failed` are valid Event states. Bell keeps the Event and resolves/degrades evidence independently.
- Evidence `pending`, `processing`, `success` and `failed` are valid Event states. Bell keeps the Event and resolves/degrades evidence independently.
v1 is closed (`additionalProperties=false`). Producers may enable a compatible revision only after all relays and Bell validate it. Any removed/renamed required field, changed meaning, enum narrowing, identity/canonicalization change, or new required member publishes a new major path such as `/v2`. During the compatibility window Bell keeps the previous version endpoint. Rollback disables the new producer version and resumes the last accepted version; it does not delete Event, Receipt, Outbox or audit facts.
+2 -2
View File
@@ -6,7 +6,7 @@ This contract shares metadata about a logical evidence object. It never grants o
- `pending`: capture was accepted but no processing started.
- `processing`: capture or encoding is in progress.
- `available`: `content_type` and SHA-256 `integrity` are required. Access authorization is negotiated outside this payload by the machine-identity/connector work.
- `success`: capture completed; `content_type` and SHA-256 `integrity` are required. Access authorization is negotiated outside this payload by the machine-identity/connector work.
- `failed`: `failure.code` and `retryable` are required. Bell keeps the immutable Event and renders evidence unavailable; it must not reject or close the Alert because evidence failed.
- HTTP `404` means an unknown logical reference. `410` means expired evidence. Both degrade evidence only, not the Event.
@@ -15,7 +15,7 @@ The payload forbids arbitrary properties, so filesystem paths, camera credential
## Ownership
- Brain may request evidence but maps only logical metadata it actually knows.
- Sense is the default evidence owner and advances the status monotonically for a given capture attempt: `pending -> processing -> available|failed`. It must retain the same `evidence_id` while status changes.
- Sense is the default evidence owner and advances the status monotonically for a given capture attempt: `pending -> processing -> success|failed`. It must retain the same `evidence_id` while status changes.
- A relay transports the reference unchanged and must not resolve it into a path or URL.
- Bell stores the latest evidence metadata separately from its immutable Event. Evidence failure/expiry never changes Alert ack/close state.
@@ -18,7 +18,7 @@
"evidence_id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"},
"owner_id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"},
"type": {"enum": ["snapshot", "clip"]},
"status": {"enum": ["pending", "processing", "available", "failed"]},
"status": {"enum": ["pending", "processing", "success", "failed"]},
"captured_at": {"type": "string", "format": "date-time"},
"status_updated_at": {"type": "string", "format": "date-time"},
"expires_at": {"type": "string", "format": "date-time"},
@@ -45,7 +45,7 @@
},
"allOf": [
{
"if": {"properties": {"status": {"const": "available"}}, "required": ["status"]},
"if": {"properties": {"status": {"const": "success"}}, "required": ["status"]},
"then": {"required": ["content_type", "integrity"], "not": {"required": ["failure"]}}
},
{
+1 -1
View File
@@ -3,7 +3,7 @@
"evidence_id": "ev-school-east-0001",
"owner_id": "sense-school-a",
"type": "snapshot",
"status": "available",
"status": "success",
"captured_at": "2026-08-31T00:00:01.125Z",
"status_updated_at": "2026-08-31T00:00:02.450Z",
"expires_at": "2026-09-07T00:00:01.125Z",
+1 -1
View File
@@ -9,7 +9,7 @@
{"name": "evidence_id", "in": "path", "required": true, "schema": {"type": "string"}}
],
"responses": {
"200": {"description": "Current metadata, including pending or failed states", "content": {"application/json": {"schema": {"$ref": "./evidence-reference.schema.json"}}}},
"200": {"description": "Current metadata, including pending, processing, success or failed states", "content": {"application/json": {"schema": {"$ref": "./evidence-reference.schema.json"}}}},
"404": {"description": "Unknown logical reference", "content": {"application/problem+json": {"schema": {"$ref": "../../events/v1/problem.schema.json"}}}},
"410": {"description": "Evidence expired; event remains valid", "content": {"application/problem+json": {"schema": {"$ref": "../../events/v1/problem.schema.json"}}}}
}
@@ -3,7 +3,7 @@
"evidence_id": "ev-sensitive-0001",
"owner_id": "sense-school-a",
"type": "snapshot",
"status": "available",
"status": "success",
"captured_at": "2026-08-31T00:00:01.125Z",
"status_updated_at": "2026-08-31T00:00:02.450Z",
"content_type": "image/jpeg",
+10 -3
View File
@@ -27,9 +27,16 @@ class EvidenceV1ContractTests(unittest.TestCase):
self.assertEqual([], self.errors_for(payload))
def test_state_specific_metadata_is_enforced(self) -> None:
available = load_json(CONTRACTS / "evidence" / "v1" / "examples" / "success.json")
del available["integrity"]
self.assertTrue(self.errors_for(available))
success = load_json(CONTRACTS / "evidence" / "v1" / "examples" / "success.json")
for required in ("content_type", "integrity"):
with self.subTest(success_requires=required):
candidate = copy.deepcopy(success)
del candidate[required]
self.assertTrue(self.errors_for(candidate))
legacy_available = copy.deepcopy(success)
legacy_available["status"] = "available"
self.assertTrue(self.errors_for(legacy_available))
failed = load_json(CONTRACTS / "evidence" / "v1" / "examples" / "failed.json")
del failed["failure"]