DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path substrate/cleanup/loomworks-foray-integration-locations-v0_1.html
Loomworks · Substrate

FORAY Integration Locations Inventory — v0.1

Version: 0.1
Date: 2026-05-24
Author: Marvin Percival (DUNIN7), prepared via Claude Code
Companion to: loomworks-substrate-hygiene-scoping-note-v0_1
Purpose: Step 1 of the substrate hygiene cleanup. Identifies every location in the engine code where one of the three wrong-pattern FORAY-shaped tables is currently being written. Operator reviews before any code changes proceed.

What this inventory is for

A walk through the Loomworks engine code, naming every place that writes to credit.foray_action_flows, audit.foray_events, or the Phase 25 FORAY-readiness wiring on memory_events. For each location, the inventory captures the event in plain English, the data being recorded, what's Loomworks-essential versus FORAY-readiness, and what the cleanup would look like.

The inventory is read-only — nothing in the engine has been changed. The next step (Step 2, designing replacement tables) waits for the Operator's review of this document. A complexity assessment closes the document.

Engine baseline


Section 1 — credit.foray_action_flows write locations

The credit substrate writes to credit.foray_action_flows from eight distinct logical operations across twelve ForayActionFlowRow constructor calls. All writes share a common shape (transaction_id, asset_id, quantity, from_party, to_party, flow_metadata). The downstream PostgreSQL trigger update_balance_on_flow maintains credit.asset_balances from these flows.

1.1 — Grant claim (issuance)

File / lines

src/loomworks/credit/flows.py:68-102 (the writer); src/loomworks/credit/grants.py:590 (the caller).

What happens, in plain English: When a person claims a credit grant (via the claim-token endpoint), the credit substrate records the issuance as a single flow from the institutional party dunin7 to the recipient's UUID, for the granted amount.

Current write:

ForayActionFlowRow(
    transaction_id=txid,
    asset_id=asset_id,                              # e.g. 'loomworks_credit_haiku'
    quantity=amount,                                # the granted amount
    from_party=DUNIN7_PARTY,                        # literal string 'dunin7'
    to_party=str(person_id),                        # UUID-as-string
    flow_metadata={"reason": "grant_claim", "grant_id": str(grant_id)},
)

Data being recorded: which person received which credit type in what amount, with the grant identifier linking back to the grant row.

What Loomworks actually needs: the operational fields (person_id, asset_id, amount, when, grant_id, why). No FORAY-shape fields are load-bearing here. The transaction_id is FORAY-decoration; the from_party='dunin7' is a polymorphic-VARCHAR institutional-party identifier that wouldn't be needed in a Loomworks-natural table.

Proposed Loomworks-natural replacement: a row in a credit.events table with fields (event_kind='grant_claimed', person_id, asset_id, amount, grant_id, occurred_at). The balance trigger replacement reads from credit.events instead of credit.foray_action_flows.

Proposed reserved location marker:

await _foray_reserved_emit("credit.grant_claimed", {
    "person_id": str(person_id),
    "asset_id": asset_id,
    "amount": amount,
    "grant_id": str(grant_id),
})

1.2 — Companion turn consumption (five flows per turn)

File / lines

src/loomworks/credit/flows.py:110-237 (the writer); src/loomworks/credit/seam.py:219 (the caller via InProcessCreditSeam).

What happens, in plain English: When the Operator's Companion turn completes against a Loomworks-managed key (system-key path, not Operator-key path), the credit substrate records five flows in one logical transaction: classifier input tokens, classifier output tokens, responder input tokens, responder output tokens, and the credit debit that prices the four token-asset flows into credits.

Current write: five ForayActionFlowRow constructors at lines 157, 165, 173, 181, 223. The first four are person → anthropic flows recording token consumption; the fifth is person → dunin7 recording the credit debit. All share transaction_id and carry flow_metadata with engagement_id, turn_event_id, and pipeline_stage ('classify', 'respond', or 'credit_debit').

Data being recorded: which Operator consumed how many tokens of which model in which pipeline stage during which engagement-turn, and the converted credit cost the substrate charged for that consumption.

What Loomworks actually needs: the operational ledger fields (person_id, engagement_id, turn_event_id, per-stage token counts, credit asset and amount, when) — the substrate's accounting depends on these. The FORAY-shape (5-row pseudo-transaction, polymorphic parties) is decoration.

Proposed Loomworks-natural replacement: a single credit.consumption_events row per turn carrying the operational fields (turn_event_id PK, engagement_id, person_id, classifier_input_tokens, classifier_output_tokens, responder_input_tokens, responder_output_tokens, credit_asset_id, credit_debited, occurred_at). The five-flow structure collapses to one row because the operational question — "how much did this Operator's turn cost?" — is one question, not five.

Proposed reserved location marker:

await _foray_reserved_emit("credit.turn_consumption", {
    "engagement_id": str(engagement_id),
    "turn_event_id": str(turn_event_id),
    "person_id": str(person_id),
    "classifier_tokens": {"in": classifier_input_tokens, "out": classifier_output_tokens},
    "responder_model_token_pair": responder_model_token_pair,
    "responder_tokens": {"in": responder_input_tokens, "out": responder_output_tokens},
    "credit_asset_id": credit_asset_id,
    "credit_debit": credit_debit,
})

1.3 — Account suspension

File / lines

src/loomworks/credit/flows.py:245-269 (the writer); src/loomworks/credit/lifecycle.py:136 (the caller).

What happens, in plain English: When an Operator's account is suspended (via the Phase 48 evaluator or admin action), the credit substrate records a one-flow synthetic-asset transaction using loomworks_account_status as the asset, with quantity=1 and metadata carrying the suspension expiry and who requested it.

Current write:

ForayActionFlowRow(
    transaction_id=txid,
    asset_id=ACCOUNT_STATUS_ASSET,                  # 'loomworks_account_status'
    quantity=1,
    from_party=DUNIN7_PARTY,
    to_party=str(person_id),
    flow_metadata={
        "event": "suspension",
        "requested_by": str(requested_by),
        "expires_at": expires_at.isoformat(),
    },
)

Data being recorded: that this person's account was suspended at some time, by someone, until some expiry.

What Loomworks actually needs: the lifecycle event fact and its metadata (person_id, event_kind='suspended', requested_by, expires_at, occurred_at). The loomworks_account_status asset and quantity=1 are FORAY-shape decoration — this is not really a value flow, it's a lifecycle event being shoehorned into the value-flow table.

Proposed Loomworks-natural replacement: a row in a Loomworks persons.lifecycle_events table (or extend the existing persons columns plus an audit-shape table). The data is account-lifecycle state, not value flow.

Proposed reserved location marker:

await _foray_reserved_emit("account.suspended", {
    "person_id": str(person_id),
    "requested_by": str(requested_by),
    "expires_at": expires_at.isoformat(),
})

1.4 — Account reactivation

File / lines

src/loomworks/credit/flows.py:272-294 (the writer); src/loomworks/credit/lifecycle.py:176 (the caller).

What happens, in plain English: When a suspended Operator's account is reactivated (e.g., they verify identity via WebAuthn or operator override), the credit substrate writes a one-flow synthetic-asset transaction recording the reactivation and the auth method that allowed it.

Current write: same shape as suspension, with flow_metadata = {"event": "reactivation", "auth_method": auth_method}.

Proposed replacement / reserved marker: row in the same Loomworks lifecycle table.

await _foray_reserved_emit("account.reactivated", {
    "person_id": str(person_id),
    "auth_method": auth_method,
})

1.5 — Account deletion

File / lines

src/loomworks/credit/flows.py:297-319 (the writer); src/loomworks/credit/lifecycle.py:236 (the caller).

What happens, in plain English: When an Operator's account is deleted, the credit substrate writes a one-flow synthetic-asset record (the deletion fact) plus zero or more balance-zeroing flows (next item).

Current write: same shape as suspension/reactivation, with flow_metadata = {"event": "deletion", "deletion_kind": deletion_kind}.

Proposed replacement / reserved marker: Loomworks lifecycle table row.

await _foray_reserved_emit("account.deleted", {
    "person_id": str(person_id),
    "deletion_kind": deletion_kind,
})

1.6 — Balance zeroing at deletion

File / lines

src/loomworks/credit/flows.py:322-359 (the writer); src/loomworks/credit/lifecycle.py:235 (the caller).

What happens, in plain English: Immediately before the deletion flow is written, the credit substrate iterates over every credit asset the person holds with a positive balance and writes a person → dunin7 flow draining the balance to zero. Zero, one, or many flows per deletion depending on how many credit types the person held.

Current write (per credit type with positive balance):

ForayActionFlowRow(
    transaction_id=txid,
    asset_id=asset_id,                              # iterates CREDIT_ASSET_IDS
    quantity=balance,                               # the positive balance being zeroed
    from_party=person_str,
    to_party=DUNIN7_PARTY,
    flow_metadata={"event": "balance_zeroing", "reason": "account_deletion"},
)

Data being recorded: that this person's balance in each credit type was drained at deletion time, and the amount that was drained.

What Loomworks actually needs: the operational fact that balances were zeroed at deletion plus the amounts (useful for any future audit query of "what did this Operator's account hold at deletion"). The mechanism — using inverse-direction flows to trigger the balance-update trigger — is FORAY-shape; in a Loomworks-natural design, deletion sets balances to 0 directly (UPDATE) and the audit row records the prior balances as JSON.

Proposed Loomworks-natural replacement: a single account.balances_zeroed_at_deletion row in a Loomworks lifecycle table carrying {person_id, balances_zeroed: {asset_id: amount, ...}, occurred_at}. The balance-update trigger goes away with the action-flows table.

await _foray_reserved_emit("account.balances_zeroed_at_deletion", {
    "person_id": str(person_id),
    "balances_zeroed": {asset_id: amount for ...},
})

1.7 — Referral credit (own-key conversion)

File / lines

src/loomworks/credit/flows.py:367-446 (the writer); src/loomworks/credit/conversion_observer.py:166 and src/loomworks/credit/seam.py:222 (the callers); also src/loomworks/api_keys/hooks.py triggers this through the seam.

What happens, in plain English: When a referee converts (saves their own LLM API key), the conversion observer fires a referral-credit flow from dunin7 to the referrer for the configured referral amount. The function is idempotent per converted_person_id to ensure a referrer of two referees accumulates both credits but a single referee can never trigger the referral twice.

Current write:

ForayActionFlowRow(
    transaction_id=txid,
    asset_id=asset_id,
    quantity=amount,
    from_party=DUNIN7_PARTY,
    to_party=referrer_str,
    flow_metadata={
        "event": "referral_credit",
        "referrer_person_id": referrer_str,
        # plus optional converted_person_id, source_grant_id, reason='conversion_detected'
    },
)

Data being recorded: the referrer earned a referral credit, in which asset, in what amount, attributed to which converted-referee.

What Loomworks actually needs: referrer_person_id, asset_id, amount, converted_person_id, occurred_at. The conversion observer's idempotency check reads flow_metadata['converted_person_id'] via JSONB extract — a Loomworks-natural table makes this a typed column.

Proposed replacement: a row in the same credit.events table proposed for §1.1 (or a sibling credit.referral_events table) with (event_kind='referral_credit', referrer_person_id, asset_id, amount, converted_person_id, source_grant_id, occurred_at). The unique constraint (referrer_person_id, converted_person_id) WHERE event_kind='referral_credit' replaces the JSONB-extract idempotency check.

await _foray_reserved_emit("credit.referral_credit", {
    "referrer_person_id": str(referrer_person_id),
    "converted_person_id": str(converted_person_id),
    "asset_id": asset_id,
    "amount": amount,
})

1.8 — Reconciliation correction (apply)

File / lines

src/loomworks/credit/proposal_applier.py:299-471 (the function); the write itself at lines 395-408.

What happens, in plain English: Phase 48 added a reconciliation evaluator that detects drift between what credit.foray_action_flows says happened and what should have happened, producing a ReconciliationProposal memory event. When the Operator approves the proposal (via an Approval Card), apply_reconciliation_correction writes one corrective flow to credit.foray_action_flows to bring the ledger back into agreement, and appends a new ReconciliationProposal version with state='approved'. The corrective flow's metadata carries applied_by (the approving person) and shape_event_id linking to the proposal.

Current write:

flow = ForayActionFlowRow(
    transaction_id=txid,
    asset_id=flow_kwargs["asset_id"],
    quantity=flow_kwargs["quantity"],
    from_party=flow_kwargs["from_party"],
    to_party=flow_kwargs["to_party"],
    flow_metadata=flow_metadata,                    # includes applied_by, shape_event_id, ...
)

The flow_kwargs are computed upstream by the Shaping specialist (pure function on the proposal payload) and the Render specialist (builds the row kwargs).

Data being recorded: an Operator-approved correction to the credit ledger, with attribution to the approving Operator and a link to the proposal that surfaced the drift.

Proposed replacement: a credit.corrections table or a row in credit.events with event_kind='reconciliation_correction' and fields (person_id, asset_id, delta_amount, applied_by_person_id, proposal_id, occurred_at). The reconciliation evaluator's drift-detection query (currently reading ForayActionFlowRow.quantity) reads from the replacement table instead.

await _foray_reserved_emit("credit.reconciliation_correction", {
    "person_id": str(person_id),
    "asset_id": flow_kwargs["asset_id"],
    "delta_amount": flow_kwargs["quantity"],
    "applied_by_person_id": str(person_id),
    "proposal_id": str(proposal_id),
})

Cross-cutting observations on the credit substrate


Section 2 — audit.foray_events write locations

The audit substrate is structurally smaller. There is one writer helper (write_setting_change_event) called from three places, all writing the same row shape with event_type='setting_change'. No other event types are written.

2.1 — Setting change via Companion (tune_setting intent)

File / lines

src/loomworks/audit/events.py:52-89 (the writer); src/loomworks/orchestration/tune_setting.py:65-99 (the local helper _audit_setting_change); call sites at tune_setting.py:489 and tune_setting.py:596.

What happens, in plain English: When the Companion classifies an Operator turn as the tune_setting intent (e.g., the Operator says "make the backdrop blur stronger"), and the substrate write succeeds, an audit row is written recording the setting key, previous value, new value, and the engagement context where the change was requested. The audit write is best-effort — a failure here is logged and swallowed because the setting change is already authoritative at that point.

Current write:

AuditForayEventRow(
    tx_id=tx_id,                                    # uuid4() per call
    event_type="setting_change",                    # only event type today
    actor_person_id=actor_person_id,
    engagement_id=engagement_id,                    # nullable
    payload={
        "setting_key": setting_key,                 # e.g. 'voice.listening.blur_intensity'
        "previous_value": <json-safe>,
        "new_value": <json-safe>,
    },
    signature=None,                                 # reserved for Kaspa signing; never written
    timestamp=now,
)

Data being recorded: an Operator (actor_person_id) changed a setting (setting_key) from a previous value to a new value, in some engagement context.

What Loomworks actually needs: actor_person_id, setting_key, previous_value, new_value, engagement_id (nullable), occurred_at. All operational data. The tx_id (separate from id), the event_type discriminator (only one value), and the signature column are FORAY-shape decoration.

Proposed Loomworks-natural replacement: a settings.changes table (or extend person_settings with a sibling audit table) with the operational fields. Drop tx_id, event_type, signature. If future settings need typed previous/new value columns rather than JSONB, that's a refinement; today they're all primitive so JSONB is fine.

await _foray_reserved_emit("settings.changed", {
    "actor_person_id": str(actor_person_id),
    "setting_key": setting_key,
    "previous_value": previous_value,
    "new_value": new_value,
    "engagement_id": str(engagement_id) if engagement_id else None,
})

2.2 — Setting change via PUT /me/settings/{key} (button click)

File / lines

src/loomworks/api/routers/me_settings.py:117-132.

What happens, in plain English: When an Operator changes a setting via the UI (e.g., clicks a control on a settings panel), the PUT endpoint writes the setting via set_setting and then calls write_setting_change_event so the click-driven change shows up in the same audit ledger as Companion-spoken changes. engagement_id is None on this path because /me/settings is person-scoped. Same best-effort posture as §2.1.

Current write: same AuditForayEventRow shape as §2.1; engagement_id=None.

Data being recorded: same fields, just sourced from the HTTP request rather than the Companion turn.

await _foray_reserved_emit("settings.changed", {
    "actor_person_id": str(person.id),
    "setting_key": setting_key,
    "previous_value": previous,
    "new_value": normalized,
    "engagement_id": None,
})

Cross-cutting observations on the audit substrate


Section 3 — memory_events Phase 25 readiness wiring

This is the more nuanced section. Memory events themselves are Loomworks-essential — they record assertion lifecycle, engagement creation, seed handling, and the substantive work the engagement does. What gets removed is the Phase 25 wiring that prepared for FORAY anchoring inside Loomworks.

3.1 — The _foray_* columns on memory_events

File / lines

src/loomworks/memory/events.py:98-104.

# Phase 25 — FORAY readiness columns. content_hash is written on
# every new event; attestation and foray_tx_ref are populated
# only when attestation is provided (Step 5) or FORAY anchors
# land (deferred).
content_hash: Mapped[str | None] = mapped_column(Text(), nullable=True)
attestation: Mapped[dict | None] = mapped_column(JSONB(), nullable=True)
foray_tx_ref: Mapped[str | None] = mapped_column(Text(), nullable=True)

Per-column status (verified by reading every source file that references these columns):

ColumnWritten?Read?Verdict
content_hashYes — every event, at events.py:271 via compute_content_hash(payload)No — no source file outside memory/events.py reads this columnPure FORAY-readiness. Removable.
attestationYes — by stamp_commit_attestation at engagement-commit time (commit_orchestration.py:124-143)Implicit — engagement-committed events carry it for future audit walks; no code reads it backMemory-relevant data, FORAY-shape column name. The data is the Operator's WebAuthn signature at commit time, which is identity/auth provenance — Loomworks-essential. The column name attestation is FORAY-overloaded.
foray_tx_refNo — never written by any source fileNo — never readPure dead column. Removable.
Proposed treatment
  • Remove content_hash column. No reader; the value is recomputable from payload if ever needed.
  • Rename attestation to commit_webauthn_attestation (or similar). The data is Memory-essential (the WebAuthn proof of the Operator's commit), but the FORAY-shape name muddles its meaning. Keep the column under its true name.
  • Remove foray_tx_ref column. Never written, never read — pure dead weight.

3.2 — The _ANCHOR_PRIORITY registry

File / lines

src/loomworks/memory/events.py:107-165.

The registry maps event_kind strings to anchor-priority levels (critical | high | standard | low). 19 entries today, organised by phase introduction:

event_kindPriorityIntroduced by
engagement_committedcriticalPhase 25
engagement_committed_divergentcriticalPhase 25
membership_createdhighPhase 25
agent_registeredhighPhase 25
seed_draftedstandardPhase 25
seed_amendedstandardPhase 25
discovery_to_seed_extractedstandardPhase 53 (P53-D12)
seed_committed_from_briefstandardPhase 54 (P54-D5)
engagement_created_from_assistancestandardPhase 55 (P55-D8)
finding_producedstandardPhase 25
finding_addressedstandardPhase 25
induction_cycle_recordedstandardPhase 25
candidate_engagement_discardedlowPhase 25
conversation_startedstandardPhase 31
operator_turnstandardPhase 31
companion_turnstandardPhase 31
key_providedhighPhase 31
upload_event_receivedstandardPhase 58
manual_content_contributedstandardPhase 59

Today, the registry is used only by the injection logic at events.py:244-255. Nothing else reads it.

Recommendation

Keep the registry but rename it. Candidates: _FORAY_INTEGRATION_PRIORITY (neutral) or _FORAY_RESERVED_EVENT_PRIORITY (more aligned with the reserved-location pattern). Either preserves the load-bearing design intent (which Memory events would deserve FORAY anchoring when integration eventually happens) without continuing to populate columns. Recommend the second — it pairs explicitly with _foray_reserved_emit.

Move the registry to where the reserved emitter lives (see §4). The registry shouldn't sit in memory/events.py once the injection logic is gone — it's reference data for the FORAY integration plan, not Memory machinery.

3.3 — The _foray namespace injection logic

File / lines

src/loomworks/memory/events.py:236-255 (inside append_event).

# Phase 25 — content hash + optional _foray namespace.
# Hash first (without `_foray` if it somehow already exists),
# then for known engagement-creation event kinds inject the
# `_foray` block carrying the priority and a denormalized copy
# of the hash. The DB column `content_hash` is the source of
# truth; the in-payload copy is for downstream consumers that
# walk payloads directly.
content_hash = compute_content_hash(payload)
anchor_priority = _ANCHOR_PRIORITY.get(event_kind)
if anchor_priority is not None:
    payload["_foray"] = {
        "anchorable": True,
        "anchor_priority": anchor_priority,
        "content_hash": content_hash,
        # Set by the caller for engagement-committed events when
        # an attestation accompanies the commit (Step 5). Self-
        # reference on the same event_id is the convention; null
        # when no attestation was provided.
        "attestation_ref": None,
    }

Status: This logic injects a _foray block into the JSONB payload for anchorable event kinds. No source file outside this function reads the _foray block. The injection writes documentation-in-data; nothing consumes it.

Proposed treatment: Remove the injection block entirely. The _foray namespace inside payloads is dead weight on every event whose kind appears in _ANCHOR_PRIORITY (roughly half the engine's event kinds). Removing it shrinks every such row's payload by ~150 bytes plus mental-model load. append_event becomes shorter and the comment above it (currently 8 lines of Phase 25 explanation) collapses.

3.4 — compute_content_hash

File / lines

src/loomworks/memory/events.py:168-180.

def compute_content_hash(payload: dict[str, Any]) -> str:
    """Deterministic SHA-256 of the canonicalized event payload..."""
    hashable = {k: v for k, v in payload.items() if k != "_foray"}
    canonical = json.dumps(hashable, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

Callers: only line 243 of append_event, populating the content_hash column and the _foray.content_hash sub-block.

Proposed treatment: remove the function when the content_hash column is removed and the _foray injection is gone. There are no other callers in src/loomworks/. (Note: there is a different content_hash function in src/loomworks/storage/object_store.py:150 for file-blob hashing — that one is Loomworks-essential and stays.)

3.5 — stamp_commit_attestation (the surviving user of the attestation column)

File / lines

src/loomworks/engagement/commit_orchestration.py:109-143; called from src/loomworks/api/routers/engagements.py:536 and :632.

What it does: Updates the most-recent engagement-committed event's attestation JSONB column with the verified WebAuthn assertion payload, after the Operator completes the WebAuthn ceremony at commit time.

Status: This is Memory-essential functionality — capturing the Operator's WebAuthn proof of approval on engagement commits. The column name is FORAY-decoration; the work is identity provenance. Do not remove. Rename the column (§3.1) and update this function and its callers to use the new name.

Cross-cutting observations on Phase 25 wiring


Section 4 — Reserved location pattern home

The scoping note proposed _foray_reserved_emit(event_kind, payload) as the reserved location pattern — a no-op function that holds the spot in code where the future FORAY call will eventually go.

Recommendation

Create a new top-level module src/loomworks/foray/ and place the reserved emitter at src/loomworks/foray/reserved.py.

Reasoning

  1. Forward-looking module layout. When actual FORAY integration eventually lands, it will need a home in the codebase. The foray/ module establishes that home now. Today it holds only the reserved emitter and (per §3.2) the priority registry. Over time it accumulates the real integration code.
  2. Discoverable. Anyone scanning src/loomworks/ sees a foray/ directory; the name signals "this is the FORAY integration surface." No confusion with credit/ (which holds value events that happen to have been FORAY-shaped) or audit/ (which holds setting-change events that happen to have been FORAY-shaped). Those modules are about credit and audit; foray/ is about FORAY.
  3. Importable from anywhere. Credit, audit, memory, orchestration, api all currently call FORAY-shaped writers from their own modules. A central from loomworks.foray.reserved import emit works from any of them.
  4. Pairs naturally with the priority registry. The _ANCHOR_PRIORITY registry (renamed per §3.2) belongs in the same module: src/loomworks/foray/priorities.py. The two pieces — "where are the reserved emit points" + "what priority would each event get" — sit alongside each other.

Proposed module shape

src/loomworks/foray/
├── __init__.py                  # exports: emit_reserved, FORAY_RESERVED_EVENT_PRIORITY
├── reserved.py                  # async def emit_reserved(event_kind, payload) → None
├── priorities.py                # FORAY_RESERVED_EVENT_PRIORITY: dict[str, str]
└── README.md                    # documents the reservation pattern and what it isn't

The function itself is intentionally minimal:

# src/loomworks/foray/reserved.py
"""Reserved locations in the engine where FORAY emissions will eventually
happen. Today: no-op. The reservations preserve the design intent in code
so that the eventual integration phase has named locations to wire up.

NOT for use in tests as a mocking hook. NOT a side-channel logger. A
zero-cost code marker.
"""
from __future__ import annotations
from typing import Any


async def emit_reserved(event_kind: str, payload: dict[str, Any]) -> None:
    """No-op reservation. Holds the spot for a future FORAY emit call.

    The eventual implementation will accept the same signature and route
    to whichever FORAY adapter the deployment configures (PostgreSQL via
    dunin7-foray ForayClient today; blockchain when Pluggable Persistence
    Phase 3 lands).
    """
    # Intentionally no-op.
    return None

Alternative considered and set aside: placing the reserved emitter inside src/loomworks/audit/foray_reserved.py. Rejected because the audit module is being narrowed in this cleanup (it currently holds the FORAY-shaped audit.foray_events table that will become a Loomworks-natural settings audit table). Colocating reserved-FORAY material with the narrowed-audit module would conflate the two cleanups.


Section 5 — Things found beyond the kickoff list

Five observations surfaced during the read. None expand the cleanup scope; all are factual flags.

5.1 — The attestation column is the one Phase 25 readiness column that's actually doing work

Per §3.1 and §3.5: content_hash and foray_tx_ref are dead, but attestation carries the Operator's WebAuthn proof on every engagement commit. The scoping note's general framing — "remove the Phase 25 wiring" — is right in direction but the attestation column's content needs to survive under a different name. This is a small clarification but worth catching before any column-drop migration runs.

5.2 — Two downstream readers of credit.foray_action_flows

The credit.conversion_observer (idempotency check for referral credits) and credit.reconciliation_evaluator (drift detection) both READ from credit.foray_action_flows via JSONB-extract metadata queries. Replacement table design (Step 2) needs to keep these consumers in mind — the operational queries become cleaner with typed columns, but the queries themselves have to be rewritten.

5.3 — The update_balance_on_flow trigger is part of the cleanup surface

credit.foray_action_flows has an AFTER INSERT trigger that maintains credit.asset_balances. Replacing the table means replacing or removing the trigger. The replacement is straightforward (a new trigger on the Loomworks-natural credit-event table, or a simpler imperative update at write time) but worth naming because it's not a code-only change — it's a schema-and-trigger change.

5.4 — The "narrative FORAY events" pattern in audit.foray_events was a methodology POC

The engine memory notes record the audit.foray_events ship as the first FORAY-narrative substrate use, distinct from credit's FORAY-value substrate; POC of the pattern, future ships extend to role/identity/workspace event types. The POC was solving the question "can we have one FORAY-narrative table that captures multiple kinds of non-value events?" — with the FORAY-as-external clarification, that question doesn't need a yes/no answer in Loomworks anymore. Three follow-ups that were extending the pattern (role/identity/workspace) can be safely deprioritised once this cleanup lands.

5.5 — Several existing tests rely on the current FORAY-shape

Test files referencing the affected code:

The Phase 25 dedicated test file goes away with the readiness wiring removal. Other tests need to be rewritten to assert against the replacement tables rather than the FORAY-shaped ones. Mechanical work, not redesign.


Section 6 — Estimated work for the cleanup

Code locations to change

Wrong-pattern tableDirect write call sitesHelper functionsIndirect callers (test surface)
credit.foray_action_flows12 ForayActionFlowRow(...) constructor calls in flows.py (11) + proposal_applier.py (1)7 writer helpers + 1 inline write in apply_reconciliation_correction8 test files
audit.foray_events3 call sites (tune_setting × 2, me_settings PUT × 1)1 writer (write_setting_change_event) + 1 local helper4 test files
memory_events Phase 25 wiring1 injection site (inside append_event at events.py:236-255) + 3 column declarations + 1 column writercompute_content_hash, _ANCHOR_PRIORITY registry11 test files (including 1 dedicated to the wiring)

Total cleanup surface: 16 distinct write code locations plus 3 column declarations plus the _foray injection block plus 1 trigger.

Migrations required

Test surgery

New code

Honest complexity assessment

Bottom line

Moderate — bigger than the scoping note's casual framing of "cleanup three wrong-shape tables," but not entangled with downstream logic that would require redesign elsewhere.

The work breaks into three uneven pieces:

Overall: a CR-and-a-half of work. The right shape might be three sibling CRs that can ship independently (audit cleanup first because it's smallest, then memory_events Phase 25 wiring, then credit substrate cleanup as the largest). Each CR is self-contained — no cross-CR dependencies because each table is independent.

What's NOT entangled (worth naming)

What COULD entangle (worth flagging)