CR identifier. CR-2026-XXX (next number in sequence; CR drafter confirms against engine repo docs/phase-crs/ listing before archival).
Version. 0.1
Date. 2026-05-24
Status. Drafted; awaiting Operator approval. No pre-flight Step 0 yet — Step 0 runs at CR execution time.
Author. Claude Code (CR drafter) on direction from Marvin Percival (DUNIN7 Operator).
Phase number. 61 (engine repo docs/phase-crs/ holds CRs through Phase 60; engine tags through phase-59-upload-pathway-completion; Phase 60 CR drafted but not yet executed and tagged. Phase 61 is the next available number.)
Companion documents.
loomworks-cr-a-audit-replacement-design-v0_1.md — design document; this CR's substrate-design source of truth.loomworks-substrate-hygiene-scoping-note-v0_2.md (record substrate/cleanup/) — scoping note that authorized the three-sibling-CR shape.loomworks-foray-integration-locations-v0_1.md (record substrate/cleanup/) — Step 1 inventory the design rests on.loomworks-substrate-review-v0_3.md (record substrate/) — substrate review that surfaced the FORAY-shaped-substrate concern.Baseline. loomworks-engine 2f04e7a (voice listening silence-submit ship). Alembic head 0070. Test count 2673 pytest / 377 vitest at baseline.
The DUNIN7 Loomworks engine carries three substrate surfaces shaped during the FORAY-immersion arc to anticipate FORAY integration — audit.foray_events, credit.foray_action_flows, and memory_events' Phase 25 readiness wiring. The substrate review v0.3 + the inventory v0.1 + the scoping note v0.2 together established:
_foray_reserved_emit) lands grep-able call sites so future FORAY integration has explicit hooks rather than redoing the substrate-shape design.CR-A is the smallest scope — half a day of work — and ships first. It validates the reserved-location pattern at a manageable surface before CR-B and CR-C apply it to the foundational memory-events substrate and the multi-day credit substrate cleanup respectively.
The discovery trajectory: scoping note v0.1 (single-CR framing) → inventory v0.1 (surfaced three-CR honest complexity, surfaced WebAuthn-attestation-column-survives-renamed correction) → scoping note v0.2 (absorbed five corrections) → CR-A design + this CR (executable plan).
Engine-only work. Five substrate changes plus three code-site updates plus four test-file updates plus the new module's foundation:
audit.events (replacement table); copy data from audit.foray_events; drop audit.foray_events. One Alembic migration.src/loomworks/foray/ with __init__.py and reserved_emit.py. Holds the no-op _foray_reserved_emit(event_kind, payload) function. CR-B and CR-C extend this module (CR-B moves _ANCHOR_PRIORITY; CR-C adds value-flow emit support).AuditForayEventRow → AuditEventRow; EVENT_TYPE_SETTING_CHANGE → EVENT_KIND_SETTING_CHANGE; column field names rewritten (event_type → event_kind, timestamp → occurred_at); docstrings drop FORAY framing.tune_setting.py:489 (tune action), tune_setting.py:596 (reset action), me_settings.py:117 (button-click PUT).test_voice_tune_setting_foray_audit.py → test_voice_tune_setting_audit.py) plus SQL/ORM/constant references rewritten. conftest.py:79 TRUNCATE chain updated.The following do not ship in CR-A:
attestation column rename to commit_webauthn_attestation; _ANCHOR_PRIORITY registry relocation; _foray namespace injection removal in memory/events.py:209-220. Separate sibling CR.credit.foray_action_flows replacement; credit.update_balance_on_flow PostgreSQL trigger replacement; 12 constructor sites; conversion observer + reconciliation evaluator query updates. Separate sibling CR._foray_reserved_emit is a no-op today. The actual ForayClient wrapper, real attestation writes, and dunin7-foray SDK adoption are downstream work.rationale TEXT NULL column lands now; populating it requires the helper writer to accept a rationale parameter and the call sites to supply one. That evolution is a separate ship.role_change, identity_change, workspace_operation are referenced in the current audit module docstring but not implemented. CR-A neither implements them nor blocks their future addition.src/loomworks/audit/ keeps its name (it's already Loomworks-shaped). Only the table inside it is renamed.If CC finds itself implementing any of the above, it has crossed a scope boundary — halt and surface per §10.
All decisions trace to the design document (loomworks-cr-a-audit-replacement-design-v0_1.md) and the scoping note v0.2. CC consumes them; does not relitigate.
| Decision | Setting | Source |
|---|---|---|
| D1 | Table name. audit.events (drop foray_ prefix). |
Design §3.1 |
| D2 | Column renames. event_type → event_kind; timestamp → occurred_at. |
Design §3.2 |
| D3 | Columns dropped. tx_id (FORAY concept, never used distinctly); signature (FORAY-readiness, never written). |
Design §3.2 |
| D4 | Column added. rationale TEXT NULL. NULL-default; no historical fill. |
Design §3.2 + §8.2 |
| D5 | Foreign keys. actor_person_id REFERENCES persons(id) RESTRICT; engagement_id REFERENCES engagements(id) RESTRICT. |
Design §3.4 + §8.3 |
| D6 | Indexes kept. actor_ts and kind_ts only. Drop tx_id (column gone), drop engagement_ts (no query exercises it). |
Design §3.3 + §8.5 |
| D7 | PostgreSQL trigger. None. Append-only audit; no projection. | Design §3.5 |
| D8 | Migration shape. One Alembic file (0071_audit_events_replacement.py). Upgrade does create-table + data-copy + drop-old-table atomically. |
Design §4 |
| D9 | Reserved emit placement. Per call site, not centralized. The reserved emit is about the business event, not the audit write. | Design §8.1 |
| D10 | Event-kind argument format. audit.setting_change (namespaced) for the reserved-emit argument; "setting_change" (bare) for the audit row's event_kind column. |
Design §8.4 |
| D11 | Reserved-location module home. src/loomworks/foray/. CR-A creates with _foray_reserved_emit; CR-B and CR-C extend. |
Scoping note v0.2 §"reserved-location module home" + Design §5.1 |
| D12 | Test file rename. test_voice_tune_setting_foray_audit.py → test_voice_tune_setting_audit.py. Filename should not retain FORAY framing post-CR-A. |
Design §6.1 |
| D13 | Reserved-slot count. 2 slots. CR-A is small; one slot covers a design surfacing during implementation, the second covers a test-flake / migration-rollback / FK-violation surprise. | Per Phase 58 v0.5 reservation pattern; scaled to CR-A's small scope |
If a decision feels wrong during build, CC halts per §10 — the chat doesn't override; it surfaces.
0071_audit_events_replacement.pySingle migration file. Revision 0071, revises 0070.
Upgrade.
CREATE TABLE audit.events with columns + defaults + foreign keys per Design §3.2 and §3.4CREATE INDEX ix_audit_events_actor_ts ON audit.events (actor_person_id, occurred_at DESC)CREATE INDEX ix_audit_events_kind_ts ON audit.events (event_kind, occurred_at DESC)sql
INSERT INTO audit.events
(id, event_kind, actor_person_id, engagement_id, payload, rationale, occurred_at)
SELECT id, event_type, actor_person_id, engagement_id, payload, NULL, timestamp
FROM audit.foray_events;DROP INDEX audit.ix_audit_foray_events_engagement_ts (and the other three indexes)DROP TABLE audit.foray_eventsThe audit schema itself is preserved (not dropped + recreated).
Downgrade. Reverse: recreate audit.foray_events, copy back (filling tx_id = id and signature = NULL), drop audit.events.
The only substrate change is the one table replacement. No supplementary tables, no projections, no views.
CR-A is the simplest substrate cleanup of the three. No DDL beyond table + indexes + FKs.
src/loomworks/foray/src/loomworks/foray/__init__.py — re-exports the no-op emitter:
"""FORAY integration reservation.
Today: holds the no-op reserved emitter. When FORAY integration
eventually opens, this module is the home for the actual ForayClient
wrapper and the emitter writes real attestations.
CR-B will move the `_ANCHOR_PRIORITY` registry into this module.
CR-C will extend with value-flow emit support.
"""
from loomworks.foray.reserved_emit import _foray_reserved_emit
__all__ = ["_foray_reserved_emit"]
src/loomworks/foray/reserved_emit.py — the no-op function with the comment-marker contract documented in the docstring (see Design §5.1).
src/loomworks/audit/models.pyAuditForayEventRow → AuditEventRow__tablename__ = "events" (was "foray_events")tx_id, signature columnsevent_type → event_kind, timestamp → occurred_atrationale: Mapped[Optional[str]] = mapped_column(String(), nullable=True)actor_person_id → persons.id (RESTRICT)engagement_id → engagements.id (RESTRICT)src/loomworks/audit/events.pyEVENT_TYPE_SETTING_CHANGE → EVENT_KIND_SETTING_CHANGEAuditEventRow (renamed)tx_id from row construction (column gone)signature=None from row construction (column gone)timestamp=now → occurred_at=nowwrite_setting_change_event keeps its name and signature; the returned value is now row.id (which was equivalent to tx_id before)src/loomworks/audit/__init__.pyUpdate re-exports:
from loomworks.audit.events import write_setting_change_event, EVENT_KIND_SETTING_CHANGE
from loomworks.audit.models import AuditEventRow
__all__ = [
"AuditEventRow",
"EVENT_KIND_SETTING_CHANGE",
"write_setting_change_event",
]
src/loomworks/orchestration/tune_setting.py:489Add reserved-emit after _audit_setting_change returns on the ACTION_TUNED branch:
await _audit_setting_change(...)
# FORAY_RESERVED_LOCATION: audit.setting_change
_foray_reserved_emit(
"audit.setting_change",
{"setting_key": setting_key, "action": "tuned"},
)
Import: from loomworks.foray import _foray_reserved_emit.
src/loomworks/orchestration/tune_setting.py:596Same shape on the ACTION_RESET branch, with "action": "reset" in the payload.
src/loomworks/api/routers/me_settings.py:117Add reserved-emit after the try/except wrapping write_setting_change_event:
try:
await write_setting_change_event(...)
except SQLAlchemyError:
logger.exception(...)
# FORAY_RESERVED_LOCATION: audit.setting_change
_foray_reserved_emit(
"audit.setting_change",
{"setting_key": setting_key, "action": "tuned_button"},
)
Import: from loomworks.foray import _foray_reserved_emit.
The reserved emit is outside the try/except — the audit write's swallow discipline is independent of the reserved-emit hook.
tests/test_voice_tune_setting_foray_audit.pyRename to tests/test_voice_tune_setting_audit.py. Inside:
FROM audit.foray_events → FROM audit.eventsevent_type column references with event_kindtimestamp column references with occurred_at in SQL stringsAuditForayEventRow import + uses with AuditEventRowEVENT_TYPE_SETTING_CHANGE with EVENT_KIND_SETTING_CHANGEtests/test_voice_silence_submit_setting.pyLine 437 SQL string update: FROM audit.foray_events → FROM audit.events; event_type → event_kind.
tests/test_voice_person_settings.pyLine 283 SQL string update: FROM audit.foray_events → FROM audit.events; column renames as applicable.
tests/conftest.py:79TRUNCATE chain update: replace audit.foray_events with audit.events.
The reserved-emit function is a no-op; over-testing a no-op wastes test budget. The function's existence and callability are covered transitively by the three call sites' existing tests (which still pass once the call sites are wired). One single import-level assertion suffices, added to the renamed test_voice_tune_setting_audit.py:
def test_foray_reserved_emit_is_importable():
from loomworks.foray import _foray_reserved_emit
_foray_reserved_emit("audit.setting_change", {"key": "value"}) # no-op
alembic upgrade head runs 0071. Inside the migration, the order is:
audit.events (columns + FKs)audit.eventsaudit.foray_events → audit.eventsaudit.foray_eventsaudit.foray_eventsAll in one transaction. If the data copy fails (e.g., FK violation on a stale row referencing a person that no longer exists), the migration aborts and the original table is preserved.
Before running the migration, CC verifies no audit.foray_events rows reference invalid persons or engagements:
-- Should return 0
SELECT COUNT(*) FROM audit.foray_events fe
LEFT JOIN persons p ON p.id = fe.actor_person_id
WHERE p.id IS NULL;
SELECT COUNT(*) FROM audit.foray_events fe
LEFT JOIN engagements e ON e.id = fe.engagement_id
WHERE fe.engagement_id IS NOT NULL AND e.id IS NULL;
If either returns non-zero, CC halts and surfaces — orphan rows need a decision (delete? backfill? preserve as historical with FK relaxed?).
Three rollback paths, ordered preference:
alembic downgrade -1 runs 0071 downgrade — recreates audit.foray_events, copies back, drops audit.events. Suitable for dev iteration.DROP TABLE audit.events and the migration is back at pre-0071 state. CC reports the failure and the Operator decides next steps.The migration's atomicity (single transaction) means partial states should not persist on failure.
CC executes the following sequence, gated by checkpoints.
loomworks-engine main; confirm HEAD matches baseline 2f04e7a (or note advance and reaffirm test-count baseline ±2).0070..venv/bin/pytest -q; record count (expected ~2673).audit.foray_events schema matches migration 0068 (no later changes)
- Three call sites still at the inventory-noted lines (or close to them — line numbers may have drifted; verify by content)
- src/loomworks/foray/ does not existphase-61-audit-cleanup from main.If any check fails, CC halts and surfaces.
Write migrations/versions/0071_audit_events_replacement.py with upgrade + downgrade per §7.1. Do not run yet.
src/loomworks/foray/ moduleCreate src/loomworks/foray/__init__.py and src/loomworks/foray/reserved_emit.py per §5.1. Add a single import-level test as in §6.5.
Run .venv/bin/pytest tests/test_voice_tune_setting_foray_audit.py -q — expected to still pass on the old table (no model changes yet).
Update src/loomworks/audit/models.py, src/loomworks/audit/events.py, src/loomworks/audit/__init__.py per §5.2-§5.4. Module imports still reference the old constant names will break — that's expected; Step 4 fixes them.
Update the three call sites per §5.5-§5.7. Each adds:
- The reserved-emit import
- The # FORAY_RESERVED_LOCATION: audit.setting_change comment marker
- The _foray_reserved_emit("audit.setting_change", {...}) call
Update the constant references in the same files (any references to EVENT_TYPE_SETTING_CHANGE become EVENT_KIND_SETTING_CHANGE).
.venv/bin/alembic upgrade head — runs 0071, creating audit.events, copying data, dropping audit.foray_events.
Verify:
- audit.events exists with the expected schema (\d audit.events in psql)
- Row count in audit.events matches what was in audit.foray_events before
- audit.foray_events no longer exists
Rename tests/test_voice_tune_setting_foray_audit.py to tests/test_voice_tune_setting_audit.py. Update its contents per §6.1.
Update tests/test_voice_silence_submit_setting.py (§6.2), tests/test_voice_person_settings.py (§6.3), tests/conftest.py (§6.4).
Run full test suite: .venv/bin/pytest -q. Expected count: 2673 +/- a small delta from the new import-level test (~2674).
If tests fail, CC halts and surfaces.
Search the engine repo for remaining audit.foray_events and AuditForayEventRow references in docs/comments. Update or remove as appropriate.
grep -rn "audit\.foray_events\|AuditForayEventRow\|EVENT_TYPE_SETTING_CHANGE" --include="*.py" --include="*.md" .
Commit chain (3-5 commits):
CC reports back:
- All 8 steps complete
- Test count: NNNN (expected ~2674)
- Migration 0071 applied; schema verified
- No remaining audit.foray_events references in code
- Branch ready for tag
Operator reviews; either approves close or requests adjustments.
On Operator approval:
phase-61-audit-cleanup at the branch HEADAt Checkpoint A, all of the following must be true:
audit.events table exists with the schema in Design §3.2audit.foray_events table does not existaudit.events row count equals the pre-migration audit.foray_events row countaudit.events rows have event_kind = "setting_change" (today's only event kind)audit.events rows have non-null actor_person_id pointing to a real persons rowengagement_id values point to a real engagements rowsrc/loomworks/foray/ module exists with _foray_reserved_emit function# FORAY_RESERVED_LOCATION: audit.setting_change comment markers and _foray_reserved_emit(...) calls.venv/bin/pytest -q passes with no failuresgrep -rn "audit\.foray_events" src/ tests/ returns only docstring references that have been intentionally preserved (if any)grep -rn "AuditForayEventRow" . returns no matches0071CR-A is the smallest of the three sibling CRs and the lowest-risk. Risks and mitigations:
Risk. If audit.foray_events has a row with actor_person_id referencing a person that has since been deleted (shouldn't happen, but could from earlier test runs in a non-clean DB), the new FK creation fails.
Mitigation. Step 0 pre-flight runs the orphan check (§7.2). If non-zero, CC halts before running the migration.
Risk. The known follow-on test_db_sequence_overflow_followon (engagement_display_identifier_seq overflow in conftest TRUNCATE chain) might surface again if conftest changes interact poorly.
Mitigation. The conftest change is single-string substitution (audit.foray_events → audit.events). Should not affect the sequence-overflow path. If a test-DB issue surfaces, follow the known workaround (ALTER SEQUENCE ... RESTART WITH 1).
AuditForayEventRow outside the audit moduleRisk. Some test or utility imports AuditForayEventRow and is missed by the grep sweep.
Mitigation. Step 7 documentation sweep includes a project-wide grep. Test failures in Step 6 would surface any missed import.
Risk. A future code change deletes or modifies the # FORAY_RESERVED_LOCATION: comment marker without removing the call.
Mitigation. The marker is a convention, not a contract. The function call itself is the source of truth; the marker is a grep aid. Future CC sessions can rely on grep _foray_reserved_emit to find call sites. The marker is supplementary.
Risk. During Step 4, CC notices that three near-identical emits feel like ceremony and is tempted to centralize.
Mitigation. D9 prescribes per-call-site. If CC believes centralization is right, halt and surface — design v0.2 may absorb. Don't centralize unilaterally.
Risk. Mild; the file rename in Step 6 disconnects line-by-line blame from the original file.
Mitigation. Use git mv so the rename is detected as a rename (preserves blame chain) rather than a delete+create.
Risk. A surprise consumes more than the 2 reserved slots.
Mitigation. Per D13, CC halts after the second slot is consumed and surfaces for v0.2 absorption rather than continuing unilaterally.
What CR-A leaves for CR-B and CR-C:
src/loomworks/foray/ module is created and validated. CR-B extends it by moving the _ANCHOR_PRIORITY registry into the module (possibly renamed _FORAY_INTEGRATION_PRIORITY).src/loomworks/memory/events.py injection point.attestation column on memory_events to commit_webauthn_attestation.src/loomworks/foray/ module is created and validated. CR-C extends with value-flow emit support.rationale column population. Lands as NULL on historical rows; the helper writer doesn't accept a rationale parameter today. Future ship adds the parameter and threads it through call sites.audit.events. Re-add when a query surface needs it.role_change, etc.). Mentioned in original docstrings; not implemented. The table is structurally ready; the implementation lands when the surface needs it.The reserved-location pattern's first application surfaced no design changes. If CR-B or CR-C surfaces a pattern adjustment, it's worth a methodology candidate at the next consolidation pass.
The per-call-site-vs-centralized question (D9) is settled per the scoping note; if subsequent CRs find the choice felt wrong in practice, that's also a methodology candidate.
This CR is drafted by Claude Code on 2026-05-24 from:
substrate/cleanup/loomworks-substrate-hygiene-scoping-note-v0_2.md)substrate/cleanup/loomworks-foray-integration-locations-v0_1.md)substrate/loomworks-substrate-review-v0_3.html)loomworks-cr-a-audit-replacement-design-v0_1.md)2f04e7a: migration 0068, src/loomworks/audit/{events,models,__init__}.py, src/loomworks/orchestration/tune_setting.py, src/loomworks/api/routers/me_settings.py, four test filesdocs/phase-crs/phase-59-cr-upload-pathway-completion-v0_1.md) as template referenceCR archival at engine repo docs/phase-crs/phase-61-cr-audit-cleanup-v0_1.md happens at Step 0 pre-flight of CR execution.
No CR-execution work begins until the Operator approves both this CR and the companion design document.
DUNIN7 — Done In Seven LLC — Miami, Florida Phase 61 Change Request — Audit cleanup — v0.1 — 2026-05-24