2026-04-24

Canary EJ Spine + Sales Audit — the Canary-Native Naming for the Perpetual Layer

Canary IP — naming and layer model. EJ Spine and Sales Audit are Canary's named primitives. The EJ Spine (Electronic Journal) is the perpetual movement layer that lands every operational signal from a connected POS as a hash-chained, append-only event stream against the canonical retail data model. Sales Audit is the projection layer that scrubs and aggregates the EJ stream into the period-summary outputs downstream merchant tools (accounting, payroll, CRM, e-commerce) actually consume. The tie between EJ Spine, Sales Audit, the perpetual stock ledger, and Canary's satoshi-precision cost calculation is original Canary design — these names appear in the codebase and the Linear backlog. This article anchors the naming so the substrate primitives, the merchant tool integrations, and the agent surfaces use one vocabulary.

The TSP orchestration that writes the EJ Spine

The Transaction Sealing Pipeline (TSP) is what produces the EJ Spine in real time. Pre-authored figure from the Canary Atlas (fig-p00, full source):

flowchart TD
    %% External entry
    SQ["Square Webhooks\nHMAC-SHA256 Signed"]:::external
    GW["TSP-01 Gateway\nValidate + Hash + Publish"]:::primary

    %% Valkey backbone
    VS[("Valkey Stream\ncanary:events\n9-field message")]:::storage
    DLQ[("Dead Letter Stream\ncanary:dead_letter")]:::alert

    %% Three independent consumer groups on canary:events
    subgraph SUB1_GROUP["Sub 1 — Hash & Seal"]
        S1["sub1-seal\nConsumer Group"]:::processing
        S1_VERIFY["Verify event_hash\nSHA-256 recompute"]:::processing
        S1_CHAIN["Compute chain_hash\nAdvisory lock + append"]:::processing
        S1_WRITE["INSERT evidence_records\nWrite-once, immutable"]:::processing
    end

    subgraph SUB2_GROUP["Sub 2 — Parse & Route (EJ Spine writer)"]
        S2["sub2-parse\nConsumer Group"]:::processing
        S2_DISPATCH["Webhook Dispatch\nresolve_route()"]:::processing
        S2_PARSE["Domain Parser\n18 Square parsers"]:::processing
        S2_CRDM["CRDM Write — EJ Spine\nUpsert to canary.sales"]:::processing
    end

    subgraph SUB3_GROUP["Sub 3 — Merkle batcher (RaaS-ready output; pre-audit)"]
        S3["sub3-merkle\nConsumer Group"]:::processing
        S3_ACCUM["Batch Accumulator\nValkey sorted set"]:::processing
        S3_TREE["Merkle Tree Build\nSort + pad + SHA-256"]:::bitcoin
    end

    %% Detection stream — bridge between Sub 2 and Sub 4
    DS[("Detection Stream\ncanary:detection\n5-field message")]:::storage

    subgraph SUB4_GROUP["Sub 4 — Chirp Detection (Q module entry)"]
        S4["detection-engine\nConsumer Group"]:::alert
        S4_EVAL["ChirpRuleEngine\n37 frozen rules"]:::alert
        S4_ALERT["write_alerts_to_session"]:::alert
    end

    %% Storage sinks
    PG_SALES[("PostgreSQL\ncanary.sales (EJ Spine tables)\nevidence_records\ntransactions + child tables")]:::storage
    PG_APP[("PostgreSQL\ncanary.app\nalerts, alert_history")]:::storage

    %% Flow: Gateway to stream
    SQ -->|"POST /webhooks/square"| GW
    GW -->|"SHA-256 hash + ULID event_id\nXADD 9-field message"| VS
    GW -.->|"HMAC failure"| DLQ

    %% Fan-out: three groups read independently
    VS --> S1
    VS --> S2
    VS --> S3

    %% Sub 1 internal
    S1 --> S1_VERIFY
    S1_VERIFY --> S1_CHAIN
    S1_VERIFY -.->|"TAMPER"| DLQ
    S1_CHAIN --> S1_WRITE
    S1_WRITE --> PG_SALES

    %% Sub 2 internal — writes EJ Spine
    S2 --> S2_DISPATCH
    S2_DISPATCH --> S2_PARSE
    S2_PARSE --> S2_CRDM
    S2_CRDM -->|"EJ Spine write"| PG_SALES
    S2_CRDM --> DS

    %% Sub 3 internal
    S3 --> S3_ACCUM
    S3_ACCUM --> S3_TREE

    %% Sub 4: detection stream consumer
    DS --> S4
    S4 --> S4_EVAL
    S4_EVAL --> S4_ALERT
    S4_ALERT --> PG_APP

    %% Styles
    classDef primary fill:#1a5276,stroke:#fff,color:#fff
    classDef processing fill:#7d3c98,stroke:#fff,color:#fff
    classDef bitcoin fill:#b7950b,stroke:#fff,color:#fff
    classDef storage fill:#1e8449,stroke:#fff,color:#fff
    classDef alert fill:#e74c3c,stroke:#fff,color:#fff
    classDef external fill:#2c3e50,stroke:#fff,color:#fff

    style SUB1_GROUP fill:#1c2833,stroke:#7d3c98,color:#fff
    style SUB2_GROUP fill:#1c2833,stroke:#7d3c98,color:#fff
    style SUB3_GROUP fill:#1c2833,stroke:#b7950b,color:#fff
    style SUB4_GROUP fill:#1c2833,stroke:#e74c3c,color:#fff

Read against the EJ Spine framing:

Sales Audit (the scrub-and-aggregate projection layer) sits downstream of EJ writes and is not pictured here — it reads from canary.sales (the EJ Spine tables) and produces clean projections for downstream consumers (merchant accounting tools, period reports, etc.). A separate figure for Sales Audit is queued.

Per-transaction state machine. The state-machine view of one transaction's journey through this pipeline is captured in atlas figure L-01 (Transaction Lifecycle) at Canary/docs/atlas/lifecycle/fig-l01-transaction-lifecycle.md.

EJ Spine — what Canary actually has

The Electronic Journal is the transaction spine. It carries everything that lands on Canary from a connected POS:

The EJ is append-only at the per-event level, hash-chained per merchant, and queryable across every child table. It is what the user means when they say "it has everything" — there is no operational signal from the POS that the EJ does not capture.

In code terms, the EJ Spine is the joined view across: - sales.transactions (the root) - sales.transaction_line_items - sales.transaction_tenders - sales.refund_links - sales.cash_drawer_* - sales.gift_card_* - sales.disputes - sales.invoices - app.evidence_records (the per-event seal) - app.ej_links (links source-system order ID to local transaction UUID)

Module T (Transaction Pipeline) is the publisher of the EJ Spine at v1. As C/D/F/J/S/P/L/W ship, each one publishes additional movement verbs into the same EJ — receipt + transfer + RTV + adjustment events from D, cost-update events from C, markdown events from P, time-clock events from L, etc. The EJ is the universal substrate every spine module writes to.

Sales Audit — the scrub-and-aggregate projection layer

Raw EJ output is too noisy for downstream consumers. It contains:

Sales Audit is Canary's projection layer:

  1. Scrubs — drops or flags non-authoritative events (cancelled, duplicate, test, pending, adjudication-needed) so downstream consumers see clean data
  2. Aggregates — rolls up per-event detail into the granularity each downstream consumer needs (per-transaction summary, per-day department total, per-week store P&L line, per-month GL posting batch)
  3. Reconciles — compares its own scrubbed-and-aggregated output against the merchant's existing tool's period summary (where the merchant has one) and surfaces variance

Sales Audit is what the merchant's QuickBooks / Xero / Gusto / loyalty- platform / e-commerce-platform actually consumes when they integrate with Canary. They never see raw EJ. They see Sales Audit-scrubbed projections at the granularity their tool expects.

Where this maps in the substrate articles

The CATz substrate articles describe the same pattern in generic terms:

CATz substrate generic term Canary-native name
Perpetual movement layer EJ Spine
Movement publisher (per-module) EJ Spine writer (T at v1; D/C/F/J/etc as they ship)
Period-summary projection Sales Audit scrubbed-and-aggregated output
Merchant's existing tool subscribing to projection Reads Sales Audit, not raw EJ
Reconciliation surface Sales Audit's variance report against merchant tool's period summary

The perpetual-vs-period boundary article describes the seam between perpetual movement and period summary. In Canary, that seam is between the EJ Spine and Sales Audit: everything EJ-side is Canary's perpetual; everything that flows out of Sales Audit to a merchant tool (or to Canary's own period reports) is projection.

The staged migration phases re-read in Canary terms:

Existing EJ Spine work (Linear)

These tickets show the EJ Spine concept is already operational in the Canary codebase at the UI / API surface for reading. The substrate work this session formalizes the WRITE side of the EJ Spine across the 13-module spine (every module's manifest declares which EJ verbs it publishes).

Implications for the substrate articles

The CATz substrate articles can stay generic (the substrate principles apply to any perpetual-movement-layer instantiation). But where they discuss the Canary instantiation, they should reference the EJ Spine and Sales Audit by name. Specifically:

These edits are small (one cross-reference each). Captured here so the naming alignment is explicit and the next pass on the substrate articles can fold it in cleanly.

Open questions

  1. Does Sales Audit get its own canonical CATz article? Probably yes — it's the named projection layer that downstream consumers depend on, and it deserves a substrate-level definition of its scrub rules and aggregation policies. Candidate: Canary-Retail-Brain/platform/sales-audit-projection-layer.md. Deferred until next substrate sweep.

  2. Sales Audit scrub-rule taxonomy. What's the canonical list of scrub reason codes (cancelled, duplicate, test, pending, adjudication-needed, etc.)? Worth codifying so each downstream consumer knows what to expect.

  3. Aggregation granularity per consumer. QuickBooks wants per-day department totals; Klaviyo wants per-customer event streams; Gusto wants per-employee per-shift labor records. Sales Audit needs a configurable aggregation profile per consumer. Spec deferred.

Sources