Follow-up from the review of #14.
What happens
After promote --switch SERIES, a switched lecture can never be re-promoted (by design: the pool copy is the source of truth). Its single-consumer assets stay at _static/<slug>/.... If a lecture from another series is later promoted and references the same asset (same mirror path, same bytes), the destination step decides the asset now has two consumers and belongs in _static/_shared/..., finds the old location is still used by a lecture outside the batch, and refuses with:
[other] FAILED: asset _static/lecture_specific/mccall/fig.png is recorded at _static/mccall/fig.png but now maps to _static/_shared/mccall/fig.png — re-promote its other consumer(s) (mccall) in the same batch
The remedy it names is impossible. Adding mccall to the batch gives:
[mccall] FAILED: lectures/mccall.md is canonical in the pool (switched from intermediate on 2026-10-08) — promotion would overwrite the pool copy
So the new lecture cannot enter the pool while it shares that asset. Switching the new lecture's series does not help either, since the switch only touches ledger entries and the lecture has none yet.
Reproduction (with the tools/tests harness)
repo = make_repo(["intro", "intermediate"])
fig = b"figure bytes"
repo.mirror_file("intermediate", "_static/lecture_specific/mccall/fig.png", fig)
repo.lecture("intermediate", "mccall", lecture_text("McCall", refs=["_static/lecture_specific/mccall/fig.png"]))
assert repo.promote("mccall").returncode == 0
assert repo.promote("--switch", "intermediate").returncode == 0
repo.mirror_file("intro", "_static/lecture_specific/mccall/fig.png", fig)
repo.lecture("intro", "other", lecture_text("Other", refs=["_static/lecture_specific/mccall/fig.png"]))
assert repo.promote("other").returncode == 1 # the error above
assert repo.promote("other", "mccall").returncode == 1 # the suggested remedy also fails
When it bites
Only mid-transition: one series switched, another still fed from its mirror, and a new lecture in the second reusing a figure the first already owns. Assets that already live in _static/_shared/ are unaffected (the destination does not change, and #14 already keeps a switched asset's record when another lecture comes to share it).
Suggested fix
Treat a switched asset's location as frozen. In the destination pass, when the old pool path of (src_rel, digest) is referenced by a lecture whose ledger entry is canonical: pool, keep dest_map[(src_rel, digest)] = old instead of recomputing it, so the new lecture is rewritten to the existing path and shares the file in place. The switched lecture's pool text is untouched, the ledger gains the new lecture as a user of the asset, and the asset keeps its canonical: pool record. A test alongside test_an_asset_shared_with_a_guarded_lecture_stays_guarded would cover it.
Follow-up from the review of #14.
What happens
After
promote --switch SERIES, a switched lecture can never be re-promoted (by design: the pool copy is the source of truth). Its single-consumer assets stay at_static/<slug>/.... If a lecture from another series is later promoted and references the same asset (same mirror path, same bytes), the destination step decides the asset now has two consumers and belongs in_static/_shared/..., finds the old location is still used by a lecture outside the batch, and refuses with:The remedy it names is impossible. Adding
mccallto the batch gives:So the new lecture cannot enter the pool while it shares that asset. Switching the new lecture's series does not help either, since the switch only touches ledger entries and the lecture has none yet.
Reproduction (with the
tools/testsharness)When it bites
Only mid-transition: one series switched, another still fed from its mirror, and a new lecture in the second reusing a figure the first already owns. Assets that already live in
_static/_shared/are unaffected (the destination does not change, and #14 already keeps a switched asset's record when another lecture comes to share it).Suggested fix
Treat a switched asset's location as frozen. In the destination pass, when the old pool path of
(src_rel, digest)is referenced by a lecture whose ledger entry iscanonical: pool, keepdest_map[(src_rel, digest)] = oldinstead of recomputing it, so the new lecture is rewritten to the existing path and shares the file in place. The switched lecture's pool text is untouched, the ledger gains the new lecture as a user of the asset, and the asset keeps itscanonical: poolrecord. A test alongsidetest_an_asset_shared_with_a_guarded_lecture_stays_guardedwould cover it.