Witness-Alert Primitive — every domain inherits Apple-alert + RSS for free
GFTCL-WITNESS-ALERT-001 · v117 substrate · finance is the first declared domain · ships in this commit.
The pattern, named once
Every domain in the cell already emits witnessed events on NATS. What's been missing everywhere is the uniform path from *witnessed-event* to *operator-notification-plus-durable-subscribable-record*. This primitive ships that path as a shared substrate layer that any domain plugs into by writing one Swift declaration — not a thousand lines of finance-specific or health-specific plumbing.
The shape, in plain terms:
1. A domain witnesses something on NATS — gaiaftcl.finance.capture.sealed, gaiaftcl.health.protocol.cure_proxy_transition, gaiaftcl.mesh.cell.unreachable, etc.
2. An alert rule binds *(subject, field, comparison, threshold, signal)* to that event class. "When Nakamoto < 5, fire a BAD alert." Operator-editable.
3. When a rule fires, the alert lands in alert_queue — a durable, append-only, content-addressed witness log in the cell's SQLite substrate.
4. Two adapters read the queue independently and deliver to the operator:
- Apple-alert adapter →
UNUserNotificationCenterlocal push (signal → interruption level:bad=time-sensitive,good=passive,neutral=active) - RSS adapter →
/feed/alerts.xml(every domain) +/feed/alerts/{domain}.xml(per-domain), served on the existing local HTTP server
5. Every delivery attempt is logged in alert_delivery_log — append-only audit of what reached the operator and when.
The queue's witness columns are immutable by constitutional trigger (trig_alert_queue_witness_immutable); the delivery-log is append-only (trig_alert_delivery_log_append_only). The same discipline the Lion Math sweep receipts run under, now for every alert.
Why two surfaces, and the division of labor
- Apple alert = push. Interrupts the operator *now*; ephemeral by nature; urgent; single-operator; on-device.
- RSS feed = durable witness. Every alert that ever fired, timestamped, immutable, subscribable, re-readable. Each entry carries the receipt reference (rule, comparison, observed value, snapshot hash) so a feed item is not a notification — it's a re-runnable witnessed event.
- Queue between them. Decouples *"the cell witnessed something"* from *"the operator was told."* Nothing is lost when the operator is away, the device is asleep, or push delivery fails.
RSS is the sovereign choice: open standard, pull-based, no third-party push vendor, inherently a feed of timestamped immutable entries (which *is* an append-only witness log in another costume). Anything can subscribe — a reader app, a script, another cell, a regulator. v1 is local-private (127.0.0.1:8423 only); authorized-public via the one-membrane-token architecture is a named next summit.
Architecture
NATS witnessed events
│
▼
┌────────────────────┐
│ AlertGovernor │ ← domain-agnostic actor
│ (GaiaFTCLCore) │ in GaiaFTCLCore
└─────────┬──────────┘
│ writes
▼
┌──────────────────────────────┐
│ alert_queue (SQLite) │ ← APPEND-ONLY
│ witness cols IMMUTABLE │ witness log
└────┬──────────────────┬──────┘
│ │
reads │ │ reads
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ AppleAlertAdapter │ │ AlertFeedRenderer │
│ UNUserNotification │ │ /feed/alerts*.xml │
└──────────────────────┘ └──────────────────────┘
Each domain plugs in via the AlertableDomain protocol with five things:
public protocol AlertableDomain: Sendable {
var domainID: String { get } // "finance"
var watchedSubjects: [String] { get } // ["gaiaftcl.finance.capture.sealed", ...]
var seedRules: [AlertRule] { get } // seeded once; operator-editable
func projectFields(payload: [String: Any]) -> [String: AlertableValue]
func renderSnapshot(rule: AlertRule, observedValue: AlertableValue,
payload: [String: Any]) -> AlertSnapshot
}
That's it. No domain ever writes queue rows, calls UNUserNotificationCenter, or renders RSS — those are the shared primitive's job.
The first domain — finance (reference impl)
Sources/FinanceAlertableEvents/FinanceAlertableEvents.swift declares:
Watched subjects:
gaiaftcl.finance.capture.sealed— capture-concentration sealedgaiaftcl.finance.alert.tripped— direct trip eventgaiaftcl.finance.resolution.concentrated— poly-market resolution concentration
Watchable fields (projected from raw payload to AlertableValue — exact Rat where finance currency lives):
nakamoto—IntNakamoto coefficienttop1_share_pct,top5_share_pct,resolution_share— exact rationalsmarket_id,signal_hint— text
Five seed rules:
| ruleID | comparison | signal | meaning |
|---|---|---|---|
rule.finance.capture.nakamoto_lt_5 |
nakamoto < 5 | bad | < 5 holders control > 50% — capture risk |
rule.finance.capture.top1_share_gt_50pct |
top1_share_pct > 1/2 | bad | single holder > half the market |
rule.finance.capture.deconcentration_ok |
nakamoto ≥ 10 | good | healthy de-concentration |
rule.finance.resolution.concentrated_gt_2_3 |
resolution_share > 2/3 | bad | single resolver > 2/3 of outcomes |
rule.finance.alert.tripped_named_bad |
signal_hint == "bad" | bad | direct finance trip with named bad signal |
Operator edits to these rules persist across launches (seeds use INSERT OR IGNORE).
Survey — where this primitive lands across the cell
The whole point of generalizing: one primitive, every domain inherits. Open frontier of declared-domain summits:
- Finance / poly markets — primary, shipped. Capture-positive, de-concentration, resolution-concentration.
- Health (OWL protocols) — alertable transitions: a protocol moving from CURE-PROXY toward CURE-CLOSED (good), a seal's falsifier theorem breaking under new input (bad), a GAMP-5 receipt expiring (bad). The RSS feed of health-seal state changes IS a regulatory artifact.
- Engineering — composite-seal-invalidated when a constituent primitive changes (bad), summit-closed (good). Design-review backstop subscribes; engineer is alerted before shipping against a broken seal.
- Substrate / mesh health — the cell watching itself. Cell unreachable, NATS subject backing up, dead-letter, witness-store write latency climbing. Sovereign ops monitoring with no third-party observability vendor — possibly the highest-daily-value use for a sole operator.
- Governance (Franklin guardian) — REFUSED terminal states, constitutional rule trips, entropy spent without value. The RSS feed becomes the constitutional audit log.
- Ingestion (VIE / Earth Substrate) — feed-source stale, schema-validation failures, witnessed-data freshness lapsing.
Each is one AlertableDomain declaration away — same shape as FinanceAlertableEvents.swift. No more plumbing.
What ships in this commit
| File | Purpose |
|---|---|
cells/xcode/Sources/GaiaFTCLCore/AlertableEvent.swift |
Protocol + types (AlertableDomain, AlertableValue, AlertRule, AlertSnapshot, AlertSignal, AlertComparison, WitnessHasher) |
cells/xcode/Sources/GaiaFTCLCore/NarratorSchemaV117.swift |
Migration: alert_rules, alert_queue, alert_delivery_log, alert_domain_registry + immutability triggers + PQ CALORIE receipt |
cells/xcode/Sources/GaiaFTCLCore/AlertGovernor.swift |
Domain-agnostic actor: register, ingest, evaluate, queue-write. Idempotent (alert IDs content-addressed on rule_id + payload_sha256) |
cells/xcode/Sources/GaiaFTCLCore/AlertQueueReader.swift |
GRDB-backed reader over the shared SubstrateDatabase pool |
cells/xcode/Sources/GaiaFTCLCore/AppleAlertAdapter.swift |
UNUserNotificationCenter push adapter with signal→interruption-level mapping. Suppresses gracefully when no app bundle. |
cells/xcode/Sources/GaiaNodeServerCore/AlertLedgerReader.swift |
Thin shim delegating to AlertQueueReader (kept here so existing RSS code path stays clean) |
cells/xcode/Sources/GaiaNodeServerCore/AlertFeedRenderer.swift |
RSS 2.0 renderer with signal-prefixed titles ([BAD] / [GOOD] / [INFO]) + audit body |
cells/xcode/Sources/GaiaNodeServerCore/HTTPListener.swift |
Routes /feed/alerts.xml + /feed/alerts/{domain}.xml (slug-validated, no path traversal) |
cells/xcode/Sources/FinanceAlertableEvents/FinanceAlertableEvents.swift |
First declared domain — finance |
cells/xcode/Sources/M8WitnessAlertSmokeTest/main.swift |
End-to-end verification |
Verification
swift run M8WitnessAlertSmokeTest runs end-to-end against the real substrate.sqlite and asserts:
1. ✅ All four V117 tables present
2. ✅ AlertGovernor.register(FinanceAlertableEvents()) writes domain row + 5 seed rules
3. ✅ BAD payload (Nakamoto=3) fires 2 expected rules
4. ✅ GOOD payload (Nakamoto=12) fires the de-concentration rule
5. ✅ Replaying the same payload is idempotent (0 new queue rows — content-addressed alert IDs)
6. ✅ RSS all-feed contains [BAD] entries; finance feed carries witness markers
7. ✅ Content-Type: application/rss+xml; charset=utf-8
8. ✅ Trigger ABORTS any UPDATE to a witness column with witness columns are immutable
9. ✅ Apple-alert drain runs without crash (suppressed when no app bundle / no auth)
swift build — full workspace builds clean.
swift run M8InventoryAuditor — OVERALL TERMINAL: COMPLETE.
v1 scope vs named-next-summit
Shipped in v1:
- Schema + governor + queue
- Apple-alert adapter (graceful no-op when not in an app bundle)
- RSS adapter on existing local HTTP server (local-private only, 127.0.0.1)
- Finance reference declaration with 5 seed rules
- End-to-end smoke test
Named next, deferred:
- Authorized-public RSS via membrane-token gate (so a regulator / consortium / authorized public can subscribe to a safety feed without sovereignty leak)
- Franklin chat rule-authoring ("alert me when any finance capture goes Nakamoto < 5")
- HOME alert-queue surface in Franklin.app (signal-colored, expandable to receipt)
- Additional domain declarations (health, engineering, mesh, governance, ingestion) — each is one Swift file like
FinanceAlertableEvents.swift
Constitutional posture
- No new external dependency. Native Apple
UNUserNotificationCenter+Network.frameworkHTTP + GRDB (already in use). - Substrate never crosses the wire — only the RSS rendering of verdicts + receipt references.
- Witness columns of
alert_queueare immutable by SQL trigger; tampering with sealed alerts is REFUSED by the database itself. - Alert IDs are content-addressed: same rule against same payload digest → same alert ID → INSERT OR IGNORE → no double-fire. Replays are safe and provably so.
- No NATS traffic leaves the cell. No telemetry. No analytics service.
Patent / license notice
© 2026 Richard Gillespie. All rights reserved. USPTO patent applications 19/460,960 and 19/096,071.
434d792bde43905a82d1ee44428334df153073b33f33f9285f927be20ef248fc.
This page serves with a substrate-honest pending-signature notice until the operator's Franklin signer cosigns it.