DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path foray-reference/foray-protocol-primer-for-loomworks-substrate-v0_1.md

<!-- File: foray-protocol-primer-for-loomworks-substrate-v0_1.md Version: v0.1 Created: 2026-05-24T00:00:00Z Author: Marvin Percival Email: marvinp@dunin7.com GitHub: github.com/DUNIN7/foray-kaspathon

Purpose: Protocol-level primer for a sibling Claude.ai conversation grounding a Loomworks substrate-unification review in FORAY's protocol grammar rather than in Loomworks' current three-table implementation. -->

FORAY Protocol Primer for Loomworks Substrate Unification

Version: v0.1 (working draft) Created: 2026-05-24T00:00:00Z


Document purpose

This is a primer on FORAY at the protocol level, written for a sibling Claude.ai conversation that is producing a Loomworks substrate review. That conversation has a draft recommendation grounded in the three existing Loomworks implementationscredit.foray_action_flows, audit.foray_events, and the readiness-wiring columns on memory_events — but not in FORAY's protocol-level shape.

Loomworks has a rare permission to wipe existing data and rebuild before production accumulates. This primer provides the protocol grammar so the substrate review can re-ground its recommendations against the protocol, not against the artifacts of how the three tables grew incrementally.


A correction to land first: five components, not four

The published FORAY v4.1 specification (the version deployed at foray.dunin7.com today) calls it "the 4-Component Model (4A) + Attestations Extension", with Attestations described as an optional fifth for third-party validation. The protocol grammar as currently shipped matches that framing.

The refined position this primer uses is that FORAY is a five-component model in which Attestation is a native cross-cutting capability, not a downstream extension. Both framings produce the same JSON output; the difference is conceptual:

For a substrate rebuild, this matters at the table level. The four DAG components share a shape. Attestations are structurally different and should live in a sibling structure, not be folded into a single polymorphic table over all five.


1. The five constructs

What they are

They are five kinds of transaction component — units that decompose a business transaction. They are not parts of an actor identity, not four kinds of provenance, and not four parts of a single transaction record. They are five typed nodes that compose into a transaction DAG, with attestation being a structurally distinct fifth that hangs off the DAG rather than sitting in it.

The four peer components (the DAG)

| Component | Code | Question it answers | Contains | |---|---|---|---| | Arrangements | ARR | What did we agree to? | Parties, terms, effective dates, legal references | | Accruals | ACC | How is the amount computed? | Formulas, valuations, computed amounts | | Anticipations | ANT | What do we expect to happen? | Forecasts, conditions, scheduled amounts | | Actions | ACT | What actually happened? | Actual amounts, completion dates, status |

Critical distinctions the spec hammers on (these are easy to conflate and the protocol is precise about the difference):

The cross-cutting fifth: Attestation

| Component | Code | Question it answers | Contains | |---|---|---|---| | Attestations | ATT | Who vouches for this — and what did they vouch for? | Attestor identity, credentials, claims, evidence |

What makes attestation structurally different from the four DAG components:

This is the deepest structural reason attestation is "cross-cutting": no DAG component has an attestation_refs[] field. The reference always goes from the attestation into the thing being attested to, never the reverse.

Composition rules

Per FORAY v4.1:

Vocabulary anchors for Claude.ai

To keep the substrate review using the canonical vocabulary:


2. Attestation at the protocol level

What an attestation attests to

An attestation attests to one or more components, named in subject_refs[]. It can attest to:

What it explicitly does not attest to as a primitive:

What an attestation proves vs. does not prove

| Proves | Does NOT prove | |---|---| | A specific party made a specific claim at a specific time | The claim is true | | The claim has not been altered since anchoring | The attestor is competent | | The attestor's credentials are recorded | Physical reality matches the digital record |

This drives the Type 1 / Type 2 distinction in the FORAY trust model:

Shape of a single attestation


{
  "id": "ATT_INSPECTION_001",
  "foray_core": {
    "attestor": "Quality Inspectors Ltd",
    "attestor_hash": "sha256:...",
    "attestor_type": "inspection_body",
    "attestor_credentials": ["ISO_17020", "UKAS_Accredited"],
    "subject_refs": ["ACT_DELIVERY_001", "ACC_INV_2026_001"],
    "attestation_type": "inspection",
    "attestation_date": "2026-01-15T14:30:00Z",
    "validity_period": { "start": "2026-01-15", "end": "2027-01-15" },
    "outcome": "certified",
    "evidence_hash": "sha256:...",
    "evidence_location": "off-chain",
    "dependencies": ["ATT_LAB_ANALYSIS_001"]
  }
}

Notable shapes:

Attestation types (enum)

Relationship to anchoring

Attestations are components — their foray_core hashes into component_hashes.attestations, which contributes to the transaction's merkle_root. When the transaction is anchored to Kaspa, the attestations are anchored with it. An attestation is not a separate on-chain primitive; it lives inside the same transaction envelope as the four DAG components.

Relationship to the five constructs


3. Canonical FORAY transaction shape

The full transaction record (what gets stored in Layer 2)


{
  "transaction_id": "DOMAIN_YYYY_QN_DESCRIPTOR",
  "schema_version": "4.1",
  "timestamp": "ISO-8601",

  "foray_core": {
    "entity": "entity_name",
    "entity_hash": "sha256:...",
    "transaction_type": "type_code",
    "total_value": 0.00,
    "currency": "USD",
    "status": "active|completed|reversed",
    "compliance_flags": ["SOX_404", "DCAA", "..."]
  },

  "component_hashes": {
    "arrangements":  "sha256:...",
    "accruals":      "sha256:...",
    "anticipations": "sha256:...",
    "actions":       "sha256:...",
    "attestations":  "sha256:..."
  },

  "arrangements":  [...],
  "accruals":      [...],
  "anticipations": [...],
  "actions":       [...],
  "attestations":  [...],

  "merkle_root": "sha256:...",

  "blockchain_anchor": {
    "kaspa_tx_id": "kaspa:qr...",
    "block_height": 0,
    "confirmation_time_ms": 0,
    "anchored_at": "ISO-8601"
  },

  "audit_data_anchor": {
    "audit_data_hash": "sha256:...",
    "audit_profile": "standard|dcaa_full|big4|minimal",
    "storage_locations": [...]
  },

  "privacy_metadata": { /* ... */ }
}

Required top-level fields: transaction_id, schema_version, timestamp, foray_core, component_hashes, merkle_root, blockchain_anchor.

Optional top-level fields: the five component arrays individually (sum ≥ 1), audit_data_anchor, privacy_metadata.

Each component has a dual-section shape


{
  "id": "ARR_DESCRIPTIVE_ID",
  "foray_core": { /* protocol-required core fields */ },
  "audit_data":  { /* optional, audit-profile-dependent */ }
}

foray_core is hashed into component_hashes.<type>. audit_data is hashed separately into audit_data_anchor.audit_data_hash. This separation is deliberate: the core protocol runs without audit_data; audit_data extends the record for SOX, DCAA, Big 4 audit profiles. A system that omits audit_data entirely is still protocol-compliant.

Transaction record vs. transaction event

This is the distinction asked about — and it maps cleanly onto FORAY's three-layer storage architecture:

| Concept | Layer | What it is | Size | |---|---|---|---| | Transaction record | Layer 2 (NoSQL) | The full transaction with all components and audit_data | ~15–20 KB | | On-chain envelope | Layer 1 (Kaspa) | Hashes + merkle_root + minimal core metadata | ~200–500 bytes | | Sealed archive | Layer 3 (Arweave + Glacier) | Encrypted complete record for litigation/dispute | ~5 KB compressed |

What gets anchored on-chain is not the transaction record. It is the envelope — a minimal payload containing transaction_id, schema_version, timestamp, foray_core_hash, audit_data_hash, merkle_root, and the anchor metadata. The full components live off-chain in Layer 2; the chain holds the cryptographic commitment.

The foundational architectural move:

> The blockchain is not a database. It is a cryptographic commitment store that enables off-chain logic to operate with on-chain guarantees.

Two distinct types of events get anchored on Kaspa:

  1. The transaction anchor eventtransaction_idmerkle_rootkaspa_tx_id. Synchronous, ~1.2 seconds of user-perceived latency. One per transaction.
  2. The sealing pointer event — when the sealed archive finishes (asynchronous, ~5–60 seconds after the synchronous anchor), a separate Kaspa transaction anchors the sealing record pointing to the Arweave + Glacier locations.

Three-layer storage model summary


Layer 1 (Kaspa,         ~500 B):    hashes + merkle_root + anchor metadata
Layer 2 (NoSQL,      ~15–20 KB):    full foray_core + full audit_data, queryable
Layer 3 (Arweave+Glacier):          sealed, encrypted, multi-party-ceremony access

For Loomworks, the relevant scope is Layer 2. Layer 1 is the FORAY anchor service's responsibility (Loomworks hands off the envelope to be anchored). Layer 3 is out of scope unless Loomworks needs litigation-grade preservation.


4. Implementation guidance for the unified Loomworks substrate

The three current tables mapped to protocol concepts

| Current Loomworks table | What it is now | Closest protocol concept | |---|---|---| | credit.foray_action_flows | Value flows | Actions (ACT) — possibly with adjacent Accruals (ACC) | | audit.foray_events | Narrative state changes | Mixed: partly Attestations, partly state transitions on ARR/ANT/ACT, partly events about state | | memory_events (with readiness-wiring columns) | Canonical append-only event log + DAG-readiness gating | The event log proper + the DAG dependency structure (currently muddled) |

The root problem the substrate review is responding to: three tables holding what is conceptually one protocol substrate, mixed with a second dimension (the append-only event log). The "three into one" instinct is right that there's overcollapse, but the protocol-respecting target is actually two state structures plus a clean event log, not a single mega-table.

Why not one polymorphic table over all five

The instinct to put all five constructs in one table seems clean. The protocol grammar pushes back for three reasons:

  1. Reference shapes differ. The four DAG components have typed forward references (arrangement_refs[], accrual_refs[], anticipation_refs[]). Attestations have a single polymorphic reference (subject_refs[]) that can point anywhere including other attestations. Forcing both into one column shape loses type information that the protocol uses for validation.
  2. Validation rules differ. The DAG must be acyclic among the four peer components, with type rules about which kinds can reference which. Attestations have their own rule set (ATT-001 through ATT-006), including a separate no-circular-references rule.
  3. Readiness semantics differ. A DAG component's "readiness" means all its upstream refs are settled. An attestation's "validity" is bounded by validity_period and outcome, which is a different lifecycle.

The protocol-respecting unified shape

Four tables, cleanly separated by what each represents:

Table 1: foray_transactions — the transaction envelope

One row per transaction. Populated at creation; finalized when ready to anchor.


foray_transactions
├─ transaction_id        TEXT PRIMARY KEY
├─ schema_version        TEXT NOT NULL          -- '4.1'
├─ entity_hash           TEXT NOT NULL
├─ transaction_type      TEXT NOT NULL
├─ status                TEXT NOT NULL          -- 'open' | 'ready_to_anchor' | 'anchored' | 'sealed'
├─ total_value           NUMERIC
├─ currency              TEXT
├─ compliance_flags      TEXT[]
├─ component_hashes      JSONB                  -- populated at anchor time
├─ merkle_root           TEXT                   -- populated at anchor time
├─ blockchain_anchor     JSONB                  -- populated by anchor service
├─ audit_data_anchor     JSONB
├─ privacy_metadata      JSONB
├─ created_at            TIMESTAMPTZ NOT NULL
├─ anchored_at           TIMESTAMPTZ

Table 2: foray_components — the four DAG component types (polymorphic by type)

One row per component. This is the unified replacement for credit.foray_action_flows plus the state-bearing parts of audit.foray_events.


foray_components
├─ component_id          TEXT PRIMARY KEY       -- e.g. 'ARR_...', 'ACC_...', 'ANT_...', 'ACT_...'
├─ transaction_id        TEXT NOT NULL          -- FK to foray_transactions
├─ component_type        TEXT NOT NULL          -- 'ARR' | 'ACC' | 'ANT' | 'ACT'
├─ status                TEXT NOT NULL          -- 'pending' | 'ready' | 'settled' | 'reversed'
├─ foray_core            JSONB NOT NULL         -- protocol-required core fields
├─ audit_data            JSONB                  -- optional, audit profile dependent
├─ arrangement_refs      TEXT[]                 -- populated by ACC, ANT, ACT
├─ accrual_refs          TEXT[]                 -- populated by ANT, ACT
├─ anticipation_refs     TEXT[]                 -- populated by ACT
├─ entity_hash           TEXT NOT NULL
├─ created_at            TIMESTAMPTZ NOT NULL
├─ component_hash        TEXT                   -- sha256(canonicalize(foray_core)), populated when ready

Type-validity rules to enforce at write time:

| component_type | arrangement_refs | accrual_refs | anticipation_refs | |---|---|---|---| | ARR | (empty) | (empty) | (empty) | | ACC | allowed | (empty) | (empty) | | ANT | allowed | allowed | (empty) | | ACT | allowed | allowed | allowed |

Readiness is derived from this structure, not a column: a component's status becomes ready when every component named in any of its *_refs[] columns has a status of ready or settled. The current readiness-wiring on memory_events is doing this work; in the unified substrate it becomes a view or a computed predicate over foray_components.

Table 3: foray_attestations — the cross-cutting fifth

Sibling table to foray_components, structurally distinct.


foray_attestations
├─ attestation_id        TEXT PRIMARY KEY       -- 'ATT_...'
├─ transaction_id        TEXT NOT NULL          -- FK to foray_transactions
├─ attestor              TEXT NOT NULL
├─ attestor_hash         TEXT NOT NULL
├─ attestor_type         TEXT NOT NULL
├─ attestor_credentials  TEXT[]
├─ subject_refs          TEXT[] NOT NULL        -- can reference foray_components OR foray_attestations
├─ attestation_type      TEXT NOT NULL          -- certification | inspection | analysis | audit_opinion | verification | oracle
├─ attestation_date      TIMESTAMPTZ NOT NULL
├─ validity_period_start DATE
├─ validity_period_end   DATE
├─ outcome               TEXT NOT NULL          -- certified | denied | conditional | expired | pending
├─ evidence_hash         TEXT
├─ evidence_location     TEXT
├─ dependencies          TEXT[]                 -- other ATT this depends on (chain)
├─ created_at            TIMESTAMPTZ NOT NULL
├─ attestation_hash      TEXT                   -- sha256(canonicalize(...)), populated when ready

Notes:

Table 4: memory_events (cleaned) — the append-only event log

Keep memory_events, but strip the readiness-wiring columns. Readiness is now derived state over foray_components, not a column on the event log. memory_events returns to being a pure stream of state changes.


memory_events
├─ event_id              ...
├─ event_type            TEXT NOT NULL          -- 'COMPONENT_CREATED', 'COMPONENT_STATE_CHANGED',
│                                               -- 'ATTESTATION_ATTACHED', 'TRANSACTION_ANCHORED', ...
├─ subject_id            TEXT                   -- component_id, attestation_id, or transaction_id
├─ subject_kind          TEXT                   -- 'COMPONENT' | 'ATTESTATION' | 'TRANSACTION'
├─ payload               JSONB
├─ occurred_at           TIMESTAMPTZ NOT NULL

The conceptual shift: memory_events is the stream of state changes. foray_transactions + foray_components + foray_attestations is the current state. Today those two dimensions are interleaved across your three tables. Pulling them apart is the unification.

How the three current tables collapse


credit.foray_action_flows  →  foray_components (component_type IN ('ACT', possibly 'ACC'))
audit.foray_events         →  three destinations depending on the row:
                                – foray_attestations (if it's an attestation)
                                – memory_events       (if it's a narrative event about state)
                                – foray_components    (if it's actually the state, not an event about it)
memory_events readiness    →  removed; derived from foray_components.*_refs[] and status
columns

The transaction-record-vs-event question, made concrete

In the unified substrate:

This three-way distinction (current state / event log / on-chain envelope) is exactly what the current Loomworks substrate has muddled by putting readiness wiring on memory_events. Readiness is current-state machinery; events are an append-only log. Mixing them prevents either from being clean.

Recommendations for the substrate review

  1. Adopt four tables, not one polymorphic table. foray_transactions (envelope) + foray_components (four DAG types, polymorphic) + foray_attestations (cross-cutting fifth) + memory_events (cleaned event log). One mega-table over all five constructs over-collapses because attestation's reference shape is fundamentally different.
  1. Strip the readiness-wiring columns from memory_events. Readiness becomes derived state over foray_components.*_refs[] and status. The event log returns to being a pure append-only stream.
  1. Use plural _refs[] arrays everywhere. Non-negotiable at the protocol level — v4.0 → v4.1 breaking change.
  1. Allow any component type as a transaction entry point. Do not require Arrangements as DAG roots. Action-only (cash sales) and Accrual-only (depreciation, period-end entries) transactions are valid per v4.1.
  1. Don't store merkle_root, component_hashes, or blockchain_anchor as columns on foray_components. Those are properties of a transaction (the grouping), computed when components are ready-to-anchor. They belong on foray_transactions, populated at anchor time.
  1. Preserve the dual foray_core / audit_data separation inside JSONB. Even if Loomworks doesn't need audit_data for v0, the protocol's hash-everything-separately model assumes it. Leaving audit_data as a nullable JSONB column means later audit-profile work doesn't require schema migration.
  1. Acknowledge what's deliberately out of scope. Layer 3 (sealed archive) is not a Loomworks concern unless litigation-grade preservation is on the roadmap. Layer 1 (on-chain anchoring) belongs to the FORAY anchor service. Loomworks is doing Layer 2 — the queryable operational store.

Path considered and set aside

A path that looked tempting during the analysis: collapse all five constructs into one polymorphic foray_substrate table with a kind discriminator and a single refs JSONB blob. It would unify the SQL surface area to one table.

Rejected because: the protocol distinguishes typed DAG references (which carry validation semantics — an Action can reference Anticipations, an Anticipation cannot reference Actions) from polymorphic subject references (which are intentionally unconstrained — an Attestation can point at anything). Collapsing these into one blob column hides the protocol's type information from the database layer and pushes all type-checking into application code, where it's easier for the next person to forget. The two-state-table shape preserves the protocol's typing at the schema level.


5. References to spec sections

For verification of any of the above against the canonical FORAY documentation in the project knowledge:


Contact