DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path substrate/cleanup/phase-61-cr-audit-cleanup-v0_1.md

Phase 61 Change Request — Audit cleanup (CR-A of substrate hygiene)

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.

Baseline. loomworks-engine 2f04e7a (voice listening silence-submit ship). Alembic head 0070. Test count 2673 pytest / 377 vitest at baseline.


1. Background and motivation

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:

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).


2. Scope

2.1 In-scope

Engine-only work. Five substrate changes plus three code-site updates plus four test-file updates plus the new module's foundation:

2.2 Out-of-scope

The following do not ship in CR-A:

If CC finds itself implementing any of the above, it has crossed a scope boundary — halt and surface per §10.


3. Architectural decisions

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_typeevent_kind; timestampoccurred_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.pytest_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.


4. Substrate changes

4.1 Alembic migration 0071_audit_events_replacement.py

Single migration file. Revision 0071, revises 0070.

Upgrade.

  1. CREATE TABLE audit.events with columns + defaults + foreign keys per Design §3.2 and §3.4
  2. CREATE INDEX ix_audit_events_actor_ts ON audit.events (actor_person_id, occurred_at DESC)
  3. CREATE INDEX ix_audit_events_kind_ts ON audit.events (event_kind, occurred_at DESC)
  4. Data copy:

```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; ```

  1. DROP INDEX audit.ix_audit_foray_events_engagement_ts (and the other three indexes)
  2. DROP TABLE audit.foray_events

The 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.

4.2 No new tables outside the migration.

The only substrate change is the one table replacement. No supplementary tables, no projections, no views.

4.3 No PostgreSQL functions or triggers.

CR-A is the simplest substrate cleanup of the three. No DDL beyond table + indexes + FKs.


5. Code changes

5.1 New module — 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).

5.2 src/loomworks/audit/models.py

5.3 src/loomworks/audit/events.py

5.4 src/loomworks/audit/__init__.py

Update 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",
]

5.5 Call site 1 — src/loomworks/orchestration/tune_setting.py:489

Add 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.

5.6 Call site 2 — src/loomworks/orchestration/tune_setting.py:596

Same shape on the ACTION_RESET branch, with "action": "reset" in the payload.

5.7 Call site 3 — src/loomworks/api/routers/me_settings.py:117

Add 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.


6. Test changes

6.1 tests/test_voice_tune_setting_foray_audit.py

Rename to tests/test_voice_tune_setting_audit.py. Inside:

6.2 tests/test_voice_silence_submit_setting.py

Line 437 SQL string update: FROM audit.foray_eventsFROM audit.events; event_typeevent_kind.

6.3 tests/test_voice_person_settings.py

Line 283 SQL string update: FROM audit.foray_eventsFROM audit.events; column renames as applicable.

6.4 tests/conftest.py:79

TRUNCATE chain update: replace audit.foray_events with audit.events.

6.5 No new test files

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

7. Migration plan (operational specifics)

7.1 Forward migration

alembic upgrade head runs 0071. Inside the migration, the order is:

  1. Create audit.events (columns + FKs)
  2. Create indexes on audit.events
  3. Data copy audit.foray_eventsaudit.events
  4. Drop indexes on audit.foray_events
  5. Drop audit.foray_events

All 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.

7.2 Pre-migration validation (Step 0 pre-flight)

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?).

7.3 Rollback plan

Three rollback paths, ordered preference:

  1. Fail forward. Most production rollbacks ship a new migration that undoes the change. Preferred for any state past CR-A close.
  2. Alembic downgrade. alembic downgrade -1 runs 0071 downgrade — recreates audit.foray_events, copies back, drops audit.events. Suitable for dev iteration.
  3. Manual. If something goes wrong mid-migration (e.g., the data copy step fails after the new table exists), 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.


8. Order of operations

CC executes the following sequence, gated by checkpoints.

Step 0 — Pre-flight

  1. Pull loomworks-engine main; confirm HEAD matches baseline 2f04e7a (or note advance and reaffirm test-count baseline ±2).
  2. Confirm Alembic head is 0070.
  3. Run baseline test suite: .venv/bin/pytest -q; record count (expected ~2673).
  4. Verify the inventory's findings still hold:
  1. Pre-migration orphan-row check (§7.2). Expected: 0 orphans.
  2. Create branch phase-61-audit-cleanup from main.

If any check fails, CC halts and surfaces.

Step 1 — Migration scaffold

Write migrations/versions/0071_audit_events_replacement.py with upgrade + downgrade per §7.1. Do not run yet.

Step 2 — Create src/loomworks/foray/ module

Create 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).

Step 3 — Audit module rename + update

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.

Step 4 — Call-site updates

Update the three call sites per §5.5-§5.7. Each adds:

Update the constant references in the same files (any references to EVENT_TYPE_SETTING_CHANGE become EVENT_KIND_SETTING_CHANGE).

Step 5 — Run migration

.venv/bin/alembic upgrade head — runs 0071, creating audit.events, copying data, dropping audit.foray_events.

Verify:

Step 6 — Test updates

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.

Step 7 — Documentation sweep

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" .

Step 8 — Commit chain

Commit chain (3-5 commits):

  1. "Create src/loomworks/foray/ module with reserved-emit hook"
  2. "Rename audit.foray_events → audit.events; drop FORAY-shaped columns; add rationale + FKs"
  3. "Add _foray_reserved_emit at three audit call sites"
  4. "Update tests for audit.events rename"
  5. (optional) "Documentation sweep — drop FORAY framing from audit module docstrings"

Checkpoint A — Operator confirms

CC reports back:

Operator reviews; either approves close or requests adjustments.

Close — Tag and merge

On Operator approval:

  1. Tag phase-61-audit-cleanup at the branch HEAD
  2. Push branch + tag to origin
  3. Merge to main (or open PR per operator's branch-protection preference)
  4. Update memory + record per standing discipline

9. Acceptance criteria

At Checkpoint A, all of the following must be true:


10. Risk assessment

CR-A is the smallest of the three sibling CRs and the lowest-risk. Risks and mitigations:

10.1 Migration-time orphan rows

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.

10.2 Test-DB sequence overflow

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_eventsaudit.events). Should not affect the sequence-overflow path. If a test-DB issue surfaces, follow the known workaround (ALTER SEQUENCE ... RESTART WITH 1).

10.3 Hidden reference to AuditForayEventRow outside the audit module

Risk. 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.

10.4 Reserved-emit comment marker drift

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.

10.5 Per-call-site reserved emit feels redundant

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.

10.6 Renaming the test file breaks git history blame

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.

10.7 Reserved-slot budget

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.


11. Carry-forward

What CR-A leaves for CR-B and CR-C:

To CR-B (Phase 25 wiring cleanup)

To CR-C (credit substrate cleanup)

Deferred from CR-A explicitly

Methodology notes

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.


12. Document trailer

This CR is drafted by Claude Code on 2026-05-24 from:

CR 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