problem-frame-gate 0.3.0__py3-none-any.whl

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.
@@ -0,0 +1,441 @@
1
+ """Portable data model for finite audit logs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from typing import Any
9
+
10
+ JsonMap = Mapping[str, Any]
11
+ DEFAULT_GATE_BUNDLE_KINDS = ("GateCheck", "OutboxClaim", "UseCap", "ConsumeResource", "RiskClose")
12
+ DEFAULT_RISK_MODES = ("fixed", "selectedEvent", "conditionalSelective", "anytime")
13
+ DEFAULT_PROTECTED_CONSTRUCTORS = {
14
+ "GateCheck": ("executor-gate",),
15
+ "OutboxClaim": ("executor-gate",),
16
+ "UseCap": ("executor-gate",),
17
+ "ConsumeResource": ("executor-gate",),
18
+ "RiskClose": ("executor-gate",),
19
+ }
20
+
21
+
22
+ class EnvelopeClass(str, Enum):
23
+ NORMAL = "normal"
24
+ ABORT = "abort"
25
+ FAIL_CLOSED = "failClosed"
26
+
27
+
28
+ class ClauseRole(str, Enum):
29
+ INVARIANT = "invariant"
30
+ LIVENESS = "liveness"
31
+
32
+
33
+ class Status(str, Enum):
34
+ INACTIVE = "inactive"
35
+ ACTIVE = "active"
36
+ DIAGNOSTIC_ACTIVE = "diagnosticActive"
37
+ SUSPENDED = "suspended"
38
+ INVALID = "invalid"
39
+ BLOCKED = "blocked"
40
+ WITHDRAWN = "withdrawn"
41
+
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class OrderEdge:
45
+ before: str
46
+ after: str
47
+
48
+ @classmethod
49
+ def from_value(cls, value: Sequence[str] | Mapping[str, str]) -> OrderEdge:
50
+ if isinstance(value, Mapping):
51
+ return cls(str(value["before"]), str(value["after"]))
52
+ if len(value) != 2:
53
+ raise ValueError("order edge must contain exactly two event ids")
54
+ return cls(str(value[0]), str(value[1]))
55
+
56
+ def to_json(self) -> list[str]:
57
+ return [self.before, self.after]
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class VersionInterval:
62
+ minimum: int = 1
63
+ maximum: int = 1
64
+
65
+ @classmethod
66
+ def from_value(cls, value: Sequence[int] | Mapping[str, int]) -> VersionInterval:
67
+ if isinstance(value, Mapping):
68
+ return cls(
69
+ int(value.get("minimum", value.get("min", 1))),
70
+ int(value.get("maximum", value.get("max", 1))),
71
+ )
72
+ if len(value) != 2:
73
+ raise ValueError("version interval must contain two bounds")
74
+ return cls(int(value[0]), int(value[1]))
75
+
76
+ def contains(self, version: int) -> bool:
77
+ return self.minimum <= version <= self.maximum
78
+
79
+ def to_json(self) -> dict[str, int]:
80
+ return {"minimum": self.minimum, "maximum": self.maximum}
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class DependencyRef:
85
+ """A dependency by envelope id, or by event plus slot."""
86
+
87
+ eid: str | None = None
88
+ event: str | None = None
89
+ slot: str | None = None
90
+
91
+ @classmethod
92
+ def from_value(cls, value: str | Mapping[str, Any]) -> DependencyRef:
93
+ if isinstance(value, str):
94
+ return cls(eid=value)
95
+ return cls(
96
+ eid=str(value["eid"]) if value.get("eid") is not None else None,
97
+ event=str(value["event"]) if value.get("event") is not None else None,
98
+ slot=str(value["slot"]) if value.get("slot") is not None else None,
99
+ )
100
+
101
+ def to_json(self) -> dict[str, str]:
102
+ data: dict[str, str] = {}
103
+ if self.eid is not None:
104
+ data["eid"] = self.eid
105
+ if self.event is not None:
106
+ data["event"] = self.event
107
+ if self.slot is not None:
108
+ data["slot"] = self.slot
109
+ return data
110
+
111
+
112
+ @dataclass(frozen=True, slots=True)
113
+ class Envelope:
114
+ """One append-only audit-log envelope."""
115
+
116
+ eid: str
117
+ event: str
118
+ slot: str
119
+ commit_time: int
120
+ writer: str
121
+ owner: str
122
+ version: int
123
+ envelope_class: EnvelopeClass
124
+ payload: JsonMap
125
+ dependencies: tuple[DependencyRef, ...] = ()
126
+ commit_group: str | None = None
127
+
128
+ @classmethod
129
+ def from_mapping(cls, value: Mapping[str, Any]) -> Envelope:
130
+ payload = value.get("payload")
131
+ if not isinstance(payload, Mapping):
132
+ raise ValueError("envelope payload must be a JSON object")
133
+ return cls(
134
+ eid=str(value["eid"]),
135
+ event=str(value["event"]),
136
+ slot=str(value.get("slot", value.get("lambda", ""))),
137
+ commit_time=int(value.get("commit_time", value.get("commit", 0))),
138
+ writer=str(value["writer"]),
139
+ owner=str(value.get("owner", "")),
140
+ version=int(value.get("version", 1)),
141
+ envelope_class=EnvelopeClass(value.get("class", value.get("envelope_class", "normal"))),
142
+ payload=dict(payload),
143
+ dependencies=tuple(DependencyRef.from_value(dep) for dep in value.get("dependencies", ())),
144
+ commit_group=str(value["commit_group"]) if value.get("commit_group") is not None else None,
145
+ )
146
+
147
+ @property
148
+ def kind(self) -> str:
149
+ kind = self.payload.get("kind")
150
+ if not isinstance(kind, str) or not kind:
151
+ raise ValueError(f"envelope {self.eid} payload.kind must be a non-empty string")
152
+ return kind
153
+
154
+ @property
155
+ def object_key(self) -> str:
156
+ for key in (
157
+ "object",
158
+ "object_id",
159
+ "frame_id",
160
+ "cert_id",
161
+ "capability_id",
162
+ "risk_id",
163
+ "outbox_id",
164
+ "lease_id",
165
+ "evidence_id",
166
+ "source_id",
167
+ "gate_id",
168
+ "bundle_id",
169
+ ):
170
+ value = self.payload.get(key)
171
+ if value is not None:
172
+ return str(value)
173
+ return ""
174
+
175
+ @property
176
+ def protected_slot(self) -> tuple[str, str, str, str]:
177
+ return (self.event, self.slot, self.kind, self.object_key)
178
+
179
+ def to_json(self) -> dict[str, Any]:
180
+ data: dict[str, Any] = {
181
+ "eid": self.eid,
182
+ "event": self.event,
183
+ "slot": self.slot,
184
+ "commit": self.commit_time,
185
+ "writer": self.writer,
186
+ "owner": self.owner,
187
+ "version": self.version,
188
+ "class": self.envelope_class.value,
189
+ "payload": dict(self.payload),
190
+ }
191
+ if self.dependencies:
192
+ data["dependencies"] = [dep.to_json() for dep in self.dependencies]
193
+ if self.commit_group:
194
+ data["commit_group"] = self.commit_group
195
+ return data
196
+
197
+
198
+ @dataclass(frozen=True, slots=True)
199
+ class Horizon:
200
+ """Finite verifier configuration.
201
+
202
+ This is strict by default. Empty safety tables are rejected by the verifier.
203
+ Use :meth:`unsafe_for_tests` only for deliberately weak unit fixtures.
204
+ """
205
+
206
+ strict: bool = True
207
+ events: tuple[str, ...] = ()
208
+ causal_order: tuple[OrderEdge, ...] = ()
209
+ availability_order: tuple[OrderEdge, ...] = ()
210
+ audit_order: tuple[OrderEdge, ...] = ()
211
+ capacities: Mapping[EnvelopeClass, int] = field(default_factory=dict)
212
+ writer_authority: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
213
+ version_intervals: Mapping[str, VersionInterval] = field(default_factory=dict)
214
+ commit_groups: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
215
+ protected_constructors: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
216
+ gate_bundle_kinds: tuple[str, ...] = DEFAULT_GATE_BUNDLE_KINDS
217
+ executor_writer: str = "executor-gate"
218
+ clock_policy: str = "integer-commit-time"
219
+ certificate_families: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
220
+ risk_modes: tuple[str, ...] = DEFAULT_RISK_MODES
221
+ env_assumptions: tuple[str, ...] = ()
222
+ codebook: tuple[str, ...] = ()
223
+ allow_local_paths: bool = False
224
+
225
+ @classmethod
226
+ def from_mapping(cls, value: Mapping[str, Any]) -> Horizon:
227
+ return cls(
228
+ strict=bool(value.get("strict", True)),
229
+ events=tuple(str(v) for v in value.get("events", ())),
230
+ causal_order=_edges(value.get("causal_order", ())),
231
+ availability_order=_edges(value.get("availability_order", ())),
232
+ audit_order=_edges(value.get("audit_order", ())),
233
+ capacities={EnvelopeClass(k): int(v) for k, v in value.get("capacities", {}).items()},
234
+ writer_authority={
235
+ str(k): tuple(str(writer) for writer in writers)
236
+ for k, writers in value.get("writer_authority", {}).items()
237
+ },
238
+ version_intervals={
239
+ str(k): VersionInterval.from_value(v) for k, v in value.get("version_intervals", {}).items()
240
+ },
241
+ commit_groups={
242
+ str(k): tuple(str(eid) for eid in eids) for k, eids in value.get("commit_groups", {}).items()
243
+ },
244
+ protected_constructors={
245
+ str(k): tuple(str(writer) for writer in writers)
246
+ for k, writers in value.get("protected_constructors", {}).items()
247
+ },
248
+ gate_bundle_kinds=tuple(str(v) for v in value.get("gate_bundle_kinds", DEFAULT_GATE_BUNDLE_KINDS)),
249
+ executor_writer=str(value.get("executor_writer", "executor-gate")),
250
+ clock_policy=str(value.get("clock_policy", "integer-commit-time")),
251
+ certificate_families={
252
+ str(k): tuple(str(issuer) for issuer in issuers)
253
+ for k, issuers in value.get("certificate_families", {}).items()
254
+ },
255
+ risk_modes=tuple(str(v) for v in value.get("risk_modes", DEFAULT_RISK_MODES)),
256
+ env_assumptions=tuple(str(v) for v in value.get("env_assumptions", ())),
257
+ codebook=tuple(str(v) for v in value.get("codebook", ())),
258
+ allow_local_paths=bool(value.get("allow_local_paths", False)),
259
+ )
260
+
261
+ @classmethod
262
+ def strict_default(
263
+ cls,
264
+ *,
265
+ agent_writers: tuple[str, ...] = ("agent",),
266
+ executor_writer: str = "executor-gate",
267
+ normal_capacity: int = 1000,
268
+ abort_capacity: int = 100,
269
+ fail_closed_capacity: int = 10,
270
+ ) -> Horizon:
271
+ """Return a strict manifest suitable for examples and production templates."""
272
+
273
+ protected = dict.fromkeys(DEFAULT_GATE_BUNDLE_KINDS, (executor_writer,))
274
+ writer_authority: dict[str, tuple[str, ...]] = {"*": (*agent_writers, executor_writer)}
275
+ writer_authority.update(protected)
276
+ return cls(
277
+ strict=True,
278
+ capacities={
279
+ EnvelopeClass.NORMAL: normal_capacity,
280
+ EnvelopeClass.ABORT: abort_capacity,
281
+ EnvelopeClass.FAIL_CLOSED: fail_closed_capacity,
282
+ },
283
+ writer_authority=writer_authority,
284
+ version_intervals={"*": VersionInterval(1, 1)},
285
+ protected_constructors=protected,
286
+ gate_bundle_kinds=DEFAULT_GATE_BUNDLE_KINDS,
287
+ executor_writer=executor_writer,
288
+ certificate_families={
289
+ "source": agent_writers,
290
+ "risk": agent_writers,
291
+ "approval": agent_writers,
292
+ "safety": agent_writers,
293
+ "formation": agent_writers,
294
+ },
295
+ risk_modes=DEFAULT_RISK_MODES,
296
+ codebook=DEFAULT_RISK_MODES,
297
+ )
298
+
299
+ @classmethod
300
+ def unsafe_for_tests(cls) -> Horizon:
301
+ """Return a deliberately weak manifest for tests that do not model authority."""
302
+
303
+ return cls(
304
+ strict=False,
305
+ protected_constructors={},
306
+ gate_bundle_kinds=(),
307
+ allow_local_paths=False,
308
+ )
309
+
310
+ def to_json(self) -> dict[str, Any]:
311
+ return {
312
+ "strict": self.strict,
313
+ "events": list(self.events),
314
+ "causal_order": [edge.to_json() for edge in self.causal_order],
315
+ "availability_order": [edge.to_json() for edge in self.availability_order],
316
+ "audit_order": [edge.to_json() for edge in self.audit_order],
317
+ "capacities": {key.value: value for key, value in self.capacities.items()},
318
+ "writer_authority": {key: list(value) for key, value in self.writer_authority.items()},
319
+ "version_intervals": {key: value.to_json() for key, value in self.version_intervals.items()},
320
+ "commit_groups": {key: list(value) for key, value in self.commit_groups.items()},
321
+ "protected_constructors": {key: list(value) for key, value in self.protected_constructors.items()},
322
+ "gate_bundle_kinds": list(self.gate_bundle_kinds),
323
+ "executor_writer": self.executor_writer,
324
+ "clock_policy": self.clock_policy,
325
+ "certificate_families": {key: list(value) for key, value in self.certificate_families.items()},
326
+ "risk_modes": list(self.risk_modes),
327
+ "env_assumptions": list(self.env_assumptions),
328
+ "codebook": list(self.codebook),
329
+ "allow_local_paths": self.allow_local_paths,
330
+ }
331
+
332
+
333
+ @dataclass(frozen=True, slots=True)
334
+ class StrictManifest(Horizon):
335
+ """Named strict manifest type for public APIs."""
336
+
337
+ @classmethod
338
+ def minimal(cls, **kwargs: Any) -> StrictManifest:
339
+ horizon = Horizon.strict_default(**kwargs)
340
+ return cls.from_mapping(horizon.to_json())
341
+
342
+ @classmethod
343
+ def from_mapping(cls, value: Mapping[str, Any]) -> StrictManifest:
344
+ horizon = Horizon.from_mapping(value)
345
+ data = horizon.to_json()
346
+ data["strict"] = True
347
+ return cls(
348
+ strict=True,
349
+ events=tuple(data["events"]),
350
+ causal_order=_edges(data["causal_order"]),
351
+ availability_order=_edges(data["availability_order"]),
352
+ audit_order=_edges(data["audit_order"]),
353
+ capacities={EnvelopeClass(k): int(v) for k, v in data["capacities"].items()},
354
+ writer_authority={k: tuple(v) for k, v in data["writer_authority"].items()},
355
+ version_intervals={k: VersionInterval.from_value(v) for k, v in data["version_intervals"].items()},
356
+ commit_groups={k: tuple(v) for k, v in data["commit_groups"].items()},
357
+ protected_constructors={k: tuple(v) for k, v in data["protected_constructors"].items()},
358
+ gate_bundle_kinds=tuple(data["gate_bundle_kinds"]),
359
+ executor_writer=str(data["executor_writer"]),
360
+ clock_policy=str(data["clock_policy"]),
361
+ certificate_families={k: tuple(v) for k, v in data["certificate_families"].items()},
362
+ risk_modes=tuple(data["risk_modes"]),
363
+ env_assumptions=tuple(data["env_assumptions"]),
364
+ codebook=tuple(data["codebook"]),
365
+ allow_local_paths=bool(data["allow_local_paths"]),
366
+ )
367
+
368
+
369
+ @dataclass(frozen=True, slots=True)
370
+ class Frame:
371
+ """A bounded decision or problem frame controlled by the audit log."""
372
+
373
+ frame_id: str
374
+ scope: str
375
+ goal: str
376
+ evidence_ids: tuple[str, ...]
377
+ actions: tuple[str, ...]
378
+ acceptance: tuple[str, ...]
379
+ resources: tuple[str, ...] = ()
380
+ risk_ids: tuple[str, ...] = ()
381
+ obligations: tuple[str, ...] = ()
382
+ provenance: Mapping[str, Any] = field(default_factory=dict)
383
+
384
+ @classmethod
385
+ def from_payload(cls, payload: Mapping[str, Any]) -> Frame:
386
+ return cls(
387
+ frame_id=str(payload.get("frame_id", payload.get("object", ""))),
388
+ scope=str(payload.get("scope", "")),
389
+ goal=str(payload.get("goal", "")),
390
+ evidence_ids=tuple(str(v) for v in payload.get("evidence_ids", ())),
391
+ actions=tuple(str(v) for v in payload.get("actions", ())),
392
+ acceptance=tuple(str(v) for v in payload.get("acceptance", ())),
393
+ resources=tuple(str(v) for v in payload.get("resources", ())),
394
+ risk_ids=tuple(str(v) for v in payload.get("risk_ids", ())),
395
+ obligations=tuple(str(v) for v in payload.get("obligations", ())),
396
+ provenance=dict(payload.get("provenance", {})),
397
+ )
398
+
399
+ def to_json(self) -> dict[str, Any]:
400
+ return {
401
+ "frame_id": self.frame_id,
402
+ "scope": self.scope,
403
+ "goal": self.goal,
404
+ "evidence_ids": list(self.evidence_ids),
405
+ "actions": list(self.actions),
406
+ "acceptance": list(self.acceptance),
407
+ "resources": list(self.resources),
408
+ "risk_ids": list(self.risk_ids),
409
+ "obligations": list(self.obligations),
410
+ "provenance": dict(self.provenance),
411
+ }
412
+
413
+
414
+ @dataclass(frozen=True, slots=True)
415
+ class AuditTranscript:
416
+ """Finite record of objects read by a checker."""
417
+
418
+ checker: str
419
+ objects: tuple[str, ...] = ()
420
+ reads: tuple[str, ...] = ()
421
+ frontiers: tuple[str, ...] = ()
422
+ clocks: tuple[str, ...] = ()
423
+ capacities: tuple[str, ...] = ()
424
+ digests: tuple[str, ...] = ()
425
+ swaps: tuple[str, ...] = ()
426
+
427
+ def to_json(self) -> dict[str, Any]:
428
+ return {
429
+ "checker": self.checker,
430
+ "objects": list(self.objects),
431
+ "reads": list(self.reads),
432
+ "frontiers": list(self.frontiers),
433
+ "clocks": list(self.clocks),
434
+ "capacities": list(self.capacities),
435
+ "digests": list(self.digests),
436
+ "swaps": list(self.swaps),
437
+ }
438
+
439
+
440
+ def _edges(values: Sequence[Any]) -> tuple[OrderEdge, ...]:
441
+ return tuple(OrderEdge.from_value(value) for value in values)
@@ -0,0 +1,191 @@
1
+ """Append-only patch installation checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from .fold import FoldKernel, FoldState
10
+ from .model import Envelope, Horizon
11
+ from .result import CheckBuilder, CheckResult
12
+ from .verifier import digest_log
13
+
14
+ Invariant = Callable[[FoldState, Sequence[Envelope]], CheckResult]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class ReadFootprint:
19
+ invariant: str
20
+ entries: tuple[str, ...]
21
+
22
+ def to_json(self) -> dict[str, Any]:
23
+ return {"invariant": self.invariant, "entries": list(self.entries)}
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class WriteClass:
28
+ name: str
29
+ object_id: str = ""
30
+
31
+ def to_json(self) -> dict[str, str]:
32
+ return {"name": self.name, "object_id": self.object_id}
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class TouchMatrix:
37
+ cells: dict[str, str] = field(default_factory=dict)
38
+
39
+ @staticmethod
40
+ def key(write_class: WriteClass, read_entry: str) -> str:
41
+ return f"{write_class.name}:{write_class.object_id}|{read_entry}"
42
+
43
+ def verdict(self, write_class: WriteClass, read_entry: str) -> str | None:
44
+ return self.cells.get(self.key(write_class, read_entry))
45
+
46
+ def to_json(self) -> dict[str, str]:
47
+ return dict(self.cells)
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class AffectedClauseSet:
52
+ clauses: tuple[str, ...]
53
+
54
+ def to_json(self) -> list[str]:
55
+ return list(self.clauses)
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class PatchProposal:
60
+ expected_source_digest: str
61
+ append: tuple[Envelope, ...]
62
+ affected_invariants: tuple[str, ...] = ()
63
+ write_classes: tuple[WriteClass, ...] = ()
64
+ read_footprints: tuple[ReadFootprint, ...] = ()
65
+ touch_matrix: TouchMatrix = field(default_factory=TouchMatrix)
66
+ transcript_digest: str | None = None
67
+
68
+ def to_json(self) -> dict[str, object]:
69
+ return {
70
+ "expected_source_digest": self.expected_source_digest,
71
+ "append": [env.to_json() for env in self.append],
72
+ "affected_invariants": list(self.affected_invariants),
73
+ "write_classes": [write.to_json() for write in self.write_classes],
74
+ "read_footprints": [read.to_json() for read in self.read_footprints],
75
+ "touch_matrix": self.touch_matrix.to_json(),
76
+ "transcript_digest": self.transcript_digest,
77
+ }
78
+
79
+
80
+ class PatchChecker:
81
+ """Practical append-only subset of the paper's patch checker."""
82
+
83
+ footprint = frozenset({"PatchKernel", "FoldKernel", "EnvelopeVerifier", "ClauseKernel"})
84
+
85
+ def __init__(self, fold_kernel: FoldKernel | None = None) -> None:
86
+ self.fold_kernel = fold_kernel or FoldKernel()
87
+
88
+ def check(
89
+ self,
90
+ horizon: Horizon,
91
+ source: Sequence[Envelope],
92
+ proposal: PatchProposal,
93
+ *,
94
+ invariants: dict[str, Invariant] | None = None,
95
+ ) -> CheckResult:
96
+ builder = CheckBuilder(footprint=set(self.footprint))
97
+ source_digest = digest_log(source)
98
+ if proposal.expected_source_digest != source_digest:
99
+ builder.error(
100
+ "patch-source-digest",
101
+ "source digest does not match patch proposal",
102
+ details={"expected": proposal.expected_source_digest, "actual": source_digest},
103
+ )
104
+
105
+ source_ids = {env.eid for env in source}
106
+ duplicate_appends = [env.eid for env in proposal.append if env.eid in source_ids]
107
+ if duplicate_appends:
108
+ builder.error(
109
+ "patch-not-append-only",
110
+ "patch contains existing envelope ids",
111
+ details={"eids": duplicate_appends},
112
+ )
113
+
114
+ self._check_footprints(proposal, invariants or {}, builder)
115
+
116
+ target = tuple(source) + tuple(proposal.append)
117
+ try:
118
+ folded = self.fold_kernel.fold(horizon, target)
119
+ except Exception as exc:
120
+ builder.error("patch-target-fold", f"patch target is not a legal folded log: {exc}")
121
+ return builder.result(digest=source_digest)
122
+
123
+ for name, invariant in (invariants or {}).items():
124
+ if name not in proposal.affected_invariants:
125
+ builder.warning(
126
+ "patch-invariant-not-rechecked",
127
+ "invariant was supplied but not listed as affected",
128
+ location=name,
129
+ )
130
+ continue
131
+ result = invariant(folded, target)
132
+ if not result.ok:
133
+ for issue in result.issues:
134
+ builder.error(issue.code, issue.message, location=issue.location, details=issue.details)
135
+ self._check_frame_invalidation(folded, proposal, builder)
136
+
137
+ return builder.result(digest=digest_log(target))
138
+
139
+ def _check_footprints(
140
+ self,
141
+ proposal: PatchProposal,
142
+ invariants: dict[str, Invariant],
143
+ builder: CheckBuilder,
144
+ ) -> None:
145
+ footprint_by_invariant = {footprint.invariant: footprint for footprint in proposal.read_footprints}
146
+ affected = set(proposal.affected_invariants)
147
+ for name in invariants:
148
+ footprint = footprint_by_invariant.get(name)
149
+ if footprint is None:
150
+ builder.error("patch-footprint-missing", "invariant has no read footprint witness", location=name)
151
+ continue
152
+ touched = False
153
+ for write in proposal.write_classes:
154
+ for read_entry in footprint.entries:
155
+ verdict = proposal.touch_matrix.verdict(write, read_entry)
156
+ if verdict not in {"touch", "non_touch"}:
157
+ builder.error(
158
+ "patch-touch-cell",
159
+ "touch matrix must classify every write/read pair",
160
+ location=name,
161
+ details={"write": write.to_json(), "read": read_entry},
162
+ )
163
+ touched = touched or verdict == "touch"
164
+ if touched and name not in affected:
165
+ builder.error(
166
+ "patch-affected-completeness",
167
+ "touched invariant is missing from affected set",
168
+ location=name,
169
+ )
170
+
171
+ def _check_frame_invalidation(self, folded: FoldState, proposal: PatchProposal, builder: CheckBuilder) -> None:
172
+ non_active_frames = {
173
+ str(env.payload.get("frame_id", env.payload.get("object", "")))
174
+ for env in proposal.append
175
+ if env.kind in {"Suspended", "Invalidated", "Withdrawn"}
176
+ }
177
+ for frame_id in non_active_frames:
178
+ for cap_id, cap in folded.component("capabilities").items():
179
+ if cap.get("frame_id") == frame_id and cap.get("status") == "unused":
180
+ builder.error(
181
+ "patch-frame-invalidates-capability",
182
+ "non-active frame patch must revoke, expire, or use outstanding capabilities",
183
+ location=cap_id,
184
+ )
185
+ for outbox_id, outbox in folded.component("outboxes").items():
186
+ if outbox.get("frame_id") == frame_id and outbox.get("status") == "authorized":
187
+ builder.error(
188
+ "patch-frame-invalidates-outbox",
189
+ "non-active frame patch must revoke or block unclaimed outboxes",
190
+ location=outbox_id,
191
+ )
@@ -0,0 +1 @@
1
+