DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path foray-reference/foray-adapter-deep-read-v0_1.md

FORAY Adapter Deep-Read v0.1

Produced 2026-05-24 by CC.

This document grounds Claude.ai's understanding of FORAY's adapter pattern across all available material on the local filesystem and the live foray.dunin7.com site. Companion to:

Scope: comprehensive. All adapter material read, all detail preserved. The Operator's standing instruction — "look at everything, we cannot afford to miss anything during an investigation" — has been honored.

Up-front orientation: the word "adapter" carries two distinct meanings in FORAY material. Both meanings are present in the canonical material and conflating them produces misleading conclusions about how Loomworks would integrate FORAY. The two meanings:

  1. Source-side adapter — translates a calling system's native events into FORAY 4A JSON. The kaspathon-repo quickbooks-adapter.js and salesforce-adapter.js are examples. These adapters consume ERP/CRM data and produce structured FORAY transactions.
  1. Persistence-side adapter — takes a constructed-and-hashed FORAY event and writes it to a storage destination (PostgreSQL, blockchain, NoSQL, S3). The dunin7-foray Node package (v3.0.0) is the reference PostgreSQL persistence adapter. The "Pluggable Persistence Architecture" doc defines this layer formally.

The protocol deep-read found foray-sdk.js referenced but absent. The reason for that absence is now clear: the SDK shape implied by the kaspathon adapters (createArrangement / createAccrual / ... / anchorToBlockchain) is not the SDK that DUNIN7 actually ships. The shipped SDK is dunin7-foray v3.0.0, which has a fundamentally different shape — emit(chainId, eventType, triggeredBy, payload) against a PostgreSQL chain rather than createArrangement(...) against blockchain anchoring. The kaspathon adapters were written against a hypothetical SDK that never materialised in the shape they assumed; the actual shipped SDK is what dunin7-workforce uses today.

This bifurcation is the load-bearing finding of this deep-read.


Source material read

Source code (all read end-to-end)

| Path | Lines | Size | Role | |---|---|---|---| | /Users/dunin7/foray-kaspathon/quickbooks-adapter.js | 627 | 20 KB | Source-side adapter (QuickBooks Online) — demo | | /Users/dunin7/foray-kaspathon/salesforce-adapter.js | 801 | 26 KB | Source-side adapter (Salesforce) — demo | | /Users/dunin7/foray-kaspathon/foray-api-server.js | ~1,000 | 41 KB | LLM-backed natural-language → 4A generator (not an adapter; reviewed for completeness) | | /Users/dunin7/foray-kaspathon/proxy-server.js | 100 | 3 KB | CORS proxy for the generator endpoints (not an adapter) | | /Users/dunin7/dunin7-foray/lib/index.mjs | 7 | — | Public API barrel for the live SDK | | /Users/dunin7/dunin7-foray/lib/foray-client.mjs | 374 | — | The live FORAY SDKForayClient class, PostgreSQL persistence + hash chain + attestation queries | | /Users/dunin7/dunin7-foray/lib/attestation.mjs | 29 | — | FORAY v4.1 attestation/attestor/outcome constants | | /Users/dunin7/dunin7-foray/lib/evidence.mjs | 46 | — | Hash + subject-ref + evidence-ref helpers | | /Users/dunin7/dunin7-foray/lib/canonical-json.mjs | 14 | — | Deterministic JSON key-ordering for stable hashing | | /Users/dunin7/dunin7-foray/test/smoke.mjs | 97 | — | SDK smoke test exercising the full Client lifecycle | | /Users/dunin7/dunin7-foray/package.json | — | — | Confirms name: dunin7-foray, version: 3.0.0 | | /Users/dunin7/dunin7-foray-validate/src/index.ts | 8 | — | TypeScript predecessor SDK — public API barrel | | /Users/dunin7/dunin7-foray-validate/src/emitter/emitter.ts | 92 | — | FORAYEmitter class (TypeScript) | | /Users/dunin7/dunin7-foray-validate/src/stores/pg-store.ts | 139 | — | FORAYStore class (TypeScript) | | /Users/dunin7/dunin7-foray-validate/src/verifier/chain.ts | 109 | — | computeHash, verifyEventHash, verifyChain | | /Users/dunin7/dunin7-foray-validate/src/events/types.ts | 86 | — | FORAYEventType discriminated union (50+ event types) | | /Users/dunin7/dunin7-foray-validate/src/validate.ts | 200 | — | G0.5 gate validation script — 20 synthetic events + integrity tests | | /Users/dunin7/mode-b-build/dunin7-foray/lib/foray-client.mjs | — | — | Earlier v2.x of the same SDK (no attestation, no canonical-json, no mutex). Diffed against current; differences are docstring + attestation-support adds | | /Users/dunin7/dunin7-workforce/tests/foray-client.test.mjs | — | — | Workforce-side test exercising ForayClient.emit + verifyChain against canonical-JSON nested payloads | | /Users/dunin7/dunin7-workforce/FORAY_SYSTEM_KEY_ARCHITECTURE.md | ~110 | — | How dunin7-workforce wires the live SDK (foray_system_key per project, registered ForayClient per system) |

Documentation (all read substantively)

| Path | Source | Lines | Role | |---|---|---|---| | /Users/dunin7/foray-kaspathon/integration-guide.html | foray.dunin7.com (local) | ~700 | Adapter Architecture section, per-ERP integration patterns, batch-vs-realtime | | /Users/dunin7/foray-kaspathon/docs.html | foray.dunin7.com (local) | ~1,250 | "FORAY adapters simply translate what exists", AI Agent Integration section (4A → agent decision cycle) | | /Users/dunin7/foray-kaspathon/specification.html | foray.dunin7.com (local) | — | Schema definitions (cross-reference; covered in protocol deep-read) | | /Users/dunin7/foray-kaspathon/about.html | foray.dunin7.com (local) | — | Project framing | | /Users/dunin7/foray-kaspathon/index.html | foray.dunin7.com (local) | — | Marketing landing — cross-checked | | /Users/dunin7/foray-kaspathon/demo.html | foray.dunin7.com (local) | — | Demo embed (not adapter material) | | /Users/dunin7/dunin7-docs/foray/FORAY_PLUGGABLE_PERSISTENCE_V1_0_0.md | DUNIN7-internal | 177 | The persistence-adapter pluggable-pipeline spec. Defines ForayPersistenceAdapter interface and admin-driven fan-out. Concept doc, dated 2026-02-19 | | /Users/dunin7/Desktop/foray-updates/FORAY_AI_Generator_Setup.md | DUNIN7-internal | — | Setup guide for the foray-api-server.js LLM endpoint (not adapter material per se) |

Live site cross-check

https://foray.dunin7.com/ returned HTTP 200 (48 KB). The local clone at /Users/dunin7/foray-kaspathon/ is identical content per spot-check.

Repos pulled

Repos surveyed but found to contain no fresh adapter material


Section 1 — The adapter pattern at the protocol level

What an adapter is — per foray.dunin7.com

From integration-guide.html (the canonical adapter framing):

> "Every FORAY adapter follows the same three-phase architecture, regardless of source system: > > Phase 1: Extract > - Connect to source system API (REST, SOAP, BAPI, etc.) > - Pull transaction data with all relevant fields > - Handle pagination for batch operations > > Phase 2: Transform > - Map source fields to FORAY 4A components > - Apply privacy obfuscation (hash parties, round amounts) > - Generate component hashes (SHA-256) > - Compute merkle root > > Phase 3: Anchor > - Submit merkle root to Kaspa blockchain > - Receive transaction ID and block confirmation > - Store full FORAY JSON with anchor reference > - Return confirmation to source system (optional)"

The accompanying ASCII diagram:


Source System (SAP, Oracle, QuickBooks, Salesforce, ...)
         |
         v
    FORAY Adapter (extracts + transforms)
         |
         v
    FORAY JSON (4A components + hashes)
         |
         v
    Kaspa Blockchain (merkle root anchored)
         |
         v
    Archive (full JSON + anchor reference)

This is the source-side adapter meaning. The integration guide treats this as the universal pattern.

Why adapters exist as a pattern

From docs.html (the "Integration Overview" section):

> "Zero Additional Data Entry — If your ERP system already processes your business transactions, you already have everything FORAY needs."

> "FORAY does not require new data — it works with what you already capture. When you record a sale, process payroll, or log a purchase order, your ERP system already stores the parties, amounts, dates, and terms that define that transaction."

> "FORAY adapters simply translate what exists" — verbatim section header.

The mapping table reproduced in docs.html:

| Your ERP Already Has | FORAY Organizes It As | |---|---| | Customer/Vendor records | Arrangements (who agreed to what) | | Calculated amounts, taxes, allocations | Accruals (how values were derived) | | Scheduled payments, deliveries | Anticipations (what is expected) | | Completed payments, shipments | Actions (what actually happened) |

And the load-bearing constraint:

> "FORAY Cannot Create Additional Data — This is by design. FORAY Protocol is a read-only audit layer — it can only organize, structure, and anchor data that already exists in your systems." > > "- No fabrication: FORAY cannot invent transactions, amounts, or parties > - No modification: FORAY cannot alter your source records > - No insertion: FORAY cannot add data back into your ERP"

> "This separation ensures audit integrity. The blockchain-anchored FORAY record is a reflection of your source system at a point in time — nothing more, nothing less."

The adapter pattern exists because FORAY is a reflection layer, not a system of record. The adapter's job is to construct a tamper-evident reflection of the source system's existing events without ever writing back to the source.

The abstract contract every adapter shares

Reading across kaspathon's two source adapters and the persistence-side ForayPersistenceAdapter interface from the Pluggable Persistence doc, the recurring contract is:

What varies per source system

The relationship between adapter, SDK, and anchor service

Per the kaspathon adapter source (the hypothetical model):


Source System  →  Adapter  →  SDK  →  Anchor Service
                              (constructs)  (persists)

Per the actual deployed model (dunin7-foray v3.0.0):


Source System  →  Adapter (does not exist for live customers)
                  Application code calling SDK directly
                               ↓
                          ForayClient
                               ↓
                  PostgreSQL `foray_events` table
                  (hash-chained, append-only)

In the live model, there is no separate "anchor service." The SDK is the persistence boundary. The PostgreSQL database is the persistence layer. Blockchain anchoring is anticipated by the Pluggable Persistence doc but not implemented in dunin7-foray v3.0.0.

Stateless translators vs stateful intermediaries

The kaspathon adapters are stateful in two narrow senses:

They are stateless in the broader sense:

The live ForayClient is stateful in a different way:

Where adapters run

Per integration-guide.html (the canonical answer): adapters are described as running adjacent to the source system, not in-process. The diagram shows the adapter as a distinct hop between source and FORAY. The implementation language is unspecified (the kaspathon adapters happen to be Node.js).

Per the actual dunin7-foray SDK usage in dunin7-workforce: the SDK runs in-process with the application. Workforce's API routes import ForayClient and call .emit() directly during request handling. There is no out-of-process adapter; the application is the adapter.

These are different patterns. The kaspathon adapters model a deployment where FORAY integration is a separable process; the dunin7-foray SDK models a deployment where FORAY is a library called inline by the application.


Section 2 — Per-adapter detail

2.1 — quickbooks-adapter.js (kaspathon, demo-grade)

Path: /Users/dunin7/foray-kaspathon/quickbooks-adapter.js Lines: 627 Header version: v2.0.0 (Corrected) License: BSL-1.1 Disclaimer (verbatim from source):

> "DISCLAIMER: This code is provided for demonstration purposes only. Production implementations require additional security hardening, error handling, testing, and validation. Consult qualified professionals before deploying in production environments."

Source system: QuickBooks Online (QBO).

Source-system data shape consumed: QBO REST API objects via a qbo client (passed in via config). Six transaction types handled:

FORAY component construction logic (Invoice example, lines 215-319):


// Step 1: Arrangement (invoice terms, parties)
const arrangement = this.sdk.createArrangement({
  source_system: 'quickbooks',
  transaction_type: 'invoice',
  reference_id: this._hashIdentifier(invoice.Id, 'invoice'),
  parties: [
    { role: 'seller', identifier: this._hashIdentifier(invoice.CompanyInfo?.CompanyName, 'company'), ... },
    { role: 'buyer', identifier: this._hashIdentifier(invoice.CustomerRef?.name, 'customer'), ... }
  ],
  terms: {
    payment_terms: invoice.SalesTermRef?.name || 'Net 30',
    due_date: invoice.DueDate,
    currency: invoice.CurrencyRef?.value || 'USD'
  }
});

// Step 2: Accrual (revenue recognition)
const accrual = this.sdk.createAccrual({
  arrangement_ref: arrangement.id,
  amount: this._obfuscateAmount(invoice.TotalAmt),
  currency: invoice.CurrencyRef?.value || 'USD',
  formula_id: 'invoice_total',
  formula_description: 'Sum of line items + tax',
  formula_inputs: {
    line_item_count: invoice.Line?.length || 0,
    tax_amount: this._obfuscateAmount(invoice.TxnTaxDetail?.TotalTax || 0),
    subtotal: this._obfuscateAmount(invoice.TotalAmt - (invoice.TxnTaxDetail?.TotalTax || 0))
  },
  debit_account: 'Accounts Receivable',
  credit_account: 'Revenue',
  fiscal_period: this._getFiscalPeriod(invoice.TxnDate)
});

// Step 3: Anticipation (expected payment)
const anticipation = this.sdk.createAnticipation({
  arrangement_ref: arrangement.id,
  expected_date: invoice.DueDate,
  expected_amount: this._obfuscateAmount(invoice.TotalAmt),
  currency: invoice.CurrencyRef?.value || 'USD',
  settlement_account: 'Cash',
  conditions: { payment_method: 'Any accepted method', partial_payment_allowed: true }
});

// Step 4: Action (only if paid)
let action = null;
if (invoice.Balance === 0 && invoice.TotalAmt > 0) {
  action = this.sdk.createAction({
    arrangement_ref: arrangement.id,
    anticipation_ref: anticipation.id,
    settlement_type: 'full_payment',
    actual_amount: this._obfuscateAmount(invoice.TotalAmt),
    settlement_date: invoice.TxnDate,
    payment_method: 'Applied'
  });
}

// Anchor to blockchain
const transaction = this.sdk.createTransaction({
  components: { arrangement, accrual, anticipation, action }
});
const anchorResult = await this.sdk.anchorToBlockchain(transaction);

Mapping decisions visible in the code:

State management:

Integration mechanics:

Error handling:

Performance characteristics:

Code structure:

Observation: the file header lists "PurchaseOrder, SalesReceipt" as supported, but anchorPurchaseOrder and anchorSalesReceipt methods do not exist in the source. The header is aspirational beyond what the code implements.

2.2 — salesforce-adapter.js (kaspathon, demo-grade)

Path: /Users/dunin7/foray-kaspathon/salesforce-adapter.js Lines: 801 Header: "FORAY Salesforce Adapter (CORRECTED v2.0)" License: BSL-1.1 (implied; same as quickbooks) Same disclaimer block as quickbooks-adapter.

Source system: Salesforce (any edition with the standard objects).

Source-system data shape consumed: Salesforce REST API or jsforce-style object representations. Six object types handled (no Contract handler implemented despite being in the type map):

FORAY component construction logic (Opportunity example, lines 226-333):


const arrangement = this.foray.createArrangement({
  parties: [accountId, ownerId].filter(Boolean),
  asset_type: this.objectTypeMap['Opportunity'],   // 'sales-opportunity'
  terms: {
    stage: this.hashValue(opportunity.StageName),
    type: this.hashValue(opportunity.Type),
    lead_source: this.hashValue(opportunity.LeadSource),
    created_date: opportunity.CreatedDate
  },
  effective_date: opportunity.CreatedDate,
  metadata: { salesforce_id: opportunityId, object_type: 'Opportunity' }
});

const accrual = this.foray.createAccrual({
  arrangement_hash: arrangement.hash,    // NOTE: uses .hash not .id
  formula_id: this.foray.getFormulaId('weighted_revenue'),
  inputs: {
    amount: this.obfuscateAmount(opportunity.Amount || 0),
    probability: opportunity.Probability || 0,
    currency: this.hashValue(opportunity.CurrencyIsoCode || 'USD')
  },
  outputs: {
    expected_revenue: this.obfuscateAmount(amount * (probability / 100)),
    risk_adjusted_value: this.obfuscateAmount(amount * (probability / 100) * 0.85)
  },
  calculation_date: new Date().toISOString(),
  metadata: { stage: ..., forecast_category: ... }
});

const anticipation = this.foray.createAnticipation({
  accrual_hash: accrual.hash,
  expected_date: opportunity.CloseDate,
  expected_amount: this.obfuscateAmount(amount),
  conditions: [{
    type: 'stage_progression',
    value: this.hashValue('Closed Won'),
    probability: probability / 100
  }],
  metadata: { next_step: ..., days_to_close: ... }
});

let action = null;
if (opportunity.StageName === 'Closed Won' && opportunity.IsClosed) {
  action = this.foray.createAction({
    anticipation_hash: anticipation.hash,
    actual_date: opportunity.CloseDate,
    actual_amount: this.obfuscateAmount(amount),
    settlement_type: 'opportunity_won',
    proof: { stage: ..., is_won: ..., closed_date: ... }
  });
}

Mapping decisions visible in the code:

State management:

Integration mechanics:

Error handling:

Performance characteristics:

Code structure:

Observation: the objectTypeMap lists Contract and Contact types but no handlers exist. Like the QuickBooks file, the header overstates implementation coverage.

2.3 — ForayClient (the live SDK / "persistence adapter to PostgreSQL")

Path: /Users/dunin7/dunin7-foray/lib/foray-client.mjs Lines: 374 Package: dunin7-foray v3.0.0 Header (verbatim):

> "DUNIN7 FORAY Client — Append-only, hash-chained event emitter with FORAY Protocol v4.1 attestation support. > > Writes tamper-evident events to PostgreSQL. Each event's hash includes prev_hash, creating an unbroken chain. Separate database per product (foray_fieldpilot, foray_workforce, foray_forge). > > Uses per-chain mutex to serialize concurrent writes (parallel agents). > > @version 3.0.0"

Source system: None — ForayClient is the persistence adapter. The application IS the source-side adapter when using this SDK.

Data shape persisted: The foray_events PostgreSQL table:


CREATE TABLE IF NOT EXISTS foray_events (
  id            SERIAL PRIMARY KEY,
  frame_id      TEXT        NOT NULL,
  sequence      INTEGER     NOT NULL,
  event_type    TEXT        NOT NULL,
  triggered_by  TEXT        NOT NULL,
  payload       JSONB       NOT NULL DEFAULT '{}',
  prev_hash     TEXT        NOT NULL,
  hash          TEXT        NOT NULL,
  hash_timestamp TEXT       NULL,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (frame_id, sequence)
);

CREATE INDEX IF NOT EXISTS idx_foray_frame   ON foray_events (frame_id);
CREATE INDEX IF NOT EXISTS idx_foray_type    ON foray_events (event_type);
CREATE INDEX IF NOT EXISTS idx_foray_agent   ON foray_events (triggered_by);
CREATE INDEX IF NOT EXISTS idx_foray_payload ON foray_events USING GIN (payload);

The shape is not the FORAY 4A protocol shape. It is frame_id + sequence + event_type + triggered_by + payload. The 4A model is layered on top via the payload JSONB, not as separate columns. An Arrangement-event would be event_type: 'arrangement.created' with the arrangement's full body in payload. The library has no Arrangement/Accrual/Anticipation/Action types as first-class concepts — those live in the application's choice of event-type strings.

FORAY v4.1 Attestation support is first-class: there is a dedicated attest(chainId, attestation) method (line 155) and four read methods specifically for attestations (getAttestations, getAttestationsFor, getAttestationsByType, getAttestationsByAttestor). The attestation event lives in the same foray_events table with event_type = 'attestation'.

Hash construction (the protocol-level integrity mechanism, lines 47-59):


function computeHash(event) {
  const hashInput = {
    frame_id:     event.frame_id,
    sequence:     event.sequence,
    event_type:   event.event_type,
    triggered_by: event.triggered_by,
    payload:      event.payload,
    prev_hash:    event.prev_hash,
    timestamp:    event.timestamp,
  };
  const content = canonicalStringify(hashInput);
  return crypto.createHash('sha256').update(content).digest('hex');
}

The first event in a chain uses prev_hash = '0'.repeat(64) (line 174).

Per-chain mutex (lines 65-81): the ChainMutex class serialises writes per chainId. Concurrent callers queue behind a Promise chain. This is the integrity-preserving mechanism for parallel agents writing to the same chain.

Methods catalog (the actual deployed SDK API):

| Method | Signature | Purpose | |---|---|---| | constructor(opts) | opts = {product, connectionString?, host?, port?, user?, password?} | Construct client. product is required and determines the database name (foray_${product}). | | connect() | () → Promise<this> | Ensure the foray_events table exists. Call once at startup. | | disconnect() | () → Promise<void> | Close the connection pool. | | emit(chainId, eventType, triggeredBy, payload) | → Promise<event> | Append one event to the chain. Acquires mutex, computes hash, commits. | | attest(chainId, attestation) | → Promise<event> | Convenience wrapper around emit() with event_type='attestation' and triggeredBy=attestation.attestor. | | getChain(chainId) | → Promise<event[]> | Return all events for a chain, ordered by sequence. | | getByType(chainId, eventType) | → Promise<event[]> | Filter by event type. | | getLatest(chainId) | → Promise<event \| null> | Most recent event in a chain. | | count(chainId) | → Promise<number> | Event count for a chain. | | getAttestations(chainId, {type, attestor, outcome, limit, offset}) | → Promise<event[]> | Filtered attestation lookup. | | getAttestationsFor(chainId, subjectType, subjectId) | → Promise<event[]> | Attestations whose subject_refs[] JSONB array contains {type: subjectType, id: subjectId}. Uses Postgres @> containment. | | getAttestationsByType(chainId, type) | → Promise<event[]> | Convenience for getAttestations({type}). | | getAttestationsByAttestor(chainId, attestor) | → Promise<event[]> | Convenience for getAttestations({attestor}). | | verifyChain(chainId) | → Promise<{valid: true, events: N} \| {valid: false, broken_at, reason}> | Walk the chain, recompute each hash, check sequence continuity and prev_hash linkage. | | exportChain(chainId) | → Promise<string> | Full chain as pretty-printed JSON. |

State management:

Integration mechanics:

Error handling:

Performance characteristics:

Code structure:

2.4 — dunin7-foray-validate (TypeScript predecessor)

Path: /Users/dunin7/dunin7-foray-validate/ Package name: dunin7-foray v0.1.0 ("private": true) Status: This appears to be an earlier TypeScript implementation of the same conceptual SDK, which has since been superseded by the JavaScript v3.0.0 package at /Users/dunin7/dunin7-foray/.

Architectural design is the same: append-only, hash-chained, per-frame event log in PostgreSQL. Two-class split rather than one-class:

Differences from v3.0.0 ForayClient:

The typed event-type list is interesting source material. It enumerates a substrate-specific (FieldPilot/PartsPilot) frame lifecycle. Reproduced for reference:


v1.0 — Core lifecycle:
  frame.open · agent.invoked · architect.spec_complete · narrator.brief_vN
  infra.setup_complete · design.assets_complete · build.component_complete
  build.complete · model.switched · strategist.checkpoint · blocker.raised
  blocker.resolved · gate.pass · gate.fail · frame.delivered

v1.2 — Governance:
  human.spec_approved · human.spec_rejected · human.pause_issued
  human.release_issued · human.status_requested · status.document_generated
  handoff.artefacts_written

v1.4 — Process discovery:
  narrator.canonical_digest_vN · build.dependency_graph_validated
  security.component_finding · clarification.requested · clarification.resolved
  model.tier_mismatch · frame.recovery_initiated · frame.recovery_approved
  frame.recovery_completed · notification.sms_sent · notification.sms_queued
  notification.queue_delivered · notification.config_changed

This is the dunin7-conductor / dunin7-workforce frame lifecycle. The typed events were the design starting point; the v3.0.0 SDK then generalised by accepting any event-type string. The application defines the vocabulary; the SDK enforces append-only + hash chain regardless of what the events mean.

2.5 — The Pluggable Persistence concept (no code; design doc only)

Path: /Users/dunin7/dunin7-docs/foray/FORAY_PLUGGABLE_PERSISTENCE_V1_0_0.md Status: Concept document, dated 2026-02-19. No code implementation found.

This document defines a persistence-side adapter interface explicitly:


interface ForayPersistenceAdapter {
  name: string;
  persist(entry: ForayHashEntry): Promise<PersistResult>;
  verifyChain(startId?: string, endId?: string): Promise<VerifyResult>;
  retrieve(filter: ForayFilter): Promise<ForayHashEntry[]>;
  isHealthy(): Promise<boolean>;
}

type ForayHashEntry = {
  id: string;
  forayJson: Record<string, unknown>;  // The complete 4A + Attestation
  hash: string;                         // SHA-256 of forayJson
  previousHash: string | null;          // Hash chain link
  sequence: number;                     // Monotonic sequence number
  createdAt: string;                    // ISO timestamp
};

Four destinations documented:

| Destination | Trust Level | Familiarity | Use Case | |---|---|---|---| | PostgreSQL (append-only + hash chain) | High | Very High | Default for enterprise/ERP | | NoSQL (MongoDB, DynamoDB) | High | High | Cloud-native deployments | | FORAY on Blockchain | Highest | Low | Regulated industries, max trust | | S3/Blob Storage (signed JSON files) | Medium | High | Archival, compliance exports |

Adoption phasing (per the doc):

Current state: Phase 1 only. The ForayClient in dunin7-foray v3.0.0 is the Phase 1 PostgreSQL adapter, but it does NOT implement the ForayPersistenceAdapter interface from the design doc (persist / verifyChain / retrieve / isHealthy). The actual API is emit / attest / various getters — closer to a producer SDK than the doc's pure-persistence interface.

This is a real disconnect between concept-doc intent and shipping implementation. The Phase 2 and Phase 3 adapters do not exist.

2.6 — OpenClaw Trading Audit Skill (referenced but not located)

docs.html mentions a reference AI-agent adapter:

> "As a working reference implementation, FORAY provides an Audit Skill for OpenClaw, an open-source AI agent platform that can execute autonomous tasks including crypto trading on platforms like Hyperliquid and Polymarket."

Status: Source not found in /Users/dunin7/. Searched for openclaw, audit-skill paths; nothing matches. The text describes:

If this skill exists, it is hosted in the OpenClaw repository (external to DUNIN7) rather than locally checked out. Treated as a documented-but-unread adapter for completeness.

2.7 — dunin7-workforce as a live application using the SDK

Not an adapter in the strict sense, but the canonical live consumer of ForayClient. Worth surfacing because it shows the SDK in production use.

Per FORAY_SYSTEM_KEY_ARCHITECTURE.md:

> "FORAY is the blockchain-style audit trail system for DUNIN7. It records every significant transaction and event in a tamper-evident append-only ledger. Each system (FieldPilot, PartsPilot, etc.) has its own dedicated FORAY database."

Mapping from dunin7-workforce concepts to ForayClient invocations:


Operator submits frame with project_id
  → API fetches projects.foray_system_key
  → If set: foray.for(foray_system_key).emit(frame_id, 'frame.open', triggered_by, payload)
  → If NULL: no FORAY event written

The note "Never derive foray_system_key from a name. Always set it explicitly on the project" is recorded as a past-incident lesson — silent derivation from project name corrupted audit data.

This is the deployed adapter pattern for an AI-agent system using FORAY. dunin7-workforce is the closest production parallel to what Loomworks might become.


Section 3 — The SDK contract

Two SDK contracts exist in the canonical material — the hypothetical one referenced by the kaspathon adapters, and the actual deployed one.

3.1 — The hypothetical foray-sdk.js (referenced, never implemented)

Inferred from kaspathon adapter call sites. The file foray-sdk.js does not exist in /Users/dunin7/foray-kaspathon/ or anywhere else under /Users/dunin7/.

Call sites in quickbooks-adapter.js

| Line | Call | Inferred signature | |---|---|---| | 24 | const ForaySDK = require('./foray-sdk'); | Module export: default class | | 96 | new ForaySDK(config.foray \|\| {}) | Constructor takes a config object | | 225 | this.sdk.createArrangement({source_system, transaction_type, reference_id, parties, terms}) | Sync; returns {id, ...} | | 252 | this.sdk.createAccrual({arrangement_ref, amount, currency, formula_id, formula_description, formula_inputs, debit_account, credit_account, fiscal_period}) | Sync; returns {id, ...} | | 271 | this.sdk.createAnticipation({arrangement_ref, expected_date, expected_amount, currency, settlement_account, conditions}) | Sync; returns {id, ...} | | 288 | this.sdk.createAction({arrangement_ref, anticipation_ref, settlement_type, actual_amount, settlement_date, payment_method}) | Sync; returns {id, ...} | | 300 | this.sdk.createTransaction({components: {arrangement, accrual, anticipation, action}}) | Sync; returns transaction object | | 309 | await this.sdk.anchorToBlockchain(transaction) | Async; returns {kaspaHash, ...} |

Call sites in salesforce-adapter.js

| Line | Call | Inferred signature | |---|---|---| | 94 | new ForaySDK(config.forayConfig \|\| {}) | Constructor takes a config object (named forayConfig here, not foray) | | 238 | this.foray.createArrangement({parties, asset_type, terms, effective_date, metadata}) | Sync; returns {hash, ...}note .hash not .id | | 258 | this.foray.createAccrual({arrangement_hash, formula_id, inputs, outputs, calculation_date, metadata}) | Sync; returns {hash, ...} | | 260 | this.foray.getFormulaId('weighted_revenue') | Sync; returns formula identifier string | | 278 | this.foray.createAnticipation({accrual_hash, expected_date, expected_amount, conditions, metadata}) | Sync; returns {hash, ...} | | 298 | this.foray.createAction({anticipation_hash, actual_date, actual_amount, settlement_type, proof}) | Sync; returns {hash, ...} | | 668 | this.foray.createAction({arrangement_hash, ...}) | Variant — Action can ref Arrangement directly (Lead conversion) | | 722 | await this.foray.anchorTransaction(transaction) | Async; method name anchorTransaction not anchorToBlockchain |

Inconsistencies between the two adapters' SDK assumptions:

  1. .id vs .hash return: QuickBooks expects .id; Salesforce expects .hash. Either both API shapes exist, or one adapter is wrong about the SDK shape.
  2. createAction reference field: QB uses arrangement_ref/anticipation_ref; Salesforce uses arrangement_hash/anticipation_hash/accrual_hash.
  3. createTransaction vs anchorTransaction: QB builds an intermediate createTransaction({components:...}) then anchorToBlockchain(transaction). Salesforce skips the wrapping step and calls anchorTransaction(transaction) directly.
  4. Config key: QB constructor takes config.foray; Salesforce takes config.forayConfig.
  5. getFormulaId: referenced only by Salesforce. QB hardcodes formula_id strings.

Inferred SDK signature (best guess):


class ForaySDK {
  constructor(config) { /* connection / chain config */ }

  createArrangement({source_system?, transaction_type?, asset_type?, reference_id?, parties, terms, effective_date?, metadata?}) → {id, hash, ...}
  createAccrual({arrangement_ref?, arrangement_hash?, amount?, formula_id, formula_description?, formula_inputs?, inputs?, outputs?, debit_account?, credit_account?, fiscal_period?, calculation_date?, metadata?}) → {id, hash, ...}
  createAnticipation({arrangement_ref?, accrual_ref?, accrual_hash?, expected_date, expected_amount, currency?, settlement_account?, conditions?, metadata?}) → {id, hash, ...}
  createAction({arrangement_ref?, arrangement_hash?, anticipation_ref?, anticipation_hash?, accrual_hash?, settlement_type, actual_amount?, actual_date?, settlement_date?, payment_method?, proof?}) → {id, hash, ...}
  createTransaction({components: {arrangement, accrual, anticipation, action}}) → transaction
  getFormulaId(name) → string
  anchorToBlockchain(transaction) async → {kaspaHash, ...}
  anchorTransaction(transaction) async → {kaspaHash?, ...}
}

This is inferred speculation. The actual foray-sdk.js file is not in the repository. The kaspathon adapters were written against a target SDK that either never shipped publicly or shipped in a different shape than the demos assumed.

3.2 — The actual deployed SDK: dunin7-foray v3.0.0

Already fully catalogued in §2.3. The contract is fundamentally different:

Method count: 14 methods total (constructor, connect, disconnect, emit, attest, _writeEvent (internal), getChain, getByType, getLatest, count, getAttestations, getAttestationsFor, getAttestationsByType, getAttestationsByAttestor, verifyChain, exportChain).

3.3 — Documentation cross-reference

The descriptive material at foray.dunin7.com (integration-guide.html, docs.html, specification.html) does NOT document either SDK contract:

There is no published SDK reference. The two adapter source files are the only place SDK calls are visible, and they disagree with each other.


Section 4 — The translation boundary

For source-side adapters (kaspathon style)

The translation boundary sits where the source-system-native object is passed into the adapter's anchor method:


Input:  Salesforce REST API JSON (Opportunity object with native fields)
Output: A constructed FORAY transaction object via SDK calls

What goes in (Salesforce Opportunity, raw):


{
  "Id": "0061T00000abc123",
  "AccountId": "0011T00000xyz789",
  "OwnerId": "00558000abc",
  "StageName": "Proposal",
  "Amount": 50000,
  "Probability": 60,
  "CurrencyIsoCode": "USD",
  "CloseDate": "2026-03-15",
  "CreatedDate": "2026-01-10T...",
  "ForecastCategoryName": "Pipeline",
  "IsClosed": false,
  "IsWon": false
}

What comes out (after Salesforce adapter, before SDK):


{
  arrangement: {
    parties: ['<hashed-account-id>', '<hashed-owner-id>'],
    asset_type: 'sales-opportunity',
    terms: { stage: '<hashed-stage>', type: '<hashed-type>', lead_source: '<hashed-source>', created_date: '2026-01-10T...' },
    effective_date: '2026-01-10T...',
    metadata: { salesforce_id: '<hashed-id>', object_type: 'Opportunity' }
  },
  accrual: {
    arrangement_hash: '<arrangement.hash>',
    formula_id: '<from sdk.getFormulaId("weighted_revenue")>',
    inputs: { amount: 50000, probability: 60, currency: '<hashed-USD>' },
    outputs: { expected_revenue: 30000, risk_adjusted_value: 25500 },  // rounded per privacy level
    calculation_date: '2026-05-24T...',
    metadata: { stage: '<hashed>', forecast_category: '<hashed>' }
  },
  anticipation: {
    accrual_hash: '<accrual.hash>',
    expected_date: '2026-03-15',
    expected_amount: 50000,
    conditions: [{ type: 'stage_progression', value: '<hashed-Closed Won>', probability: 0.6 }],
    metadata: { next_step: '<hashed>', days_to_close: 45 }
  },
  action: null   // null because IsClosed=false; only created when IsClosed && StageName==='Closed Won'
}

Translation steps in detail

  1. Identifier hashing — every Salesforce Id, AccountId, OwnerId is run through hashIdentifier(id, type) which SHA-256-hashes ${entitySalt}:${type}:${id}. The full 64-char hex is stored in the QuickBooks adapter; the Salesforce adapter does the same; but the Salesforce adapter's hashValue() for non-identifying values truncates to 16 chars.
  1. Amount obfuscation — per privacy level. QuickBooks rounds to nearest \$100 at standard, \$1000+noise at high, hashes at defense. Salesforce rounds to nearest \$1000 at standard, \$10000+noise at high, hashes at defense.
  1. Field selection / projection — only fields the adapter knows how to map are carried. Salesforce's Description field on Opportunity is dropped (privacy + structural mismatch). QuickBooks' Line[] is summarised to a count, not enumerated.
  1. Derived computation — Salesforce adapter computes expected_revenue = amount probability/100 and risk_adjusted_value = expected 0.85. QuickBooks adapter computes subtotal = TotalAmt - TxnTaxDetail?.TotalTax. These derived fields don't exist in the source; the adapter constructs them.
  1. Component skipping — both adapters decide which 4A components to construct based on source-system state. Unpaid Invoice → no Action. Unconverted Lead → no Action. Account → no Anticipation, no Action.

What information is lost or added in translation

Lost:

Added:

Required vs optional vs configured fields

| Field type | Source | Notes | |---|---|---| | Id | Required from source | Validation throws if missing | | TotalAmt / Amount | Required from source for monetary types | Validation throws if missing | | transaction_type / asset_type | Static configuration in the adapter | objectTypeMap in Salesforce; literal strings in QB | | privacy_level | Configured at adapter construction | Defaults to 'standard' | | entitySalt | Optional config; random 32 bytes if not provided | Cryptographic salt | | source_system | Hardcoded in QB adapter ('quickbooks') | Identifies origin | | formula_id | Hardcoded string per anchor method | 'invoice_total', 'bill_total', etc. |

For persistence-side adapter (ForayClient style)

The translation boundary is different — the application has already done 4A construction, and the SDK's job is to persist + hash:


Input:  An application-constructed event payload (any JSON shape)
Output: A PostgreSQL row in foray_events with a chained hash

There is no 4A-specific translation in the SDK. The application chooses event-type strings and payload shapes. The SDK enforces:

The Pluggable Persistence design doc envisioned a stricter contract — a ForayHashEntry with explicit forayJson + hash + previousHash — but the live SDK is looser. The application can emit('arbitrary.event.type', 'system', {anything: 'goes'}) and the SDK will chain it.


Section 5 — Persistence and replay

kaspathon adapters

There is no queue, no journal, no event log on the adapter side. Replay would require the application to re-extract from the source system and re-call the adapter.

ForayClient (dunin7-foray v3.0.0)

The architectural posture is: events are persisted synchronously or they fail synchronously. There is no eventual-consistency fallback. This is consistent with the FORAY "tamper-evident" claim — there is no buffer where events could be tampered with before persistence.

Pluggable Persistence design (not implemented)

The design doc envisioned fan-out persistence with admin-configurable retry:


type ForayPipelineConfig = {
  destinations: { postgres, nosql, blockchain, s3 };
  requireAll: boolean;   // true = all enabled must succeed; false = best-effort
  retryPolicy: { maxRetries, backoffMs };
};

In the imagined Phase 2 architecture, an event would be fanned out to all enabled destinations in parallel. The retry policy would apply per destination. Best-effort mode would let a slow destination not block others.

None of this is implemented today. The live SDK is single-destination PostgreSQL with no retry policy.


Section 6 — Configuration and deployment

kaspathon adapter configuration

Constructor config shape (QuickBooks):


new QuickBooksForayAdapter({
  quickbooks: qboClient,        // Required: a QBO client instance
  foray: {/* SDK config */},    // Required: SDK constructor arg
  privacyLevel: 'standard',     // 'minimal' | 'standard' | 'high' | 'defense'
  entitySalt: '<hex>',          // Optional: 32-byte hex; random if absent
  retryAttempts: 3,
  retryDelayMs: 1000,
  enableLogging: true,
  minRequestInterval: 100
});

Credentials needed:

Deployment: Library, imported by the application. No standalone deployment.

Lifecycle: No start/stop methods. Construct, call anchor methods, discard.

ForayClient configuration


new ForayClient({
  product: 'fieldpilot',          // Required: determines database name foray_${product}
  connectionString: 'pg://...',   // OR host/port/user/password individually
  host: 'localhost',              // Default: PG_HOST env or 'localhost'
  port: 5432,                     // Default: PG_PORT env or 5432
  user: 'postgres',               // Default: PG_USER env or 'postgres'
  password: '...'                 // Default: PG_PASSWORD env
});

Environment variable conventions:

Credentials needed:

Deployment: Library, imported by the application. The PostgreSQL database itself must be provisioned separately (one database per product, with the foray_events table created on first connect()).

Lifecycle:

Per-system provisioning (from workforce architecture doc):

  1. CREATE DATABASE foray_{key};
  2. Run FORAY schema migrations against the new database (creates foray_events).
  3. Register a ForayClient in the application's services layer.
  4. Set foray_system_key on the relevant projects.

Adapter deployment per integration-guide.html

The descriptive guide does not specify a single deployment model. It implies the adapter runs somewhere between the source system and FORAY — could be a container, a service, a library. Per integration-guide.html:

> "Phase 1: Extract — Connect to source system API (REST, SOAP, BAPI, etc.)"

implies the adapter has network access to the source system. The integration patterns assume API access; no on-premise database connection details are documented.


Section 7 — Documentation conventions

Organization of adapter material at foray.dunin7.com

integration-guide.html is the primary integration document. Its structure:


1. Why FORAY + ERP                  — value proposition
2. Integration Pattern               — five-step Extract/Transform/Hash/Anchor/Store
3. SAP S/4HANA                      — per-system: audit gap, FORAY mapping table
4. Oracle EBS / Cloud               — per-system: audit gap, mapping
5. QuickBooks                       — per-system: invoice lifecycle mapping, link to .js
6. Salesforce                       — per-system: object lifecycle table, cross-system reconciliation
7. NetSuite                         — per-system: transaction mapping table
8. Adapter Architecture             — three-phase pattern
9. Privacy Levels                   — Standard / High / Defense-grade
10. Batch vs. Real-Time Anchoring  — strategy table

docs.html is the protocol-and-integration overview. Its adapter-relevant sections:

The canonical structure of an adapter description (inferred from SAP/Oracle/QuickBooks/Salesforce/NetSuite sections in integration-guide.html)

Each per-ERP section follows a similar shape:

  1. Native Audit Capabilities — what the source system already offers
  2. The Audit Gap — table of source-system capability vs limitation (typically: admins can alter logs)
  3. FORAY + {System} Integration — table of source-module / FORAY-mapping / integration-point

The shape is descriptive, not prescriptive. It does not specify endpoint URLs, message formats, or wire protocols.

Are there templates or scaffolds for new adapters?

No templates or scaffolds exist in the repository. The two source adapters (QuickBooks, Salesforce) are the only working code examples. They diverge enough from each other that they cannot serve as a single template.

The Pluggable Persistence doc proposes a ForayPersistenceAdapter interface (for the destination-side adapter), but this is concept-level, not a code scaffold.

How does the protocol communicate adapter compliance expectations?

From RFC-0001.md (in foray-protocol-db, read in the challenge deep-read):

> "Connector implementations. Build a FORAY-compliant connector for any source system. The connector specification defines the submission schema. Any system that can produce a valid FORAY submission is a compliant source. There is no certification required to build. Certification is available for those who want it."

The compliance bar is produce a valid FORAY submission per the schema. There is no formal SDK-conformance test, no certification suite. Adapter compliance is structural — does your adapter produce valid 4A JSON per the schema reference. The schema reference is in docs/DUNIN7_FORAY_Schema_Reference.md (covered in the protocol deep-read).


Section 8 — Other substantive findings

F1 — Two adapter meanings is the load-bearing observation

The single most important finding from this deep-read. "Adapter" in FORAY material refers to:

  1. Source-side (translation) — kaspathon-style, converts ERP data to 4A
  2. Persistence-side (destination) — Pluggable Persistence concept, takes 4A + hash to storage

The two adapters' lifecycles, contracts, and concerns are different. A Loomworks integration design that conflates them would design the wrong thing. The decision space is:

F2 — The "AI Agent Integration" section in docs.html is the canonical Loomworks-shaped use case

Reproduced verbatim from docs.html lines 1118-1148:

| FORAY Component | What It Captures for AI Agents | Example | |---|---|---| | Arrangement | The agent's mandate — what it was authorized to do, by whom, under what constraints | Agent authorized to approve purchase orders under \$50K, referencing model version hash and system prompt hash | | Accrual | The agent's reasoning — what information was analyzed, what it determined | Agent evaluates three vendor quotes, determines Vendor B is \$2,400 below market rate | | Anticipation | The agent's intent — what action it planned, with what expected outcome | Agent plans to approve \$47,200 PO to Vendor B; expected outcome: delivery within SLA | | Action | The agent's execution — what it actually did, the result, variance from intent | PO approved and submitted at 14:32:07 UTC; confirmation reference PO-2026-04471 |

This is the mapping a Loomworks-as-FORAY-anchored-system would use. Every Companion-mediated assertion commit decomposes:

The model version hash + system prompt hash bit is particularly relevant — Loomworks already has a system_prompt field on its specialists. A FORAY arrangement for a Loomworks engagement would hash these.

F3 — Privacy levels are adapter concerns, not SDK concerns

In both kaspathon adapters, privacy obfuscation happens inside the adapter (_obfuscateAmount, _hashIdentifier). The SDK never sees plaintext values. This pattern means:

The dunin7-foray SDK has no built-in privacy obfuscation — values are persisted as-is in JSONB. This is consistent with the adapter-handles-privacy convention. A Loomworks adapter would need to decide its privacy posture before calling the SDK.

F4 — The kaspathon adapters use entitySalt as a primitive party identifier in some paths

In Salesforce's Quote/Order handlers (e.g., line 350):


parties: [accountId, this.entitySalt].filter(Boolean)

The entitySalt is being used as a stand-in for "this adapter's organisation." This is concerning — entitySalt is meant to be a cryptographic salt for hashing identifiers; using it as a party id directly leaks cryptographic material into the FORAY transaction. This appears to be a demo-code shortcut where the adapter's owning organisation is fixed and the salt happens to be unique enough. A Loomworks production adapter should not replicate this pattern.

F5 — There is no kasperatcoder or named anchor service

Across all source and documentation read, no service called the "FORAY anchor service" exists as a deployable HTTP API. The Kaspa blockchain is the anchor service in the kaspathon adapters' model; PostgreSQL is the anchor service in the dunin7-foray model. The Pluggable Persistence design doc proposes additional anchors (NoSQL, S3, on-chain hash anchoring) but none are implemented.

Implication for Loomworks: "integrating with the FORAY anchor service" is ambiguous wording. Concrete choices are:

The pragmatic Loomworks choice today is the first option.

F6 — The kaspathon adapters appear to be marketing artifacts, not deployable

Considered together: (a) the disclaimer block calls them demonstration-only; (b) they reference an absent SDK; (c) the two adapters disagree on SDK shape; (d) batch processing is sequential; (e) no error replay; (f) entitySalt mis-used. **These adapters were written to describe what a FORAY adapter could look like, not to be deployable.** They populate the integration-guide.html sections with link-out references but cannot themselves be installed and run.

The live deployed FORAY usage is dunin7-foray v3.0.0 + dunin7-workforce direct calls. There is no production source-side adapter in the kaspathon style.

F7 — The dunin7-foray SDK silently allows arbitrary event_type strings

The SDK's emit(chainId, eventType, triggeredBy, payload) accepts any TEXT for eventType. The application is the source of vocabulary — there is no SDK-side validation. This means:

F8 — Per-chain mutex serialisation is a real constraint for parallel agents

The ChainMutex (lines 65-81 of foray-client.mjs) serialises ALL writes to a given chain. If multiple agents in Loomworks write to the same engagement's chain in parallel, they queue. Per-chain throughput is therefore single-threaded.

For Loomworks specifically: this means parallel Companion processes operating on the same engagement would queue on writes. This is necessary for hash chain integrity but is a throughput consideration.

F9 — There is no specified delivery semantic (at-most-once / at-least-once / exactly-once)

Neither the kaspathon adapters nor the live SDK specifies a delivery guarantee. The live SDK is exactly-once if the transaction commits, and the application sees the success result; if the application crashes after the DB commit but before processing the result, the event is durably persisted but the application doesn't know it. There is no idempotency token or replay-detection mechanism.

For Loomworks: an idempotent commit would have to be the application's concern — e.g., a memory_event has a content_hash that could double as an idempotency check.

F10 — TypeScript predecessor's typed event-type union is more disciplined than v3.0.0's free strings

The dunin7-foray-validate package (the TypeScript predecessor) enforces a discriminated union of event types at compile time. v3.0.0 dropped this constraint. The trade-off:

The v3.0.0 choice was deliberate (the SDK is now generic across products), but a more disciplined application would re-establish a typed vocabulary at its own layer.

F11 — There is no observability or metrics emission from the SDK

Neither SDK emits Prometheus / OTel / structured logs at observability frequencies. The application would have to wrap SDK calls to capture latency / failure rate. The enableLogging flag on kaspathon adapters just routes to console.log with timestamps; no structured logging.


Things found that contradict prior deep-read material

Contradiction 1 — The protocol deep-read inferred the SDK from kaspathon source; this deep-read finds the actual SDK has a different shape

The protocol deep-read §5 ("The FORAY anchor service — operational interface") concluded:

> "There is enough material in the local repository to know the conceptual shape: the submitting system constructs a FORAY envelope with a declared persistence layer, submits it to a layer-specific service (probably via the absent foray-sdk.js), receives a blockchain_anchor block..."

That conclusion was inferred from the kaspathon adapter call sites. It is partially wrong — the actual deployed SDK (dunin7-foray v3.0.0) does NOT match that shape. The deployed SDK:

The protocol deep-read's conclusion accurately describes the kaspathon hypothetical model, but the live deployed model is the ForayClient-PostgreSQL chain. The two are different architectures.

Contradiction 2 — The challenge deep-read described an "anchor service" not as an HTTP API

The challenge deep-read §5 said:

> "The challenge site does not document a FORAY anchor-service wire interface... the persistence layer (Kaspa or otherwise) is referenced as a property of 'real' submissions but the demo does not perform anchoring."

This is correct, but incomplete. There is a live persistence layer — PostgreSQL via dunin7-foray — that is not what the challenge site documents but is what actually runs in dunin7-workforce. The challenge site's framing emphasises Kaspa; the deployed reality is PostgreSQL. Both are persistence layers in the Pluggable Persistence sense; only one (PostgreSQL) has a working implementation.

Contradiction 3 — Both prior deep-reads talked about FORAY adapters as if there were a single canonical pattern

The protocol and challenge deep-reads both wrote about "adapters" without distinguishing source-side from persistence-side. The kaspathon adapters were referenced as if they showed how Loomworks would integrate. This deep-read finds the kaspathon adapters are demo-grade and reference an absent SDK; the production pattern is dunin7-foray ForayClient called directly by application code. A Loomworks integration would more closely resemble dunin7-workforce's pattern (which is library-call-from-application) than the kaspathon ERP adapter pattern.

This is the most important correction to surface for Claude.ai's next round of substrate-review work.


Things asked for but not found

| Topic | What's missing | Where it likely is | |---|---|---| | The hypothetical foray-sdk.js source | Absent from kaspathon repo and the rest of the filesystem | Was never implemented in the shape the adapters assume; or lives in DUNIN7-internal storage not visible here | | OpenClaw Trading Audit Skill source | Not in /Users/dunin7/ | Lives in the OpenClaw repository (external) | | A working Kaspa anchor implementation | Not in any reviewed source | Would need to be built per Pluggable Persistence Phase 3 | | A standardised ForayPersistenceAdapter implementation | The interface is in the design doc; ForayClient doesn't conform to it | Phase 2/3 of Pluggable Persistence not yet built | | End-to-end flow demonstration (source → SDK → anchor → verify) | The kaspathon repo's foray-api-server.js demonstrates source → LLM → 4A JSON, but stops there | The dunin7-workforce app shows application → ForayClient → PostgreSQL, but the kaspathon-style adapter layer between source ERP and 4A is missing | | SDK reference documentation | Neither SDK has published reference docs | The shape has to be inferred from source code (which I did in §3) | | Performance benchmarks | Adapter rate limit and SDK mutex behaviour suggest single-chain throughput is low single-digits/second, but no published benchmarks | None found | | Adapter certification / compliance test suite | RFC-0001 mentions certification is "available for those who want it" but no test suite is referenced | Not in any reviewed repo | | NetSuite, SAP, Oracle adapters as source code | Only QuickBooks and Salesforce have source adapters | NetSuite/SAP/Oracle are described in integration-guide.html but no code exists |


Closing — implications for the Loomworks framing

Three reads now compose:

  1. Protocol deep-read — protocol-level: schema, attestation rules, framing drift
  2. Challenge deep-read — integration-pattern surrogate: pattern codes, library taxonomy, Loomworks-shaped use case (#472 Emergent Agent Transaction)
  3. This deep-read — adapter-level: source-side adapters demo-grade only, persistence-side via dunin7-foray ForayClient, AI-agent 4A mapping from docs.html

What this means for the substrate review work that follows:


End of adapter deep-read.