CU26 — What an edit did, before paying to find out
CU25 built the pre-pass and then used it for one thing. The engine names the whole plan with nothing executed, asks which of those answers are already there, and skips what only fed one — and then throws the names away.
They are worth keeping. Two versions of one graph name a node differently exactly when its recipe changed, so comparing two sets of names says what an edit did. The question is the one everybody with a cache asks out loud on a Tuesday afternoon — did I just invalidate the encoder, or only the head? — and today it is answered by running and watching which nodes take time.
Executor::foreseen is pub for that, foreseen_json is the bridge, and
somatize.foreseen is where it is asked:
from somatize import foreseen
foreseen.names(g) # {node: name}, nothing executedforeseen.unneeded(g, x, store=store) # what would not have to run at allforeseen.changes(before, after) # what the edit didforeseen.snapshot(g) # the same, kept for a later comparisonFindings and not buckets, and the case that decided it
Section titled “Findings and not buckets, and the case that decided it”The first shape was a partition: every node into exactly one of changed,
downstream, stale, added, gone, unknown, same. It reads well and it
is wrong, and the counterexample took one run to appear — edit the encoder’s
code and bump the head’s salt:
{'embed': ['STALE'], 'head': ['SALTED', 'SUSPECT']}The head’s name moved, so it runs again: SALTED is true. And it runs on what
the encoder handed back, which is the old code’s answer: SUSPECT is true too.
A partition can carry one of the two, and the one it carries is this node will
be recomputed — the reassuring half of a node about to compute the wrong
thing.
So the shape is {node: [finding, ...]}, which is what somatize.health
already answers with, and for the same reason: what happens to a node is more
than one fact. A node with nothing said about it is fine, and absence being the
good answer is what keeps the ones that matter readable.
| finding | what it says |
|---|---|
CHANGED | its shape moved: another class, or who feeds it |
RESETTLED | it is frozen at another state — other weights, another version |
SALTED | its salt moved |
DOWNSTREAM | none of those moved and its name moved anyway |
STALE | its name did not move and its code did |
SUSPECT | something above it is STALE |
ADDED / GONE | it is in one graph and not the other |
UNVERSIONED | its answer is kept and nobody can say whether its code moved |
UNKNOWN | it cannot be named on one side or the other |
CHANGED against DOWNSTREAM is what makes a list of forty nodes readable: one
of them is where the edit is and the rest is what inherited it. Who feeds a node
is part of its shape because rewiring it moves its key without touching anything
the node is made of — and that is an edit, not something somebody else did.
One question, or two
Section titled “One question, or two”The first three are the same finding split three ways, and the split came from the other end of the wire. A sibling slice — a tree of experiments, one snapshot per commit — asked what did the code do, and to that question a node re-frozen at Tuesday’s checkpoint is a false positive: the architecture is the same one, trained again. To the question this module opened with — does my cache still hold — it is not a false positive at all: the answer under that name really is a different answer, and the name moving is the cache being right.
Both are true, and neither is a reason to give the other the wrong answer. So a
name that moved says which part of the recipe moved it, and the two readers
take the part they came for: all three, or CHANGED alone. Weights belong to a
version; they are not a version.
UNKNOWN is the absence CU25 already gives up on, said out loud: a .mapped()
node is named out of the content of its items and nothing under it can be named
either. It is the one place where cannot tell has to survive all the way
to the reader, because the alternative reads as checked, and fine.
STALE, which is the finding the key cannot give
Section titled “STALE, which is the finding the key cannot give”The fingerprint of the code is deliberately not in the key — CU13 decided
it, and the reason still holds: a cosmetic refactor would invalidate half the
store in silence. It is kept beside the value and compared on a hit, which
turns that into a line on stderr.
The cost of that decision is that editing the body of a forward renames
nothing. A diff that only compared names would answer nothing changed to the
very edit being asked about — which is why the fingerprint is looked at here,
where it is an opinion and not an invalidation. STALE is the finding that
says you should have bumped the salt, and the two are the same edit answered
and not answered:
edited, salt bumped {'embed': ['CHANGED'], 'head': ['DOWNSTREAM']}edited, salt not {'embed': ['STALE'], 'head': ['SUSPECT']}And it reaches down, which is what SUSPECT is. A stale node hits, so
everything under it goes on being fed the old code’s answer, whatever became of
its own name.
UNVERSIONED, which the notebook found
Section titled “UNVERSIONED, which the notebook found”It needs both fingerprints, and a class with no source to read has none — a
notebook cell, an exec. The first draft treated that as no opinion and said
nothing, and writing examples/11-what-an-edit-did.ipynb is what showed what
that means: in a
notebook every node is defined in a cell, so a graph compared with an edited
copy of itself came back {} — nothing to report about an afternoon of edits,
in the one place where the question is asked most.
So the absence is a finding. Its scope is the scope a version is recorded at —
_note_the_code computes one only for what is kept, because parsing an AST
for a node nobody remembers anything about would be paid by everyone who
declares a graph. A graph with no cache in it gets no opinion about its code and
is not told so once per node.
Two graphs, or two snapshots
Section titled “Two graphs, or two snapshots”changes takes either. A Graph is a live object and two versions of one
module do not coexist in an interpreter, so comparing two commits means
comparing what was written down: snapshot(g) is plain JSON with the names
already worked out — the shape of each node, its state, its salt, what is kept,
the fingerprints, and the edges, which SUSPECT needs to reach down.
The cut is the one this project keeps making between the fact and whoever draws it, and it is what lets the sibling slice delete its own comparison and consume this one. Two snapshots are comparable when they were taken with the same input, which the default — none at all — always is.
What it does not need
Section titled “What it does not need”A store’s contents. Naming is the Keeper’s and the keeper is the store’s —
the core computes no hash — so a store is where the hash function comes from and
nothing else is asked of it. store=None opens a temporary directory and gives
the same names. unneeded is the only one of the three whose answer is about
what is in there, and it is the only one that demands one.
The input. Every key on both sides carries the same hash of it, so which
input it is cancels out of every comparison. changes(before, after) with
nothing at all gives the same findings as with the real batch, and does not pay
the 121 ms of weighing it that CU24 measured.
What is not in it
Section titled “What is not in it”- The figure.
overlaidputs findings on a graph, and its channel is health: the outline turns red and red means ill. A node that changed is not ill, so it would need a channel or a colour of its own, and that is a drawing decision and not this slice’s. - A silent edit above a cached node. A version is recorded only for what
is kept, so a code change in an uncached node moves nothing: its key does
not carry its code, and the cached node below it goes on hitting. The runtime
has the same blind spot for the same reason — it compares a fingerprint on a
hit, and there is no hit to compare on. Closing it means versioning every
node at
somatize, which is CU13’s trade to reopen and not this slice’s. - A fan-in whose order does not matter.
(a | b) >> cand(b | a) >> cnamecdifferently, though what reaches it is a dict keyed by node id and the order is nothing to the node. A conservative miss, nobody’s answer is wrong, and it is CU13’s to decide — not something a diff should hide._recipecarries the order for exactly that reason: it says the truth about what the key does today.
Questionnaire
Section titled “Questionnaire”A name, without a run (soma-core/tests/unit/execution.rs,
soma-python/tests/test_foreseen.py)
- the names foreseen are the names things are kept under
- nothing ran to find out — a node that cannot run at all still has one
- and it does not depend on having a store
- what cannot be foreseen is missing rather than wrong
- without a keeper nothing is named, which is the silence a run gets
- what is already kept says what would not have to run
- and asking that needs somewhere to look
What an edit did (soma-python/tests/test_foreseen.py)
- a graph compared with itself has nothing said about it
- a changed recipe renames what is under it and nothing above it
- and
CHANGEDagainstDOWNSTREAMsays which is which - another class under the same name is a change of shape
- other weights under the same code are not
- and two parts of one recipe moving says both
- rewiring a node is a change of its own and not an inherited one
- a node that is only in one of them is not a change to a name
- a mapped node cannot be told about either way
- the input cancels out of the comparison
The code that changed and the name that did not say so
(soma-python/tests/test_foreseen.py)
- an edit the key cannot see is said out loud
- what is under a stale node is not told it is fine
- and a node that recomputes still recomputes from a stale answer
- a class nobody can version says so rather than nothing
- and a node nothing is kept of is not told off for having none
- a bumped salt is what
STALEis asking for
Two graphs, or two snapshots (soma-python/tests/test_foreseen.py)
- a snapshot answers exactly as the graph it was taken of
- and it survives the graph it came from, through JSON
- what is under a stale node is found through a snapshot too