Releases: KanenasInGreece/Shared_Memory
Release list
v0.8.50 — a change group enforces what it can, and states what it cannot
Added
-
The change groups now enforce what can be enforced, and say plainly what
they cannot. The rule that touching one member of a group means reviewing the
whole group is a discipline, and a discipline is what fails on the release
where someone is in a hurry.tests/test_change_group_contracts.pytakes the
mechanically checkable obligations and makes them tests:- All four version pins must agree (Group 1). The release version lives in
four files, two of them client copies — which is why even a server-side fix
touches this group — and until now nothing checked that a bump reached all
four. A missed one ships a client announcing a version the gateway does not
recognise, and the only symptom is a compatibility warning from a command
nobody runs on a good day, so the divergence outlives the change that caused
it. Both client copies must also pin oneapi_version. - Every table a migration creates must reach
schema_init.sql(Group 4),
and the migration chain must have no gaps or duplicate numbers. This cannot
see a missing constraint — only the live diff can — but it catches the
coarsest omission: a migration adding a table and nobody regenerating the
artefact a fresh install actually applies. - Every script the upgrade path names must exist (Group 5). A documented
step naming a file that is not there fails on a stranger's machine while they
follow the instructions faithfully.
Each test names its group and the failure it prevents, and all are
mutation-verified. - All four version pins must agree (Group 1). The release version lives in
Notes
- Three of five groups are still partly or wholly unenforced, and that is now
written down rather than assumed. Group 3 (daemon behaviour and
observability) has no mechanical tie at all — whether a change can be seen
working and failing remains entirely a matter for eyes. Group 4's most
dangerous class, a constraint silently dropped from the fresh-install
artefact, has bitten three times and is caught only byverify_schema_init.py
run against a throwaway database. A green suite does not mean a group was
cleared.
v0.8.49 — the capture surface explains what each field captures and which failure it prevents
Changed
-
The capture surface now explains itself to someone who did not build it.
Every field it asks for existed because something downstream breaks without
it, and that breakage is almost always SILENT — the save succeeds, the record
is searchable, and only synthesis quietly fails to happen. Until now the
surfaces described the fields accurately and assumed the reader already knew
why they mattered, which is only true of the people who designed them.SKILL.mdgains a record-model section, placed before the tasks: a table
of the three record types against who owns which field, then a paragraph per
field saying what it captures, which failure it prevents, and what it costs to
get wrong. It closes with what to derive silently, what to propose for
correction, and what must always be asked.The
--helpstrings gain the contract of each flag: shape, whether it
repeats, whether it is required, and what the gateway does when it is wrong.The split is deliberate. Help text is read when the caller already knows it
wants the flag;SKILL.mdis read when deciding whether a field applies at
all — and the elicitation decision, should I interrupt the operator for
this?, cannot be made from a description of the shape alone. -
source_refis documented as answering a DIFFERENT question per record
type, which was true in the code and stated nowhere. On a fact it is where
the KNOWLEDGE came from, and it silently sets that fact's evidential weight.
On a retrospective it names THE INSTRUMENT THAT MEASURED THE OUTCOME — a claim
its grounding facts cannot make on its behalf, because those facts may belong
to another project entirely and cite a different file tree. -
The asymmetry in
grounded_inis now explained where it is enforced. A
decision may rest on experience, because a project's first decisions are
genuinely made before it has evidence. A retrospective may not: it exists to
report what measuring showed, so with nothing measured it asserts a verdict
from nowhere — and it strands the decision it judges, which reaches its own
topics through it. -
And a test that fails when the contract moves without the documentation.
A good intention does not survive forty releases — the stale examples above
are the proof.tests/test_capture_surface_documented.pyasserts that every
capture flag a client offers, every ingress refusal the gateway can return,
every outcome rating, and the worked examples' version andapi_versionare
present in the skill document. It checks presence, never wording, so ordinary
edits do not fail it; what it makes impossible is ADDING a caller-visible part
of the contract that nobody explains. Its exemption list is the point rather
than a loophole: a new flag fails until someone either documents it or names
it mechanical, and both are answers. It caught an omission on its first
run —domain_without_project, a refusal shipped in v0.8.47 and documented
nowhere.
Documentation, help text and one test. No behaviour change, no schema change, no
wire-contract change.
v0.8.48 — a spelling is exact, so it must not be gated behind a similarity score
Fixed
-
A spelling variant could register as a new project or section whenever it
also scored below the similarity floor. The guard that refuses a name
differing from a registered one only in separators or capitalisation was
applied to the TRIGRAM NEIGHBOURS a confusable query returned — which quietly
made an exact rule conditional on a fuzzy one. Measured on the live registry:
testingversusTest_Ingscores 0.545 against a floor of 0.6, so the
variant never reached the check and registered as a brand-new value. This was
latent in the project registry from v0.8.44 and was reproduced on the domain
registry the day it shipped:Shared_Memory_Monitoris now refused against the
registeredshared-memory-monitor, and was not before.The spelling check now runs over every registered name, ahead of the
confusable query, through one shared pure helper both axes call. The floor was
deliberately not lowered: that would flatten two populations it exists to
separate — legitimately distinct names sit just under it — and would train the
reflex to override a warning that fires on correct input. The two gates answer
different questions and run in order: a SPELLING is exact equality on a
normalised key and cannot be confirmed away; a CONFUSABLE is a fuzzy neighbour
the operator may confirm as genuinely distinct. Both error codes are unchanged. -
save_decision --domainwas parsed and dropped on the floor. The flag
reached the argument parser and nothing threaded it into the record, so a
decision fell back to inheriting its evidence's sections. It read as correct
because the inherited answer happened to match what had been asked for — the
edge carriedasserted_by='inherited'where an assertion should have been
bare. Now packed into the decision blob, besideproject, which is the half
the gateway resolves a judgement's axes from.vector-skill.py's
save_decisiongains the same parameter, so the two front doors stay at parity.The regression tests assert the provenance of the edge, not just the
presence of a section name: a decision whose evidence sits in the same section
produces the same name either way, and only the stamp tells an assertion from
a default.
Notes
- A live probe over the registries found no existing pair of projects, and no
pair of sections within one project, sharing a spelling key — so the defect
registered nothing before it was caught beyond the one probe value, which was
retired.
v0.8.47 — a domain is a registered section of one project, and the record that owns the scope names it
Added
-
A domain is now a registered SECTION of one project, with an identity of its
own.domainhad been a free-text metadata field with nothing to be unknown
against: a typo and a new section were the same event and both entered the
corpus silently. Migration 028 addsproject_domains— keyed on a surrogate
id, referencingprojects(id)and never a project name, with the section's
label unique only within its project becauseoperationsunder one project
andoperationsunder another are different sections that share a word. Its
alias junctiondomain_aliaseslands in the same migration rather than as a
follow-up, so a retired spelling resolves from the first day the axis exists.Ingress mirrors the project protocol exactly: an unregistered value is refused
400 domain_unknownwith proposals, and the second submission registers it —
behind the same two naming guards a new project faces, because the agent that
sets the flag is the agent that makes the typo. Proposals match a section's
description as well as its name, which is the one real difference from the
project axis: project names are short and typo-shaped, while an operator
reaching for a section may type a word that appears nowhere in its name.The graph gains
(:Fact|:Decision|:Retrospective)-[:DOMAIN_OF]->(:Domain)-[:PROJECT_OF]->(:Project),
written in the existing single outbox round-trip.:DomainandDOMAIN_OFare
spine, pinned in code — an amendment to the frozen-spine decision, made
because the fold gate is intended to read this axis, and a renameable label
would falsifyontology.yaml's own promise that consolidation touches only
spine identifiers. -
Who controls which axis, stated once and enforced. A fact asserts its
own project and domain and mints its own entities. A decision asserts its
own project and domain, and inherits its entities from the facts it grounds
in. A retrospective asserts neither axis — project and domain both come
from the decision it judges, so a verdict is always filed with what it judges
rather than with the later evidence that measured it; one that supplies a
domain is refused400.A decision that names no domain inherits its grounding facts' sections as a
default, never a ceiling. This is the load-bearing part: a decision reaches
further than the fact that prompted it. A fact may observe that agents write to
the graph directly — an infrastructure observation — while the decision it
provokes governs which agents are authorised to write, which is about access
and sits above the infrastructure that prompted it. Capping a decision at its
evidence's sections would file it away from the section that most needs to
surface it. The rule guards itself on the existing provenance stamp: a bare
edge is an assertion, a stamped one is a default, and inheritance declines
wherever an assertion exists. -
--domainon the CLI (repeatable) and onsave_decision, plus the MCP
equivalent. The value is stored verbatim and never split on a separator, since
a separator that can occur inside a value is not a delimiter. The skill
elicits a section only when the record's project already has registered
ones — a project with an empty registry is never prompted, so the first
section in any project stays a deliberate act. -
GET /health→domain_identity, besideproject_identity: registry
versus graph, plusunattached— a section with noPROJECT_OFedge. That
last number exists for a traversal that has not been built yet. Cross-project
and cross-domain synthesis will walk from a section to its project and from a
record to its grounding facts, and a section missing that edge would drop out
of the walk silently, presenting as a quiet corpus rather than an error. -
backfill_domain_of.pyenqueues the historical population through the
outbox, in two modes: a record's own sections, or a re-run of the gateway's own
inheritance query for a judgement that asserted none. The second mode exists so
the rule has a single implementation — a repair that re-derives a rule is a
repair that can disagree with the thing it repairs.
Notes
-
Migration 028 ships schema and nothing else — no seed, no data repair. A
seed on this axis cannot be derived from the data the way the project registry
seeded itself, because the values needing registration are exactly the ones
that must not be registered verbatim. An emptyproject_domainsis the correct
state for a new install: sections arrive through ingress, like projects. -
There is deliberately no name-keyed
:Domainfallback, unlike the project
axis. Losing aPROJECT_OFedge violates an axis that already gates folding,
so that write falls back to a name; nothing gates on domain yet and the value
survives in Postgres either way, so the honest answer to "no identity" is no
edge and a log line. -
Known, unchanged, and named so it is not mistaken for a defect: the
consolidation daemon's internaldomainvariable holds the project, and
community_summaries.metadata->>'domain'stores a project name. Nothing in the
fold path reads a record'smetadata->>'domain', which is why this release
cannot change fold behaviour. Untangling that naming belongs with the release
that moves the fold gate onto these axes.
v0.8.46 — a decision's payload lives in Postgres, and the graph carries the key to it
Changed
-
A decision's options and confidence are no longer copied into the graph —
they are read from the record that owns them. Both values were written onto
the:Decisionnode at first write and projected back out of graph expansion
into a search hit'sadr_props. Nothing anywhere filtered, ordered or matched
on either one: they were only ever rendered. A second copy of a value nobody
walks on buys nothing the node'spg_iddoes not already give, while
guaranteeing that the two stores can disagree — and they did. The copy of the
options silently missed the majority of decisions for months, and the
confidence copy was measured in exactly that state when this shipped: present
on every decision that records one in Postgres, present on barely a third of
the nodes, with a clean cutover no writer could ever close.Graph expansion now dereferences both from Postgres in one batched
primary-key lookup for the whole walk, keyed on thepg_idevery neighbour
already carries — sub-millisecond, and skipped entirely when a walk turns up
no decision. The search response is unchanged, field for field, so no client
needs anything and the wire contract stays atapi_version 4.This is the successor to the projection-widening decision, not a reversal of
it: that decision bought richer hits at zero extra query and deliberately left
deeper provenance behind. What it deferred is what arrives here — the reader
reaches the record instead of a copy of part of it.⚠ The rule this applies, and its limit: duplicate what the walk consumes,
dereference what the reader renders. It is a test to apply, not a preference —
applied to a project's identity the same rule says the opposite, because the
synthesis gate walks on it. A fact's evidence weight stays on the node here for
a different reason again: it is derived at write, not copied, which makes it
a separate question rather than the same one. -
The guard against shredded options moved with the value it protects. A
barelist()over a JSON string explodes it into single characters, turning
three options into several hundred one-character ones. Every store that has
held this value has been able to hold it as a string, so the guard now sits on
the Postgres read rather than on the graph read that no longer happens. -
The dereference cannot fail a search. Graph context enriches a search and
has never been allowed to fail one; adding a query to that path would have
changed its failure modes, so the whole helper — fetch and row handling — is
fail-open and logs. A payload error costs the hit itsadr_propsand nothing
else.
v0.8.45 — a checker that dies for a reason it misreports
Fixed
-
The two verifiers could not run the way the documentation says to run them.
verify_schema_init.pyandverify_neo4j_init.pyloaded their environment by
importingpython-dotenvand returning silently when it was absent. Nothing
was loaded, so the next connection failed withfe_sendauth: no password supplied— a credentials error reported for what is actually a missing
dependency, sending the reader to check passwords, roles andpg_hbawhile
the real cause was the invocation.That is worse than an ordinary papercut for two reasons. First, every
documented invocation omits the dependency:AGENTS.mdandREADME.md
between them show fiveuv runlines for these tools, none with
--with python-dotenv. So the documented way to prove an install was sound
could not work on a clean machine. Second, these are the two scripts whose
entire job is to prove a property — a checker that dies for a reason it
misreports teaches the wrong lesson twice, and the thing it was going to verify
goes unverified.Both now parse the env file directly, in the same dependency-free,
candidate-list formapply.pyhas always used (frameworkshared-memory/.env
first, repo root as the pre-0.6 fallback), and neither can be defeated by a
missing package again. A real exported variable still wins over the file, so
pointing a tool at another database keeps working.⚠ The audit that found it was of every
_load_envin the framework, not of
one file — the other fifteen were already self-parsing, and these two were the
outliers precisely because they were written later and reached for the library.
v0.8.44 — project identity, and who gets to name a project
Changed
-
A project is now an identity, and its name is a label on it. The registry
gained a surrogate key (projects.id, migration 027); the name stays unique
and queryable, which is what a client asserts, an operator types, and the
client-side graph templates filter on. The two tables that referenced the name
now reference the identity.The reason this is not bookkeeping: the project axis gates consolidation.
The cross-project fold requires decisions from at least two distinct projects,
and it counted the project name. That is correct only while the set of
project nodes happens to be one-to-one with the registry — and nothing
enforced that. A partly-applied rename leaves two nodes for one project, the
same project counts twice, and a "cross-project" insight gets synthesised out
of a single project's decisions. An identity error on this axis is a synthesis
error, not a misfiled record.A rename also stops being a distributed rewrite. Records, a registry row, two
referencing tables, a graph node and every belonging edge all carried the same
string, so moving it meant rewriting all of them with no stable thing to map
to. Now the identity never moves and the graph cost of a rename is one
property write on one node. -
The fold gate counts identities, and fails closed without one. A project
node carrying no identity contributes nothing to the two-project rule rather
than falling back to its name — a fallback would keep the defect live for the
whole upgrade window, and permanently for any node the registry does not know.
The cost is a fold that does not happen; the alternative cost is a false
cross-project insight. The write path does the opposite and deliberately so: a
project it cannot identify still gets its node and its edge, because a record
with no project edge violates the axis outright.⚠ Not the internal node id, which was proposed and is worse than the name:
without a uniqueness constraint, two nodes sharing a name collapse correctly
under the name and would count as two under an element id. -
An alias is now an alternate label on one identity, not a mapping between
two names. An inactive alias row therefore stays true forever instead of
needing re-pointing every time its target is renamed. -
The promotion ledger records both the name and the identity. The name is
the evidence — what a record was moved onto, on the day it moved — and a
rename must never rewrite it. The id is the durable pointer. Its foreign key
on the mutable name is dropped: a ledger that remembers a name must not be
forced to forget it when that name stops being current. -
A decision's project is now checked against the registry, like a fact's. Decisions were exempt, and the reasoning that exempted them mistook presence for validity: a decision does fail without a project field, but a present name that no registry knew was accepted, and the graph write then minted a project node for it. That is the one way the graph can end up holding a project the registry does not have — and unlike the ingress→outbox window, which leaves the graph behind the registry and always resolves itself, it never does. Retrospectives stay exempt, and that one is a scope statement rather than an oversight: they arrive on their own endpoint and inherit the project of the decision they judge.
Added
-
Both facts and decisions may introduce a NEW project — and the gateway judges the name, not the claim. Work legitimately starts before its project exists: a discussion produces an idea, the idea is saved as a fact, and a decision grounded on that fact commits to acting on it. So
new_projectis available on both record types (--new-projectonsave_decision, and the existing metadata field on a fact), declared once, on the first record that names the project.But a declaration is not a defence, because the client that sets the flag is the client that makes the spelling error. Two refusals now stand in front of the registry:
project_spelling_variant— not overridable. Names reduce to a comparison key (lowercase, alphanumerics only), so a proposal differing from a registered project only in separators or capitalisation is refused outright, naming the spelling to use. No confirmation can make it a separate project, because it is not one — every retired spelling this framework's registry carries as an alias arrived in exactly that shape.project_confusable— refused once, then confirmable. Above a trigram-similarity floor the response names the registered projects the proposal is close to, and the caller proceeds only by naming them back inconfirm_distinct_from(--distinct-from). The confirmation is the neighbour's name, deliberately not a second boolean: a flag can be flipped without reading anything, while the name cannot be produced without having seen it. Each near match is its own claim — confirming one does not wave through another.
The floor is derived, not guessed, and is env-overridable (
PROJECT_CONFUSABLE_SIMILARITY, default0.60) because it depends on how a deployment names things. Measured over all 666 pairs of one live 37-project registry: the closest legitimately distinct pair scored 0.500 and no pair reached 0.6, while typos of a registered name scored 0.78–1.00 and separator/case variants scored exactly 1.00. The default sits in the gap. Too low trains the reflex to override; too high never fires.⚠ Two things this deliberately does not do: it never auto-corrects a near-miss onto the closest registered project (that is inference, and a plausible wrong project is worse than a parked one), and it never refuses a similar name outright (a genuinely separate project with a similar name is real, and the operator is the one who knows).
-
scripts/reconcile_project_identity.py— the graph half of a Postgres
migration, which no migration can perform. It stamps existing project nodes
with their registry identity, matching by name once, and refuses to create a
node or invent a registry row: a node whose name is in no registry is
reported and left alone, because deciding what that means is an operator's
judgement about their own corpus. Idempotent; read-only without--apply; now
part of the documented upgrade path. -
GET /health→project_identity—nodes,unidentified,
mismatched,unregistered,complete. Without it an unfinished upgrade is
invisible: cross-project folds simply stop happening, which looks exactly like
a quiet corpus. Additive — a monitor that does not know the field renders as
before.api_versionis unchanged.
Fixed
-
A fresh install could not register a single project — the schema generator
was dropping IDENTITY columns.schema_init.sqlrenderedid BIGINT PRIMARY KEYwhere the live column isGENERATED BY DEFAULT AS IDENTITY: valid DDL,
applies without error, matching constraints, and every insert then has to
supply the key the database was supposed to issue. Reproduced on a throwaway
database before the fix — the firstINSERT INTO projectsfailed outright.This is the third class of DDL this generator has been found dropping,
after everyCHECKand everyFOREIGN KEY, and all three shared one shape:
invisible to the entire test suite, because the only thing that reads that
file is an install nobody re-inspects. So the fix is in both halves —
the generator emits identity columns, andverify_schema_init.pynow diffs
key generation per column, which is the check that would have caught all
three. Verified by running the verifier against the known-broken file and
confirming it fails, then against the regenerated one and confirming it
passes.
v0.8.43 — an axis declaration is not a topic name
Fixed
-
An axis declaration can no longer enter the graph as a topic. A project
says which project a record belongs to. It is established at first write
from the client's working directory, and it is carried by its own edge — it is
never a subject a record can be about. A previous release closed the typed
door: the enrichment daemon can no longer create a project node, nor point any
relation at one. This closes the untyped door beside it, which is the one the
data actually came through.A name of the form
Project: <something>is an ordinary entity name. It never
touches the label allowlist, so nothing in the typed gate could see it, and it
arrived on the same relation every genuine topic uses. Measured on a live
corpus before the repair: eleven such entities carrying 152 inbound edges,
the largest of them the graph's second-biggest hub with 91. Every record that
merely named a project was being clustered with every other record naming
it — which is a cluster keyed on the axis, not on a theme, and it had reached
the point of anchoring narrative folds.The inbound entity-name gate now rejects the
Project:/Domain:form
wherever names enter the graph.This is deliberately a test of the name's FORM, never a lookup against the
project registry — the obvious implementation, and the wrong one. Registered
project names are frequently real topics in their own right: a project is
often named after the very thing its records discuss, and short registry names
are ordinary English words. Measured on this corpus, one registry row was
simultaneously a system entity carrying 91 inbound edges — a gate that
resolved bare names against the registry would have deleted a hub of true
statements the same size as the axis hub it was meant to remove. A name that
spells outProject:has declared which axis it is on; a bare name has
declared nothing. Keeping it a form test also keeps the check pure — no
database, no I/O.Domain:is rejected before the domain axis exists, on purpose: the axis is
specified, and the same mistake is otherwise made twice.⚠ The gate governs what reaches the graph, never what is stored. A
rejected name stays verbatim in the record's own metadata and remains
searchable there — the episodic tier is left pristine, as it is for every
other name this gate rejects.
v0.8.42 — every install gets the whole manifest, as real copies
Fixed
-
Both skill-delivery paths now agree on what a symlink means, and neither
skips the manifest. v0.8.41 madesync_skills.shphase 1 manifest-driven;
verifying the result on four real installs showed two of them had never
receivedDocumentation/schema.mdat all, while the script reported
success. Phase 2 short-circuited onscripts/being a symlink andcontinued
past everything else — so the symlink, which only makesmemory_bridge.py
auto-current, was being read as "this whole install is current".This is the second time that short-circuit has caused exactly this, and the
first fix is why it recurred:SKILL.mdwas hoisted above thecontinue, a
per-file repair to a per-loop defect, so the next file added to the
manifest fell into the identical hole. Phase 2 now iteratesMANIFEST.txt,
and the short-circuit decides one thing only — whetherupdate_skill.shneeds
to run. -
update_skill.shno longer writes through a symlink. It applied every
staged file withmv, which replaces a symlink with a regular file. So the
self-update path silently undid the arrangement the sync path depends on: a
repo-linked file, auto-current by construction, became a frozen copy of that
day's content — invisible until it had gone stale. The exact mirror of the
defect above, on the other delivery path. A symlinked destination is now left
as a link and reported as such.
Changed
-
⛔ An installed skill file is now always a REAL COPY, never a symlink into a
source checkout. Repo-linkingmemory_bridge.pybought auto-currency at a
price that is only visible once: it binds every agent on the machine to one
checkout's path, so moving, renaming or archiving the project breaks all of
them at once — silently, with the first symptom being an agent failing
mid-task. Staleness is the lesser risk precisely because it is detectable:
every file is content-compared on each sync anddoctorreports version skew.
It also makes the local development path produce the same result as the
shipped one, sinceupdate_skill.shfetches from GitHub and writes real files
for everybody else already.Both delivery paths now replace any symlink they find, and both close the
hazard that creates:cpandcmpeach follow a link, so a naive
implementation would write into the source tree and would report a link
pointing at identical content as "already current" forever. A symlinked
scripts/orDocumentation/is dissolved into a real directory before
anything is written inside it, andsync_skills.shrefuses an install
directory that is itself a link rather than making the source its own
destination. README's four per-agent blocks now copy the whole package —
they previously installed two of the six files the manifest ships — and
AGENTS.mdstates the copy-only rule. -
sync_skills.sh's agent list is env-overridable via
SHARED_MEMORY_SYNC_AGENTS(colon-separated), instead of four$HOMEpaths
baked into a code path. Those four are our agent set, not the agent set —
and hardcoding them is also what made this delivery logic untestable, which is
how the same defect shipped twice.tests/test_skill_delivery.pynow runs the
real scripts against a temporary tree and asserts what actually lands there,
because the whole defect class lives in shell control flow: a test that read
the source for a filename would have passed throughout both failures, since
the filename was there — above acontinuethat skipped it.
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.