Version. v0.4
Date. 2026-07-31
Supersedes. v0.3 at record 9c1766f, v0.2 at d697187, v0.1 at b36a676. All stand as siblings, unaltered. Unexecuted at the time of this bump.
Changes from v0.3. One, and unlike the three prior bumps it changes how the build runs rather than how the document reads. Checkpoint A no longer requires Operator confirmation. v0.1 to v0.3 halted it for his sign-off before merge; charter R-2 halts a Checkpoint A only for items needing his browser or passkey, and authorises merge-and-tag at Checkpoint B including the push. Nothing at this checkpoint needs him. The checkpoint now reports and proceeds on a clean report, and names four conditions that halt and queue instead. The gate was an interruption the charter had already removed, written into the CR in the same session whose governing complaint was unnecessary interruption.
Changes from v0.2. One, in an instruction rather than a rationale. §6.4 told the execution session to re-confirm "these five anchors"; the table holds six positions, because :142 appears twice in different roles across the two modules. A session counting cells against "five" would look for a duplicate to discard, and the candidate is redirect_edge_backfill.py:142 — the dict construction, which is the line the repair targets. The count is removed rather than corrected: it carried no information the table did not already hold, and could only ever be wrong. The shared numeral is now flagged explicitly as coincidence.
> The discipline this establishes, recorded because it now has four instances. A line number asserted without a read (corrected at status brief v0.2 §8); a commit SHA gestured at rather than looked up (build list, recorded for the next bump); a source characterised loosely (D-5, corrected at v0.2); and this count. Every one is a specific stated where the work did not need one. None of the four changed what gets built; all four had to be caught by the executing session. The rule that follows: state the anchor, not a count of anchors; state what you read, not what you infer surrounds it. Carried to the manifest's methodology candidates alongside state only what you have read back.
Author. Claude.ai (drafting session). Approving: Marvin Percival.
Charter. standing-notes/dunin7-standing-authorization-charter-v0_1, ratified 2026-07-30. This is a drafting session's output; no session executes a change request it drafted (§1).
Target. /Users/dunin7/loomworks-engine on DUNIN7-M4. No work in loomworks, loomworks-ui, or stele.
Baseline. Engine main a317051. Tag registry-completeness-v0_1 sits on c4014d6, the merge commit one before — git describe returns registry-completeness-v0_1-1-ga317051. Alembic head 0102 as declared in migrations/versions/ (single head, 102 revisions). Working tree carried M uv.lock unstaged at inspection time.
Build-list item. B-29.
Priority. Standard, with one ordering constraint that is not negotiable — see §4.
Confidential. Internal DUNIN7.
CR number. CR-2026-159. Highest present in the record at drafting is CR-2026-158; CR-2026-157 is filed and unexecuted. [CC verifies at Step 0 against change-requests/ and docs/phase-crs/; advance if taken.]
Companion to. inspection-briefs/loomworks-b29-b27-b5-engine-inspection-findings-v0_1 (the grounding read, record e037a37); inspection-briefs/loomworks-b25-sizing-sweeps-findings-v0_1 (the original sizing, corrected by the note below); inspection-briefs/loomworks-inspection-corrections-note-v0_1 (C-3, C-4, C-5 apply here); current-status/dunin7-status-brief-v0_2 §3 D-1 (the seam decision and its reasoning).
Status. Pre-execution. Ready for execution by a session that did not draft it.
Provenance.wasGeneratedBy is declared at src/loomworks/memory/base.py:147 with its meaning in a trailing comment: the memory event id that produced this version. It is required — a bare UUID annotation with no default and no default factory, unlike the four fields around it.
Every write path constructs provenance before the event exists, so every caller fills the field with a freshly minted UUID that points at nothing. Eighty-three sites across thirty-seven files do this. Three sites in persons/personal_engagement.py do it correctly, by pre-minting the identifier and passing the same value to both the provenance and the insert.
append_event mints its own event identifier at memory/events.py, inside the MemoryEventRow(...) constructor, after the object has already been serialised — and never back-patches. The stored value can therefore never be the real one.
This CR stamps the field inside append_event, before serialisation. One file, one reordering. The eighty-three sites are not touched; their values simply stop reaching the database. Two raw-SQL paths that bypass append_event entirely get a one-line repair each, because no seam change reaches them.
Why this is low risk. Nothing reads the field. No query filters on it, no projector derives from it, no response schema exposes it, no test asserts its value, and nothing hashes it. The surface is write-side only, so no downstream consumer can break.
Why it should not wait. The FORAY reserved emitter is a no-op today and provenance is not in its payload. When integration opens, whatever sits in the payload and provenance JSONB columns becomes the anchored composition. Fabricated provenance inside an anchored history is materially worse than fabricated provenance outside one.
append_event mints the event identifier before serialising, rebuilds the object's provenance with the real value, and serialises from the corrected object — so both the payload JSONB and the provenance JSONB columns carry the true identifier.engagement/wasderivedfrom_backfill.py and engagement/redirect_edge_backfill.py, one line each.memory/events.py, which currently asserts something false.provenance.wasGeneratedBy equals the row's event_id, on both the seam path and the two repaired backfills.relocate_memory_object. It preserves object_id and object_version and rewrites only the row's home; it does not touch provenance and is not part of this surface.main at a317051 or later, working tree clean apart from any pre-existing M uv.lock..env untouched. playground_dev is the live production database and is not touched.
C-1. This CR lands before B-25 is built. At three sites the two items are the two keyword arguments of a single Provenance(...) expression on consecutive lines:
| Site | Lines |
|---|---|
| api/routers/seed_extraction.py | :258-259 — wasAttributedTo=extraction_actor, wasGeneratedBy=uuid.uuid4() |
| api/routers/seed_commit_from_brief.py | :305-306 — wasAttributedTo=draft_actor, wasGeneratedBy=uuid.uuid4() |
| api/routers/me_create_engagement.py | :841-842 — wasAttributedTo=person_actor, wasGeneratedBy=uuid.uuid4() |
Because this CR does not touch those sites, the collision does not arise — but only if B-25 is not built concurrently. B-25's change request may be drafted while this builds.
C-2. This CR lands before FORAY integration opens at src/loomworks/foray/reserved_emit.py. The emitter is a no-op today and its payload carries only event_id and anchor_priority; provenance is not in it and nothing hashes the JSONB columns. After integration, changing the stamped value changes payload composition across the anchor boundary.
C-3. This CR lands before the completion arc's engine rider (CR-B). Same engine, same ruling.
D-1. Seam option B — append_event stamps the field itself. Recorded with full reasoning in current-status/dunin7-status-brief-v0_2 §3. In summary: both options are correct and neither can break a consumer; option A touches all 83 sites and guarantees a line-level collision with B-25; option B touches one file B-25 never opens. Reversal cost before build starts is one redraft.
D-2. The rebuild uses model_copy, nested. Provenance is frozen (base.py:144) and MemoryObject is frozen (base.py:161), so the corrected provenance cannot be assigned in. The object is copied with a copied provenance.
> A tension to state rather than leave implicit. memory/base.py's module docstring says frozen models "enforce non-erasure structurally — a memory object cannot be mutated, only superseded by a new version." This change copies an object before it is persisted, not after. Nothing recorded is altered, and no version is superseded; the copy exists only to carry a value that could not be known when the caller built it. The invariant the docstring protects is about the record, and the record is untouched. The implementation carries a comment saying so, so the next reader does not have to re-derive it.
D-3. The eighty-three dead stampers are not swept in this CR. After the seam lands they are harmless but misleading — a reader sees wasGeneratedBy=uuid.uuid4() and reasonably assumes it is stored. Sweeping them requires making the field optional at base.py:147, which is a declaration change touching 37 files, and it collides with B-25 at the three sites in C-1. The sweep is queued behind B-25 and recorded on the build list under Part 4. This is a deliberate deferral, not an oversight.
D-4. No historical backfill. Every object written before this change carries a fabricated value in the field. Because the damage is structurally knowable — every object outside the three correct sites — the honest remediation is one recorded statement in the record, not a per-record marking pass. This CR files that statement as part of its implementation notes. Quantifying how many live records are affected needs a query against the serving perimeter and stays parked as a decision of a different kind.
D-5. The module docstring is corrected in the same change. memory/events.py currently opens: "append_event is the single code path for writing to the memory event log. No other function in the codebase writes to memory_events directly." Three functions do — persons/personal_engagement.py::_append_event, engagement/wasderivedfrom_backfill.py, engagement/redirect_edge_backfill.py — plus relocate_memory_object in the same module.
> Corrected at v0.2. v0.1 said relocate_memory_object is acknowledged in "the docstring's own later text", which reads as though the module docstring both asserts the claim and undercuts it. It does not. Two different docstrings are involved: the module docstring is five lines and never mentions relocation; the "one sanctioned exception" language lives in relocate_memory_object's own function docstring, further down the file. The substance was right — the exception is acknowledged somewhere in that file — but the module docstring is uncontradicted on its own terms, and simply false.
A false docstring at the seam is how the "one seam" sizing error happened; correcting it is cheap and prevents the next one.
src/loomworks/memory/events.py
Today, inside append_event, in this order:
payload = object.model_dump(mode="json")
event = MemoryEventRow(
event_id=uuid.uuid4(),
...
payload=payload,
provenance=object.provenance.model_dump(mode="json"),
...
)
The identifier is minted inside the constructor, after payload has been serialised from the un-corrected object. Both the payload JSONB (which contains a nested provenance) and the dedicated provenance JSONB column therefore carry the caller's fabricated value.
After, mint first and correct the object before either serialisation:
# 4. Mint the event identifier, then stamp it into the object's
# provenance before anything is serialised. wasGeneratedBy means "the
# memory event id that produced this version" (memory/base.py:147), and
# the caller cannot know that value — it does not exist until this line.
# Both Provenance and MemoryObject are frozen, so this is a nested copy,
# not an assignment. Nothing recorded is mutated: the copy happens
# before persistence, so the non-erasure invariant the frozen models
# protect is untouched.
event_id = uuid.uuid4()
object = object.model_copy(
update={
"provenance": object.provenance.model_copy(
update={"wasGeneratedBy": event_id}
)
}
)
payload = object.model_dump(mode="json")
event = MemoryEventRow(
event_id=event_id,
...
payload=payload,
provenance=object.provenance.model_dump(mode="json"),
...
)
Why both columns are fixed by one change. payload is dumped from the whole object, which includes provenance; the provenance column is dumped from object.provenance. Both read the corrected object. Correcting payload after the fact would have fixed only one of the two.
A note the implementation carries. model_copy(update=...) skips validation in Pydantic. Here the update is a UUID into a UUID-annotated field, so it is safe; the CR records this so a future reader does not have to establish it.
A coherence gain worth naming. The FORAY reserved emit at the end of append_event already sends {"event_id": str(event.event_id), ...} — the correct identifier. After this change, the value in the emit and the value in provenance are the same value. They disagree today.
[CC determines] whether the local name object is rebound or a new name introduced. Rebinding keeps the diff minimal and every downstream reference correct; a new name is clearer to read. Either is acceptable; pick one and be consistent within the function.
Replace the module docstring's false claim. The corrected text names the sanctioned exceptions rather than asserting there are none:
append_event is the primary write path and the only one that maintains the six projections.relocate_memory_object, in this module, moves existing rows and writes no new event.memory_events directly and maintain their own locking, version advance, and projection upsert: persons/personal_engagement.py::_append_event, engagement/wasderivedfrom_backfill.py, engagement/redirect_edge_backfill.py. Any change to the event-write contract must reach all four sites.src/loomworks/engagement/wasderivedfrom_backfill.pyThe module mints the row's real event identifier, then on the next line mints a second, different UUID and stamps that into provenance. The correct value is already in scope one line above the wrong one.
Repair: assign the provenance variable from the event identifier rather than minting again. One line.
src/loomworks/engagement/redirect_edge_backfill.py
Same defect, wider apart. event_id = str(uuid.uuid4()) is minted, and eleven lines later "wasGeneratedBy": str(uuid.uuid4()) mints a fresh throwaway.
Repair: "wasGeneratedBy": event_id. One line.
Note — determination closed at v0.2. One repair covers both columns in both modules. v0.1 left this open as [CC verifies]; it was verified at v0.1's filing and the anchors are recorded here so the execution session confirms rather than investigates.
Both modules build a single provenance dict, embed that same object in the relationship payload, and serialise the same object again for the dedicated column — so correcting the dict at its construction corrects both destinations.
| Module | Builds the dict | Embeds it in the payload | Serialises for the column |
|---|---|---|---|
| engagement/wasderivedfrom_backfill.py | :78 | :97 | :142 |
| engagement/redirect_edge_backfill.py | :142 | :164 | :206 |
The execution session re-confirms every anchor in the table at Step 0 alongside the site sweep, and halts if any has moved — a shifted line here means the module changed after this CR's evidence base was taken.
Six positions, five numerals. :142 appears twice, in different roles — wasderivedfrom_backfill.py's column serialisation and redirect_edge_backfill.py's dict construction. The shared number is coincidence, not a cross-reference. Check all six cells; do not treat either :142 as a duplicate of the other. (v0.1 and v0.2 said "these five anchors", counting numerals rather than positions — a count that carried no information the table did not already hold, and could only be wrong. Corrected at v0.3 by removing the count.)
src/loomworks/persons/personal_engagement.pyNo change. Its three sites pre-mint, stamp, and pass the same identifier to the insert. They are correct, and they are the in-tree proof that the field means what its comment says.
None. No table, no column, no constraint, no index. The change is entirely to what is written into two existing JSONB columns going forward. Alembic head stays at 0102.
The field currently has zero value assertions anywhere in the tree — 312 test-tree occurrences of the string are either construction boilerplate or the unrelated Relationship vocabulary term of the same name. This CR establishes the first.
append_event and assert the stored row's provenance["wasGeneratedBy"] == str(row.event_id), on both the provenance column and the nested payload["provenance"]. Assert it holds even when the caller passed a deliberately wrong value.wasderivedfrom_backfill. Run the backfill and assert every written row satisfies the same equality.redirect_edge_backfill. Same.personal_engagement. A characterisation test asserting the three sites still satisfy the equality — they do today, and this locks it.
Verification environment. Throwaway database on the walk-audit pattern. No connection to playground_dev.
Per-step commits. Suite green at each. Halt on anomaly per charter §6.
Step 0 — pre-flight and archival.
Verify the CR number is free. Verify engine HEAD, working tree, and alembic head against §Baseline; report any divergence and halt if the tree has moved in a way this CR's evidence base does not anticipate (charter C-3). Re-run the fabricating-site sweep and confirm 83 sites / 37 files. Archive this CR to docs/phase-crs/.
Commit: CR-2026-159 step 0: pre-flight + CR archival
Step 1 — the seam.
§6.1 and §6.2. Suite green.
Commit: CR-2026-159 step 1: stamp wasGeneratedBy at the seam; correct the events.py docstring
Step 2 — the two bypasses.
§6.3 and §6.4. Suite green.
Commit: CR-2026-159 step 2: repair the two backfill provenance fabrications
Step 3 — tests.
§8 items 1–5. Full suite green.
Commit: CR-2026-159 step 3: provenance equality regression tests
CHECKPOINT A — report, then proceed. Report: suite counts, the five new tests passing, a live verification on the walk-audit dev stack showing a freshly appended event whose stored provenance equals its event_id, and confirmation that no pre-existing test required editing.
Proceed to Step 4 on a clean report. No Operator confirmation is required here. (Corrected at v0.4. v0.1 to v0.3 said "Operator confirms before merge." Charter R-2 halts a Checkpoint A only for items needing the Operator's browser or passkey, and authorises merge-and-tag at Checkpoint B including the push. Nothing at this checkpoint needs him, so the gate was an interruption the charter had already removed.)
Halt and queue if any of these fired: a pre-existing test needed editing; the live verification did not show the equality; suite counts moved in a way the change does not explain; or any charter §6 anomaly.
Step 4 — implementation notes.
docs/phase-impl-notes/cr-2026-159-implementation-notes-v0_1.md, carrying D-4's recorded statement: every memory object written before this commit carries a fabricated value in provenance.wasGeneratedBy, outside the three personal_engagement.py sites; no backfill was performed; the count of affected live records is unquantified and parked.
Commit: CR-2026-159 step 4: implementation notes
CHECKPOINT B — merge and tag. --no-ff merge to main, annotated tag provenance-seam-v0_1, push. Authorized under charter R-2 including the push. Deployment is never autonomous (F-1) — the launchd restart is the Operator's act and batches to cadence.
provenance.wasGeneratedBy equals the row's event_id on the seam path, in both the provenance column and the nested payload.personal_engagement.py sites still satisfy it.memory/events.py's docstring names all four write paths.0102.loomworks-engine. No database touched except a throwaway.
CR-2026-159 — B-29, the provenance seam. Execution session.
CR: loomworks-record/change-requests/cr-2026-159-provenance-seam-v0_4.md
Grounding read: inspection-briefs/loomworks-b29-b27-b5-engine-inspection-findings-v0_1.md
Corrections that apply: inspection-briefs/loomworks-inspection-corrections-note-v0_1.md — C-3, C-4, C-5
Read the CR in full before starting. Charter dunin7-standing-authorization-charter-v0_1 governs.
Target: /Users/dunin7/loomworks-engine at main a317051 or later.
The engine belongs to this CR for its duration. Confirm no other session holds it.
playground_dev is the live production database and is not touched.
Verification runs on the walk-audit dev pattern: spare port, inline environment,
throwaway database, live processes and .env untouched, teardown after.
Steps 0 through 3 auto, per-step commits, suite green at each.
CHECKPOINT A reports and proceeds on a clean report — no Operator confirmation.
Halt and queue only on the four conditions the CR names there.
Step 4 auto. CHECKPOINT B merges, tags provenance-seam-v0_1, and pushes.
Deployment is never yours — the restart batches to the Operator's cadence.
Two things that are anomalies, not adjustments:
- any pre-existing test needing an edit (nothing asserts the old behaviour)
- any divergence between the CR's evidence base and the live tree
Append the outcome to current-status/dunin7-status-brief at close.
DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-159 — B-29: the provenance seam — v0.4 — 2026-07-31 One seam, two bypasses, one false docstring. Eighty-three sites untouched by design.