Skip to content

Releases: KanenasInGreece/Shared_Memory

v0.8.50 — a change group enforces what it can, and states what it cannot

Choose a tag to compare

@KanenasInGreece KanenasInGreece released this 05 Aug 21:58
54ce6ce

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.py takes 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 one api_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.

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 by verify_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

Choose a tag to compare

@KanenasInGreece KanenasInGreece released this 05 Aug 21:41
79e6568

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.md gains 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 --help strings 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.md is 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_ref is 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_in is 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.py asserts that every
    capture flag a client offers, every ingress refusal the gateway can return,
    every outcome rating, and the worked examples' version and api_version are
    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

Choose a tag to compare

@KanenasInGreece KanenasInGreece released this 05 Aug 21:07
5a544e4

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:
    testing versus Test_Ing scores 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_Monitor is now refused against the
    registered shared-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 --domain was 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 carried asserted_by='inherited' where an assertion should have been
    bare. Now packed into the decision blob, beside project, which is the half
    the gateway resolves a judgement's axes from. vector-skill.py's
    save_decision gains 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

Choose a tag to compare

@KanenasInGreece KanenasInGreece released this 05 Aug 20:39
ac3666a

Added

  • A domain is now a registered SECTION of one project, with an identity of its
    own.
    domain had 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 adds project_domains — keyed on a surrogate
    id, referencing projects(id) and never a project name, with the section's
    label unique only within its project because operations under one project
    and operations under another are different sections that share a word. Its
    alias junction domain_aliases lands 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_unknown with 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. :Domain and DOMAIN_OF are
    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 falsify ontology.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 refused 400.

    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.

  • --domain on the CLI (repeatable) and on save_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 /healthdomain_identity, beside project_identity: registry
    versus graph, plus unattached — a section with no PROJECT_OF edge. 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.py enqueues 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 empty project_domains is the correct
    state for a new install: sections arrive through ingress, like projects.

  • There is deliberately no name-keyed :Domain fallback, unlike the project
    axis. Losing a PROJECT_OF edge 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 internal domain variable holds the project, and
    community_summaries.metadata->>'domain' stores a project name. Nothing in the
    fold path reads a record's metadata->>'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

Choose a tag to compare

@KanenasInGreece KanenasInGreece released this 05 Aug 17:19
803073d

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 :Decision node at first write and projected back out of graph expansion
    into a search hit's adr_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's pg_id does 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 the pg_id every 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 at api_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
    bare list() 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 its adr_props and nothing
    else.

v0.8.45 — a checker that dies for a reason it misreports

Choose a tag to compare

@KanenasInGreece KanenasInGreece released this 05 Aug 15:43
e745ccb

Fixed

  • The two verifiers could not run the way the documentation says to run them.
    verify_schema_init.py and verify_neo4j_init.py loaded their environment by
    importing python-dotenv and returning silently when it was absent. Nothing
    was loaded, so the next connection failed with fe_sendauth: no password supplied — a credentials error reported for what is actually a missing
    dependency
    , sending the reader to check passwords, roles and pg_hba while
    the real cause was the invocation.

    That is worse than an ordinary papercut for two reasons. First, every
    documented invocation omits the dependency
    : AGENTS.md and README.md
    between them show five uv run lines 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 form apply.py has always used (framework shared-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_env in 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

Choose a tag to compare

@KanenasInGreece KanenasInGreece released this 05 Aug 13:21
ce19e9d

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_project is available on both record types (--new-project on save_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 in confirm_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, default 0.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 /healthproject_identitynodes, 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_version is unchanged.

Fixed

  • A fresh install could not register a single project — the schema generator
    was dropping IDENTITY columns.
    schema_init.sql rendered id BIGINT PRIMARY KEY where the live column is GENERATED 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 first INSERT INTO projects failed outright.

    This is the third class of DDL this generator has been found dropping,
    after every CHECK and every FOREIGN 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, and verify_schema_init.py now 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

Choose a tag to compare

@KanenasInGreece KanenasInGreece released this 05 Aug 10:57
82f4635

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 out Project: 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

Choose a tag to compare

@KanenasInGreece KanenasInGreece released this 04 Aug 16:07
b59cf3a

Fixed

  • Both skill-delivery paths now agree on what a symlink means, and neither
    skips the manifest.
    v0.8.41 made sync_skills.sh phase 1 manifest-driven;
    verifying the result on four real installs showed two of them had never
    received Documentation/schema.md at all
    , while the script reported
    success. Phase 2 short-circuited on scripts/ being a symlink and continued
    past everything else — so the symlink, which only makes memory_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.md was hoisted above the continue, 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 iterates MANIFEST.txt,
    and the short-circuit decides one thing only — whether update_skill.sh needs
    to run.

  • update_skill.sh no longer writes through a symlink. It applied every
    staged file with mv, 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-linking memory_bridge.py bought 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 and doctor reports version skew.
    It also makes the local development path produce the same result as the
    shipped one, since update_skill.sh fetches 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: cp and cmp each 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/ or Documentation/ is dissolved into a real directory before
    anything is written inside it, and sync_skills.sh refuses 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.md states the copy-only rule.

  • sync_skills.sh's agent list is env-overridable via
    SHARED_MEMORY_SYNC_AGENTS (colon-separated), instead of four $HOME paths
    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.py now 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 a continue that skipped it.

v0.8.41 — a rename is one transaction; the Neo4j constraints are provable

Choose a tag to compare

@KanenasInGreece KanenasInGreece released this 04 Aug 15:25
fea1f12

Fixed

  • Renaming a project is now one transaction per pair, applied whole or not at
    all.
    normalize_projects.py used 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 own note. 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. Never ON UPDATE CASCADE,
    which would do the same damage with no trace at all.

  • --dry-run is 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.sh no longer refreshes a hardcoded subset of the client
    package.
    Phase 2 and the parity test both read MANIFEST.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.md was
    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.alternatives can no longer be shredded into single characters on
    the read path.
    The graph-expansion projection called list() 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, and list() 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.cypher is 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: MERGE keeps 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 make CREATE CONSTRAINT fail — 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; --apply creates
    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.cypher cannot:
    Neo4j refuses CREATE CONSTRAINT while 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. --apply drops the conflicting index first, which costs nothing
    — a uniqueness constraint creates its own backing index on the same key.

Changed

  • AGENTS.md upgrade path and README §6 now cover the Neo4j side. The
    documented upgrade ran apply.py and restarted, which covers Postgres and
    says nothing about Neo4j; and README described neo4j_init.cypher as
    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.