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:

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

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:

Watchable fields (projected from raw payload to AlertableValue — exact Rat where finance currency lives):

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:

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:

Named next, deferred:

Constitutional posture

Patent / license notice

© 2026 Richard Gillespie. All rights reserved. USPTO patent applications 19/460,960 and 19/096,071.

Federation cosignature: pending operator signing host (v26). Witness (sha256 of rendered body): 434d792bde43905a82d1ee44428334df153073b33f33f9285f927be20ef248fc. This page serves with a substrate-honest pending-signature notice until the operator's Franklin signer cosigns it.