v0.8.41 — a rename is one transaction; the Neo4j constraints are provable
Fixed
-
Renaming a project is now one transaction per pair, applied whole or not at
all.normalize_projects.pyused to run three independent sweeps over the
alias map: rewrite every record and commit, then try to record every alias,
then rewire every graph node. The second sweep could fail on a pair the first
had already committed, and the third ran regardless of both — so a single
failure left a state no sweep could describe: records moved onto the new name,
the old name still registered with no alias recorded, and the graph pointing
at the new node anyway. None of it was reported as a failure, and the exit
status was zero.Each pair is now one transaction. It commits whole or rolls back whole, its
graph half runs only if its Postgres half committed, a failed pair is
named with its reason and does not stop the pairs after it, and the run exits
non-zero when any pair failed. The target's registration is checked before
any write rather than after the records were already committed onto it.This matters because two foreign keys point at
projects.name, so retiring a
registry row can be vetoed — and atomicity is what makes that veto harmless:
the pair rolls back, the old name stays registered and still resolves, and
every save keeps working. The hazard was never the veto, it was the half-write. -
A rename no longer destroys the name a promotions-ledger row originally
targeted. Ledger rows are re-pointed inside the same transaction, with the
original target preserved in the row's ownnote. The ledger exists to answer
what was this before; silently rewriting its target would destroy exactly
the evidence that makes a one-way write auditable. NeverON UPDATE CASCADE,
which would do the same damage with no trace at all. -
--dry-runis now a real preflight. It reports the records, ledger rows
and alias rows a rename will touch — including superseded alias rows,
which are deliberately not re-pointed (re-pointing them would falsify the
history they exist to preserve) and will therefore veto the rename. Finding
that out from a failed run is finding it out too late. -
sync_skills.shno longer refreshes a hardcoded subset of the client
package. Phase 2 and the parity test both readMANIFEST.txt; phase 1 read
a list of filenames in the script. So a file could be added to the manifest,
ship to every agent, and be refreshed by nobody.Documentation/schema.mdwas
updated at source in two consecutive releases, copied to the tracked skill
tree in neither, and shipped stale to every client for both — while the script
printed success. Phase 1 is now driven by the manifest too, so the manifest's
own promise that it is "the whole maintenance surface" is true. -
Decision.alternativescan no longer be shredded into single characters on
the read path. The graph-expansion projection calledlist()on the
property unconditionally. All 223 Decision nodes currently hold a Neo4j LIST
OF STRING, where that is a harmless passthrough — but this property has been
written as a JSON string before, andlist()on a string explodes three
alternatives into several hundred one-character ones. A string is now one
entry.
Added
-
migrations/verify_neo4j_init.py— proof that the declared Neo4j
constraints are actually in force. Postgres has a migration ledger; Neo4j
has none.neo4j_init.cypheris a one-time manual step, so a long-lived
instance enforces whatever was true the day someone last applied it, and a
constraint added in a later release reaches new installs and nobody else. That
failure is silent by construction:MERGEkeeps working, writes keep
succeeding, and the only symptom is a duplicate node appearing under a race —
at which point the constraint that would have prevented it is the thing you no
longer have.The script diffs declared against live, and for each missing constraint counts
the duplicate values that would makeCREATE CONSTRAINTfail — because
"missing" and "cannot be added without repairing data first" are very
different situations and the difference must not be discovered halfway through
an apply. Constraints belonging to another system on a shared instance are
reported as foreign, never touched. Read-only by default;--applycreates
what is missing. Exit status 1 when a declared constraint is not in force.⚠ It also handles an upgrade trap that re-running
neo4j_init.cyphercannot:
Neo4j refusesCREATE CONSTRAINTwhile a plain index covers the same
label and property. A fresh install never meets this, because the constraints
are applied before anything creates an index; an instance where someone added
a lookup index by hand is blocked indefinitely, with no error unless somebody
goes looking.--applydrops the conflicting index first, which costs nothing
— a uniqueness constraint creates its own backing index on the same key.
Changed
AGENTS.mdupgrade path and README §6 now cover the Neo4j side. The
documented upgrade ranapply.pyand restarted, which covers Postgres and
says nothing about Neo4j; and README describedneo4j_init.cypheras
idempotent and safe to re-run, which is true and insufficient — re-running it
does not clear a blocking plain index and does not tell you whether anything
is enforced. Both now point at the verifier, and Phase 5 confirms both stores
rather than trusting an exit status.