approval-engine/docs/emission-cadence.md
tegwick a786b061da Derive heartbeat cadence from the shared coordination frame
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
2026-10-15 20:41:49 +02:00

161 lines
7.9 KiB
Markdown

# Emission cadence — source declaration
Statute §9.6: approval evidence is **load-bearing** and **low-volume**. Rate
monitoring is the wrong form. A handful of revocations a month has no rate to
drop below; suppression is indistinguishable from a quiet month.
This file is the source side of `GH-WP-0002-T04`. That task is the detection
surface; this declaration is what it reads. Machine-readable copy:
[`../cadence.yaml`](../cadence.yaml).
## Form
**Heartbeat, plus reconciliation counts.** Not a rate.
### Heartbeat
An authenticated positive claim: *nothing to report*, together with cumulative
per-class committed transition counts. The claim can itself go missing, which is the point — silence
becomes a missing positive rather than a quiet month.
| Field | Value |
| --- | --- |
| Class | `heartbeat` |
| Interval | f = 1 hour; SHOULD also be emittable on demand |
| Assertion | `nothing-to-report` |
| Counts | `{issuance, use, supersession, revocation}` of committed outbox rows |
| Missing heartbeat | **finding** after effective 3f = 3h (registration 2f = 2h plus receiver 1.5 grace); admission pending |
### Reconciliation
Compare this engine's committed outbox counts per class to `audit-core`'s
accepted event counts for `source=approval-engine` and the corresponding
`action`. Divergence is a **finding**, not a log line.
| This engine | `audit-core` `action` |
| --- | --- |
| `issuance` | `approval.issuance` |
| `use` | `approval.use` |
| `supersession` | `approval.supersession` |
| `revocation` | `approval.revocation` |
| `heartbeat` | `audit-core.heartbeat` |
Account for occurrence versus receiver acceptance time as specified below;
identical window bounds alone do not align those populations. Undrained local
rows are this engine's lag, not yet a divergence. A row
with `drained_at` set that `audit-core` does not hold is the omission
case §9.6 names.
## What is a finding
- No per-class heartbeat arrives beyond the effective 3f deadline (f emission interval).
- `audit-core` count for a class is less than this engine's drained count
for that class.
- Outbox depth or age exceeds the lag bound in `cadence.yaml` (drain stuck).
None of these are rate drops. None of these are informational logs.
## Residual
Atomicity prevents accidental omission (crash between mutation and emit).
Cadence and reconciliation **detect** adversarial omission after the fact.
Nothing in the model prevents a compromised source from suppressing. That
residual is stated, not closed.
## Imported declaration and source inventory — 2026-10-03
`cadence.yaml` now uses InfoTechCanon's published candidate 0.2.0 contract,
schema 0.1, rather than the historical flattened draft. The source-owned
inventory is `extensions.approval-engine.inventory`: issuance, use,
supersession and revocation are rare load-bearing transitions; heartbeat is
the load-bearing delivery control, reconciled without a recursive heartbeat.
No evidence classification or emission interval has been relaxed.
The implementation emits four atomic `audit-core.heartbeat` events, asserting
the four transition classes through `data.class`; it does not emit
`approval.heartbeat`. Production CLI defaults to 86400s. The receiver request
in `audit-source-registration.md` remains 172800s; it is not a live admission
receipt. Lag depth 100 and age 3600s remain declared bounds, with observer
enforcement pending. Repository conformance does not prove stream completeness.
Live closure stays in APPROVAL-WP-0002-T04: audit-core/platform must bind exact
sender `approval-engine`, tenant `tenant:platform`, explicit redact policy,
the already delivered protected credential/native custody (2026-09-11),
authenticated ingest and all four heartbeat class registrations.
Synthetic native accepted/duplicate, read-refusal and independent archive
readback already passed in the platform receipts
`docs/evidence/2026-09-11-factory-native-producers.json` and
`docs/evidence/2026-09-11-factory-native-readback.json`; these do not prove
deployed service or lifecycle transaction emission. Remaining acceptance
requires service admission, actual lifecycle drain and bearer revocation,
same-window drained-versus-accepted reconciliation, startup heartbeat and
missing-class/lag findings. Existing AUDIT-WP-0009 detection code is reused;
neither a second observer nor new receiver APIs are required.
Orientation: Emergent Knowledge 0.2.5, digest
`9e186dbd6f2d5eb455bca24db4b03cd866c63c4e94660430dd15f2ca9856215f`,
start/evidence cards verified-files, orientation-only.
Validation: full owner suite 169 passed. Imported owner schema SHA-256
`6d52692eb1e4d1325e0d6062d95acded98beddaae5bcfe24c6f0e91f1b6be6ae`
and NetKingdom profile returned contract_valid/profile_assessed/conformant true
with zero findings for the authoritative inventory above. Reproduce from
net-kingdom with:
```bash
uv run tools/emission-cadence-profile/emission_cadence_profile.py \
--contract-schema ../info-tech-canon/infospace/schemas/emission-cadence.schema.yaml \
--load-bearing heartbeat \
--rare-load-bearing issuance --rare-load-bearing use \
--rare-load-bearing supersession --rare-load-bearing revocation \
../approval-engine/cadence.yaml
```
This supplies an owner-authored instance to NK-WP-0035; it does not accept the
proposed NetKingdom profile or claim receiver admission/observed cadence.
## Bounded source snapshot — 2026-10-04
`GET /v1/cadence?since=<timestamp>&until=<timestamp>` requires approval:observe
and the exact store tenant. Supply exactly one timezone-aware since and until,
with since earlier than until; encode plus signs in timezone offsets. Bounds
are inclusive since, exclusive until. The existing query-free lifetime response
is preserved; it cannot substitute for a bounded reconciliation.
The response identifies source, tenant, normalized UTC bounds and window_basis
created_at. Each class has committed, drained and pending counts for rows created
in that window, plus acknowledged_in_window for local drain acknowledgments in
that window regardless of creation time. All counts come from one database
statement snapshot. Zero classes are explicit; no payloads or identities are
selected or returned, and the source performs no receiver calls.
Audit Core's published `/v1/reconciliation` uses **receiver acceptance time**,
not occurrence time. Local drained_at is acknowledgment time and may follow
acceptance, especially if an accepted event's response was lost and a duplicate
was acknowledged later. Equal textual since/until bounds alone do not align
those populations. The observer must account for delayed delivery, acknowledgment
boundaries and duplicate acceptance, or use independently correlated event ids
and an agreed settled window before treating a count mismatch as omission.
Pending occurrence rows are lag; neither count agreement nor this source
snapshot proves completeness or resistance to source compromise.
This implements T04's missing bounded source computation. Receiver registration,
observed lag/findings and native reconciliation remain blocked owner acceptance;
no existing live image is promoted by this change.
## Configurable coordination frame — 2026-10-15
The timing paragraphs in the dated October 3 migration section describe its
historical daily defaults. Current source defaults and the prepared manifest
are derived from coordination-engine/config/coordination-frame.json, initially
f=3600s by founder decision. The generated approval_engine/coordination.py and
cadence.yaml retain the config digest; CLI and direct OutboxWorker defaults
use FRAME_SECONDS. The deployment argument explicitly supplies the same value
and its pod annotation retains the frame digest. Runtime applies remain under
the owner change gate; no hourly live cadence is claimed by this source edit.
Class mappings, ownership, atomic outbox behavior and count-window accounting
are unchanged. A different frame is regenerated for emitter and observer
settings together using net-kingdom/tools/coordination-frame/render.py.