Version. 0.1
Date. 2026-05-25
Author. Claude Code
Engine HEAD. 573abe1 (merge of phase-63-credit-substrate-cleanup)
Tag. phase-63-credit-substrate-cleanup
Companion documents.
phase-63-cr-credit-substrate-cleanup-v0_1.md — the CR these notes close.loomworks-cr-c-credit-substrate-cleanup-design-v0_1.md — the design these notes execute.phase-61-implementation-notes-v0_1.md + phase-62-implementation-notes-v0_1.md — sibling implementation notes from CR-A and CR-B.loomworks-substrate-hygiene-scoping-note-v0_2.md — the scoping note that authorized the three-sibling-CR shape; this document closes its work.loomworks-foray-integration-locations-v0_1.md — the Step 1 inventory that fed the scoping note's corrections.CR-C is the third and final sibling Change Request from the substrate hygiene scoping note v0.2. When CR-C closes — with this commit — Phase A of the development assessment is complete. The FORAY-shape substrate hygiene cleanup arc is done.
These implementation notes serve a dual purpose:
§9 carries the arc-closure summary. §§1-8 cover CR-C specifically.
Phase 63 closed in five commits on the engine, merged as 573abe1:
64516f5 — Migration 0073 (in-place transform) + CreditFlowRow ORM renamefa26163 — Refactor three downstream readers to typed-column predicates7a13132 — Update 12 constructor sites with typed columns + reserved-emit (includes inline reader 3 refactor)c894f9b — Documentation sweep: credit.foray_action_flows → credit.flowsbad7ea7 — Update tests for credit.flows rename and typed-column queriesThe cleanup proceeds as the scoping note v0.2 anticipated: CR-A audit cleanup (smallest, validates pattern) → CR-B memory-events Phase 25 wiring (scale-down to 1 site) → CR-C credit substrate (scale-up to 12 sites, the architectural test).
credit.foray_action_flows no longer exists. credit.flows carries the 2 historical rows (preserved through ALTER TABLE ... RENAME) plus the new typed-column schema:
| Column | Type | Status |
|---|---|---|
| id | UUID PK | unchanged |
| transaction_id | UUID NOT NULL | unchanged (actually groups multi-row transactions) |
| event_kind | VARCHAR(64) NOT NULL | NEW — 9-value taxonomy (issuance/consumption_token/consumption_credit/suspension/reactivation/deletion/balance_zeroing/referral_credit/corrective; legacy_unknown for historical rows that fall outside) |
| asset_id | VARCHAR(64) NOT NULL | unchanged |
| quantity | BigInt NOT NULL | unchanged |
| from_party | VARCHAR(128) NOT NULL | unchanged |
| to_party | VARCHAR(128) NOT NULL | unchanged |
| occurred_at | TIMESTAMPTZ NOT NULL | renamed from timestamp |
| turn_event_id | UUID NULL | NEW — extracted from JSONB for consumption flows |
| converted_person_id | UUID NULL | NEW — extracted from JSONB for referral_credit flows |
| extra_metadata | JSONB NOT NULL | renamed from metadata (retires SQLAlchemy reserved-name workaround) |
Two new indexes serve the typed-column reader queries:
ix_credit_flows_kind_converted_person — for the conversion-observer + write_referral_credit_flow idempotency queriesix_credit_flows_kind_turn_event — for the reconciliation-evaluator drift query
src/loomworks/credit/models.py: class renamed ForayActionFlowRow → CreditFlowRow. ORM model carries the three new typed columns; extra_metadata direct mapping (no SQLAlchemy reserved-name workaround); AssetBalanceRow.last_flow_id FK updated to credit.flows.id (intra-credit, same DeclarativeBase — survived the rename transparently).
src/loomworks/credit/flows.py: 11 constructor sites updated. Each now:
CreditFlowRow(...) with explicit event_kind=<value> kwargturn_event_id for consumption flows; converted_person_id for referral_creditextra_metadata carries residual fields after typed-column extraction_foray_reserved_emit("credit.<event_kind>", payload) immediately after db.add(...)# FORAY_RESERVED_LOCATION: credit.<event_kind> comment marker
src/loomworks/credit/proposal_applier.py:399: 12th constructor site updated identically with event_kind="corrective".
Three downstream readers refactored from JSONB-extract to typed-column predicates:
conversion_observer._conversion_credit_already_written — replaces flow_metadata["event"].astext == "referral_credit" AND flow_metadata["converted_person_id"].astext == str(uuid) with event_kind == "referral_credit" AND converted_person_id == uuidreconciliation_evaluator._sum_recorded_consumption_for_turn — adds explicit event_kind == "consumption_credit" filter; replaces JSONB-extract on turn_event_id with typed-column predicatewrite_referral_credit_flow inline — same pattern as conversion_observer (Reader 1 logic appears here in the writer's idempotency check)All three readers now indexable. The architectural upside the design promised: queries become readable and indexable; the typed columns make the identification explicit.
| Metric | CR-B baseline (ead29a8) | Final (CR-C close) |
|---|---|---|
| Collected | 2705 | 2708 (+3) |
| Passed | 2670 | 2673 |
| Skipped | 33 | 33 |
| Failed | 2 (pre-existing) | 2 (same two) |
The +3 is test_foray_reserved_emit_for_credit.py (issuance + referral_credit + suspension smoke tests covering the per-call-site discipline at the writer surface).
Pre-existing failures unchanged: test_api_assertion_retract (JSON-encoding bug at app.py:461) + test_phase_44_evaluator::test_concern_scan_does_not_re_fire_within_dedup_window. Both confirmed against main baseline; both unrelated to CR-C.
8 test files updated mechanically:
tests/conftest.py (TRUNCATE chain table name)tests/test_phase_47_credit_substrate.py (21 construct sites + event_kind kwarg added to each)tests/test_phase_48_reconciliation_evaluator.py (raw-SQL INSERT helper updated to new column shape)tests/test_phase_48_conversion_detection.py (29 references — class rename + JSONB-extract → typed-column predicates)tests/test_phase_49_near_exhaustion.py (1 construct site)tests/test_phase_49_proposal_applier.py (table name + 2 raw-SQL JSONB-extract queries)tests/test_phase_49b_amendment.py (table name + raw-SQL column name)tests/test_phase_50_conversion_credit_override.py (17 references — class rename + JSONB-extract → typed-column predicates)
The kickoff §2 framed trigger preservation as "the main unknown going in." This was the load-bearing claim of CR-C's in-place RENAME strategy: PostgreSQL's ALTER TABLE ... RENAME is metadata-only; the trigger binds to the table OID rather than the table name; trigger body references column names CR-C does not change.
After Step 2 (migration applied), synthetic-insert verification:
Test setup:
test_trigger_d28a8a05 (random UUID-suffix)test_asset_<random>
Synthetic INSERT executed against credit.flows (the renamed table):
INSERT INTO credit.flows
(id, transaction_id, event_kind, asset_id, quantity,
from_party, to_party, occurred_at, extra_metadata)
VALUES (..., 'issuance', test_asset, 100,
'dunin7', test_party, now(), '{}'::jsonb);
Result observed in credit.asset_balances:
| party_id | balance | last_flow_id |
|---|---|---|
| dunin7 | -100 | a327ea09-4ee1-... |
| test_trigger_d28a8a05 | +100 | a327ea09-4ee1-... |
The trigger fired. Both sides of the double-entry maintained atomically. The trigger's binding to the table OID survived the ALTER TABLE ... RENAME transparently. The trigger body's NEW.to_party, NEW.from_party, NEW.asset_id, NEW.quantity, NEW.id column references all resolved correctly against the renamed table's columns (none of which were touched by CR-C).
Decision D3 + D8 validated empirically. The "don't touch the trigger" call was correct. The "rename in place rather than drop-and-recreate" call was correct. This was CR-C's load-bearing architectural bet and it paid off.
legacy_unknown row count
The 9-value event_kind taxonomy plus legacy_unknown fallback covers all production patterns the design anticipated. The backfill query:
UPDATE credit.flows
SET event_kind = COALESCE(
extra_metadata->>'event',
CASE
WHEN extra_metadata->>'pipeline_stage' IN ('classify','respond')
THEN 'consumption_token'
WHEN extra_metadata->>'pipeline_stage' = 'credit_debit'
THEN 'consumption_credit'
WHEN extra_metadata ? 'grant_id'
THEN 'issuance'
WHEN extra_metadata ? 'shape_event_id'
THEN 'corrective'
ELSE 'legacy_unknown'
END
);
Dev DB result: 2 rows → both issuance (both had grant_id in metadata). legacy_unknown count: 0.
Production data shape: unknown at CR-C close time. When CR-C's migration runs against production, the legacy_unknown count will be empirically established. If it's non-zero, that's not a problem — legacy_unknown is a designated bucket and application code never writes it (new rows always carry an explicit event_kind). Future analytics or compliance work can reclassify if needed.
Recommendation for the Operator: when the migration runs against production, capture the post-migration distribution:
SELECT event_kind, COUNT(*) FROM credit.flows GROUP BY 1 ORDER BY 2 DESC;
The result will be small and informative. Worth recording in production-deploy notes when that ship happens.
Both slots were consumed by execution-time discoveries that the design didn't fully anticipate. They are evidence for Principle 4 (inventory enumeration vs cognitive density per reference) of the methodology candidates the arc surfaced.
The design anticipated test scope expansion (CR-B's slot 1 surfaced 4 phase-specific tests beyond the inventory's "10 incidental"). For CR-C, the test surface count was correct (8 files) but the per-file density produced four discrete discoveries:
Discovery 4.1.1 — Multi-line JSONB predicate the sed-script's regex didn't catch.
The sed-script regex matched CreditFlowRow.extra_metadata["event"].astext\s\n\s== "value" patterns. Inside test_phase_48_conversion_detection.py (lines 486-489), one predicate was split across more lines than the regex anticipated:
CreditFlowRow.extra_metadata[
"converted_person_id"
].astext
== str(referee.id),
Required manual edit. Lesson: Mechanical sed-sweeps work for ~85% of patterns; the remaining 15% need manual review. The cognitive cost is in the review, not the editing.
Discovery 4.1.2 — UUID/str comparison after typed-column conversion.
Test assertion at test_phase_48_conversion_detection.py:535 compared converted_person_id values as set against a set of string UUIDs:
converted_ids = {f[0].converted_person_id for f in flows}
assert converted_ids == {str(referee_a.id), str(referee_b.id)}
After CR-C, f[0].converted_person_id is a UUID object (typed column), not a string. The comparison failed because {UUID(x)} != {str(x)}. Fix: drop the str() calls on the RHS.
Lesson: Typed-column conversions change Python-side type semantics in ways the JSONB-extract version's .astext predicate hid. Tests that compared f.extra_metadata["x"].astext with str(other) were comparing string-to-string. Tests that compare f.typed_column need to compare UUID-to-UUID. The semantic shift is real and tests must adapt.
Discovery 4.1.3 — Two missed raw-SQL metadata->> queries in test_phase_49_proposal_applier.py.
The design §6.3 explicitly called out the test_phase_48_reconciliation_evaluator.py:128 raw-SQL INSERT helper (CR-B's slot-2 finding equivalent). It did not enumerate raw-SQL read queries in other test files. test_phase_49_proposal_applier.py had two WHERE metadata->>'source_proposal_id' = :pid queries at lines 354 and 471 that needed updating to WHERE extra_metadata->>'source_proposal_id'.
These were trivial sed-fixable once found, but the design's enumeration didn't surface them.
Lesson: "Raw-SQL helpers" in the design's scope-warning grep ≠ all "raw-SQL queries against the affected table." Read sites are as important as write sites for the grep enumeration.
Discovery 4.1.4 — Mangled import in test_phase_48_conversion_detection.py:39.
The pre-CR-C import was:
from loomworks.credit.flows import write_issuance_flow
from loomworks.credit.models import ForayActionFlowRow
Two separate lines. The test-sweep sed script's pattern from loomworks.credit.flows import CreditFlowRow → from loomworks.credit.models import CreditFlowRow collapsed two related lines incorrectly, producing:
from loomworks.credit.models import CreditFlowRow, write_issuance_flow
But write_issuance_flow lives in loomworks.credit.flows, not loomworks.credit.models. The import error surfaced at test collection time.
Lesson: Multi-line import re-arrangements need more careful sed patterns. The fix was to split the import back into two lines, one per module.
write_referral_credit_flow
Design §3 D9 said "the 3rd reader is inside the writer." In execution, refactoring this required interleaving the typed-column predicate refactor (the reader part) with the writer's typed-column kwargs addition (the writer part). The two changes couldn't be cleanly separated — the function's idempotency check uses the same converted_person_id value the writer later passes to the new row.
The result is one larger function rewrite (one commit, function body coherent) rather than two separable edits (reader commit + writer commit). The final code is cleaner — the function reads as a single coherent flow rather than two awkwardly-bridged sections.
The cost was cognitive overhead beyond the per-site estimate the design anticipated. No halt produced. The interleaving was working with the function's existing structure, not against it.
Lesson: When a "reader" and "writer" are interleaved in one function, refactor them together rather than trying to separate by site. The cognitive overhead is upfront; the resulting code is cleaner.
Per the CR §10.7 reservation, two slots were the budget. Both consumed. No third surprise emerged. The reserved-slot discipline held.
The CR-A → CR-B → CR-C arc validated several patterns. After three CRs, the patterns have enough evidence to lift to methodology canon at the next consolidation pass.
| CR | Sites | Emits | Held? | |---|---|---|---| | CR-A audit | 3 | 3 | yes | | CR-B memory | 1 | 1 | yes | | CR-C credit | 12 | 12 | yes |
Three-instance evidence across the full range Loomworks will encounter. Pattern is settled. Ready for methodology canon lift.
| Instance | Application | Form |
|---|---|---|
| 1 | CR-A: rename test_voice_tune_setting_foray_audit.py → test_voice_tune_setting_audit.py | Positive (rename) |
| 2 | CR-B: rename test_phase_25_commit_attestation.py → test_commit_webauthn_attestation.py | Positive (rename) |
| 3 | CR-B: delete test_phase_25_content_hash_and_foray.py outright (both framing and tested functionality gone) | Positive (delete) |
| 4 | CR-C: no renames needed because credit test files were already named after their phase rather than after the FORAY framing | Negative (no-op) |
Instance 4 is the negative case — the principle correctly predicted "no renames needed here" because the credit test files (test_phase_47_credit_substrate.py, etc.) describe their phase-origin which remains historically correct. The principle holds in both directions: rename or delete when filename framing references a cleaned-up origin; leave alone when the existing name still describes what the file tests.
The negative case is methodologically important — a principle that only predicts positive actions is less useful than one that predicts both. Principle 2 is now validated in both forms. Ready for methodology canon lift.
| CR | Strategy | Why |
|---|---|---|
| CR-A audit | Drop-and-recreate | Independent table, no FK to other rows, no trigger |
| CR-B memory | In-place ALTER COLUMN | One column rename within an existing table; FK to engagements survives |
| CR-C credit | In-place ALTER TABLE + ALTER COLUMN | Trigger AND FK on the table; both survive transparent rename |
The rule emerging: rename when downstream bindings exist (trigger, FK, indexes); recreate when independent. Three-instance evidence. Ready for methodology canon lift.
| CR | Inventory accuracy | Per-reference density | |---|---|---| | CR-A | Exact (3 sites, 4 test files) | Matched inventory | | CR-B | Undercounted by 4 (test functions inside "incidental" files testing the wiring directly) | Significantly higher per-reference work | | CR-C | Exact count (8 files) | Four execution-time discoveries within those files (§4) |
The pattern: inventories enumerate references but undercount the cognitive density per reference. A grep that returns "X test files" doesn't capture (a) the per-file work density, (b) the multi-line patterns that mechanical sed misses, (c) the type-shift semantics after typed-column conversion, (d) the raw-SQL queries beyond the obvious INSERT helpers. Three-instance evidence. Ready for methodology canon lift.
Held cleanly. The ALTER TABLE ... RENAME + ALTER COLUMN ... RENAME operations preserved every downstream binding (trigger, FK, indexes) transparently. The migration is 158 lines (vs CR-B's ~85 for column-rename-with-drops), but operational complexity is lower because the in-place transform avoids the drop-and-recreate ceremony entirely.
The trigger continuity verification (§2) was the moment that locked this decision in.
Validated empirically. The synthetic-insert test confirmed the trigger fires correctly on the renamed table. The trigger body's column references (NEW.id, NEW.to_party, NEW.from_party, NEW.asset_id, NEW.quantity) all resolved against the renamed table's columns, none of which were touched by CR-C.
The kickoff §2 framed the trigger as "the main unknown going in." In execution, it was the smoothest piece — exactly because the design's call to leave it alone was correct.
event_kind taxonomy + legacy_unknown fallback
Held cleanly in dev. The 2 dev rows had grant_id in metadata; both backfilled as issuance. legacy_unknown count: 0 in dev. Production may produce some legacy_unknown rows; the design accepts this as a designated bucket.
Validated. The third reader inside write_referral_credit_flow was correctly identified at design time. In execution, refactoring it produced slot-2 consumption (interleaving with the writer) but no halt. The interleaving produced cleaner final code; design D9 (count the inline reader explicitly) was correct.
Held cleanly. Twelve 5-line emit blocks across two files did not feel like ceremony. The pattern (one emit per business event) read naturally even at scale-up. The discipline scales 3 → 1 → 12 without friction. Pattern settled. See §5.1.
When FORAY anchoring eventually opens:
audit.events, memory_events, and credit.flows are all clean Loomworks-shaped tables — no FORAY framing in the substrate_foray_reserved_emit call sites with the # FORAY_RESERVED_LOCATION: <namespace>.<event_kind> marker convention: 3 in audit (CR-A) + 1 in memory (CR-B) + 12 in credit (CR-C). When integration opens, the hook becomes the real attestation write.src/loomworks/foray/ module foundation: _foray_reserved_emit (no-op today) + _ANCHOR_PRIORITY registry (19 entries from CR-B). Future FORAY integration adds the ForayClient wrapper here.memory_events carries the commit_webauthn_attestation typed column (CR-B's rename of the WebAuthn proof column) — Phase C's Memory work inherits this cleanly._ANCHOR_PRIORITY registry has 19 entries today; Phase C's new event kinds extend the registry by adding entries to src/loomworks/foray/anchor_priority.py rather than by editing memory/events.py.turn_event_id → memory_events.event_id and converted_person_id → persons.id — would entangle credit metadata with other DeclarativeBases (CR-A D5 precedent applies)event_kind restricting to the 9 known values — adds migration friction every time a new event kind lands; today's trade favors flexibilitylegacy_unknown historical rows — production rows that fall in this bucket stay as historical noiseCarried forward from CR-A and CR-B, unchanged at CR-C close:
test_api_assertion_retract JSON-encoding bug at src/loomworks/api/app.py:461 — TypeError: Object of type ValueError is not JSON serializable in _log_request_validation. Pre-existing through three CRs. Worth a small fix at next infra ship.test_phase_44_evaluator::test_concern_scan_does_not_re_fire_within_dedup_window dedup-window flake — pre-existing through three CRs. Worth investigation when next touching evaluator surfaces.engagement_display_identifier_seq overflow — known follow-on, workaround applied every CR; conftest still doesn't reset the sequence between runs. Worth a test-infra ship eventually.New items surfaced at CR-C:
event_kind distribution post-backfill for the legacy_unknown count (§3). Worth recording in deploy notes when production migration runs.
The three-CR arc began with loomworks-substrate-hygiene-scoping-note-v0_2.md and closes with these notes.
Three FORAY-shaped substrate surfaces replaced:
| Before | After | CR | Engine tag |
|---|---|---|---|
| audit.foray_events | audit.events | CR-A | phase-61-audit-cleanup (616bab9) |
| memory_events.attestation JSONB + 2 FORAY-readiness columns + injection logic | memory_events.commit_webauthn_attestation + _ANCHOR_PRIORITY relocated | CR-B | phase-62-memory-events-cleanup (ead29a8) |
| credit.foray_action_flows + JSONB-extract readers | credit.flows + typed-column readers + 2 new indexes | CR-C | phase-63-credit-substrate-cleanup (573abe1) |
16 reserved-emit call sites with # FORAY_RESERVED_LOCATION: <namespace>.<event_kind> markers: 3 in audit + 1 in memory + 12 in credit. The future-FORAY-integration substrate is in place.
src/loomworks/foray/ module foundation:
reserved_emit.py — the no-op _foray_reserved_emit(event_kind, payload) function (CR-A)anchor_priority.py — the relocated _ANCHOR_PRIORITY registry with 19 entries (CR-B)ForayClient wrapper and the real attestation write when FORAY integration opensPhase A of the development assessment complete. The substrate is clean of speculative FORAY framing.
7cce914)
The trajectory is visible at record.dunin7.com/sections/substrate/cleanup/ and will remain visible for as long as the record stands.
DeclarativeBase with persons/engagements metadata; DB-level FKs would reject test fixtures using fabricated UUIDs. FKs dropped from both layers. Captured as design-time-pattern lesson.test_phase_48_reconciliation_evaluator.py bypassed the ORM. Inventory cognitive density gap pattern emerged.write_referral_credit_flow. The PostgreSQL trigger preservation worked transparently — the "main unknown going in" was the smoothest piece.The cleanup arc surfaced more methodology than any single CR could have. The three-CR sequence's value is in the principle accumulation as much as the substrate cleanup.
Four principles validated across three CRs (§5):
Worth a separate consolidation ship to lift these into methodology canon. CR-D does not exist; the consolidation is not a "CR-D" — it's a methodology-only ship distinct from the substrate-change CR series.
The three previously-FORAY-shaped surfaces:
audit.events — 1 event kind today (setting_change); structurally extensible to future kindsmemory_events — commit_webauthn_attestation column carries Memory-essential WebAuthn proof; FORAY-readiness columns gone; _foray namespace injection removed; reserved-emit at the single write sitecredit.flows — 9-value event_kind taxonomy explicit; three typed columns serving the readers via indexed predicates; trigger + FK preserved; reserved-emit at 12 sitesThe future-FORAY hook surface:
_foray_reserved_emit call sites with grep-discoverable comment markers_ANCHOR_PRIORITY registry colocated with the emit hooksrc/loomworks/foray/ module ready for the ForayClient wrapperThe substrate is ready. When the Operator decides to open FORAY integration, the hooks are in place. Until then, the substrate is operationally clean of speculative framing.
Cleanup arc closed cleanly.
CR-C landed clean. Five clean commits. Trigger continuity verified empirically (§2). Both reserved slots consumed by execution-time discoveries documented as evidence for Principle 4. The methodology candidates §5 have three-instance evidence each and are ready for canonical lift.
The substrate is clean. The reserved-emit hook surface is in place. The foundation module exists. Phase A is complete. Phase B and Phase C horizons are open.
The trajectory across scoping note v0.1 → inventory → scoping note v0.2 → three sibling CRs → these closing notes is preserved at record.dunin7.com/sections/substrate/cleanup/ for as long as the record stands. Future maintainers — human or otherwise — can read the full discovery record and understand what was learned, what changed, and why.
This is the closing artifact of the cleanup arc. Phase A done.
DUNIN7 — Done In Seven LLC — Miami, Florida Phase 63 Implementation Notes — v0.1 — Closing the FORAY-shape Substrate Hygiene Cleanup Arc — 2026-05-25