What the agents did, read out of the text they wrote, without running any of it.
The corpus memview serves is what sessions wrote down. The reader mines what
they did, from transcripts under ~/.claude/projects.
Target: say what a command named and what it changed — which constant a name was bound to, which files a literal loop touched, which machine a path belonged to, which program a heredoc carried and what that did in turn. Exceptions should be few, and each a known kind rather than a shrug.
It is an abstract interpreter: evaluate as far as the text determines, then stop. What is undetermined is recorded as undetermined and counted, so the gap is a number rather than a silence.
execution-model.md specifies the syntax layer underneath,
reader/src/syntax/, and the three gates that govern it;
evaluator.md the evaluator beside this chain, which says what
the files will hold rather than which were touched; and
concept-model.md designs the layer above — lifting these
effects into the concepts they served, and lowering a concept back to a
command. The chain below reads
through it — reader/src/project.rs puts that tree into the flat shape the
rest of the chain consumes, and project::read is the entry point every artefact
is built from.
shell.rs and its pest grammar are still here, and are not dead: they are the
second answer that makes a disagreement mean something. --bin projection asks
both readers the same question over one corpus — 96.3% of 145,219 distinct
commands read identically, and the 3.7% that do not are ranked. That comparison
is what found six defects nothing else could see; see Two readers, below.
Three readings of one command, each exact in its own domain and refusing by name outside it, and each with a judge that is not its own code. They are easy to run together, and the names used for them below are not interchangeable.
| question | answers in | refuses by | judged by | |
|---|---|---|---|---|
| syntax — execution-model.md | what is this text | a tree | construct | the round-trip law; bash's own printer; bash -n |
| reading — this document | what did it touch, read from history | a named path; a described set S ⊆ L; a counted remainder |
reason (Why) |
the PATH-shim oracle on fixtures; the second reader |
| evaluator — evaluator.md | what will the files hold | a file's exact text, or a finite set of texts, or its absence | construct (Unfollowed) |
the file after the call, on every live call |
| concepts — concept-model.md | what was it for | a typed, invertible concept with holes | reason (Why) |
lift ∘ lower = id; the effect reading; the author's description |
The reading is an abstract interpretation in one sense only: it evaluates as far as the text determines and stops, and Why this is not sound abstract interpretation below says what it refuses to do. The evaluator is concrete: given the file, it computes the file. The two share the tree, the operation tables and the subjects; they do not share a domain.
Each stage's authoritative explanation is its module doc-comment.
| module | question |
|---|---|
reader/src/syntax/ + project.rs |
which commands does this script run? |
reader/src/syntax/python/ |
the tree for the Python inside it (execution-model.md) |
reader/src/shell.rs + shell.pest |
the same question, second answer — the check on the first |
reader/src/shell_ops.rs |
what does one command do, to which paths? |
reader/src/python.rs + python.pest |
same, for inline Python |
reader/src/javascript.rs + javascript.pest |
same, for inline JavaScript |
reader/src/sql.rs + sql.pest |
same, for inline SQL — but in TABLES, not files |
reader/src/program.rs |
the types both carried readers answer in |
reader/src/shell_files.rs |
resolved against a cwd, which files? |
reader/src/predict/ |
what will the files it writes hold afterwards? (evaluator.md); --bin predict-report ranks what it does not follow yet |
reader/src/reading.rs |
the whole corpus surveyed, as a value the report and both apps draw |
reader/src/activity.rs |
what kind of work — test, build, edit, deploy? |
reader/src/concept.rs |
what was it FOR — and back to a command again (concept-model.md) |
reader/src/episode.rs |
which sequences of acts recur across an instruction's calls (concept-model.md, instrument 5) |
reader/src/doing.rs |
timeline: agent · minute · repo · kind · count · verdict |
src/commits.rs |
what the repositories recorded, renames followed |
src/agents.rs |
who works where — the roster behind /agents |
src/couse.rs |
which memories are used together in one turn |
The crate split is a boundary, not tidying. What does this text mean lives
in reader/; whose work was it stays in the viewer. That lets the console —
which spawns processes on the root-of-truth Mac — read a command without linking
a viewer carrying routes, auth and configuration. reader/src/lib.rs has the
reasoning.
Derived, never verbatim. No command line, prompt or output text reaches any
artefact — typed structure and counts only. The rule and its one lifted half
(timelines, 2026-08-02) are in doing.rs.
cd health && nix develop -c bash -c "sed -i 's/a/b/' src/geo/velocity.ts"
shell.rs two commands. The inner script is ONE WORD — quoting is the
parser's job, meaning is not.
shell_ops.rs ChangeDir{to:"health"} · Carries("-c") unwraps argv to `bash -c
…` = Nested{script}, re-parsed in its own scope
→ Transform{program:"s/a/b/", in_place:true, paths:[…]}
shell_files.rs cwd is …/health, so the relative path resolves; in_place makes
it a write.
activity.rs an edit.
Stops are deliberate. An unresolvable cd makes the cwd unknown, not stale, so
the relative path names nothing rather than the wrong thing. Under ssh,
everything belongs to that machine and never reaches the local index.
An invented path makes every downstream count a lie. Every refusal errs toward undercounting.
- Nothing is looked up on disk. Reading history, that filesystem is gone. Predicting a live call, the console reads the files and passes their text in as a value — the reader still opens nothing (evaluator.md).
- Nothing is expanded beyond
~and$HOME, the one knowable value. - A word needs a
/, a~or an extension to be a path. Costs real reads (rg foo srclosessrc); keeps flag values from becoming filenames. - A remote path belongs to the remote machine.
- Both arms of an
if, every arm of acase: sometimes reached. At most one ran. - A function definition's body: sometimes reached. Defining it runs none of it.
An undetermined subject is counted, not dropped. Otherwise a command using a file nobody can name is indistinguishable from one using none.
A glob is not a shrug. for f in *.log names no file — the directory it was
answered against is gone — but
⟦*.log⟧ = some S ⊆ L(*.log) ∩ Files(dir, t)
is an unknown finite subset of a known language, which $(git rev-parse HEAD)
is not. Still counted as not named, since a subset of a pattern is not a file.
What it buys is falsifiability: the oracle runs the loop for real and asserts
every path bash touched matches the pattern (S ⊆ L), needing no old filesystem.
Bounded stops where the shell stops being concatenation. ${f%%:*} is a rational
transduction and would need an automaton, so it stays opaque. The automata are
not built; the population needing composition is small enough to read off
shell-files.
A loop body is certain only if the loop certainly ran it. while/until
test before the first iteration, so empty input runs the body zero times. A for
is the other way: with nullglob off, a pattern matching nothing expands to
itself, so even a glob loop runs its body once. The rule is bash's.
-
Pick the right corpus, and say which. Two rates are comparable only against one denominator, so when a figure has to hold across a change, copy
~/.claude/memview/cache/bash-corpus.jsonlfor the duration and pass the copy. ⚠~/.claude/corpus/union.jsonlwas that frozen copy and is RETIRED (2026-08-29): it rested on the claim that the live transcripts shrink, and #1240 measured otherwise; the live corpus grows, which moves a rate just as surely. Rebuild from transcripts when the question is about what is current. See the corpus discipline in execution-model.md.⚠ Absolute figures in this file dated on or before 2026-08-23 were taken over a union that held one era TWICE — 141,545 of 298,895 rows, collapsed on 2026-08-24 by memview#1130. The conclusions stand, because they rest on ratios and on before/after diffs of the same corpus, where the duplication cancels on both sides. The counts do not: they are inflated, and any "biggest first" ranking taken then was weighted toward whatever spanned the transition.
The STANDING figures here — the opaque-shapes table and the current coverage — were re-taken on 2026-08-24. The dated before/after tables were deliberately NOT: an ablation is a record of what was run, and its two halves are only comparable to each other.
-
Change the table or the grammar.
-
Re-run the report. The number that matters is the one that moved; the failure list is the next thing to build.
-
Ablate. Undo the change; the new test must fail.
⚠ Read counts, not rates. The denominator is commands run, not written, so
it moves whenever loop unrolling does and figures either side are not comparable.
It moves counter-intuitively: unrolling $(seq N M) adds commands and file uses
but opens loops holding commands the verb table lacks, so more is understood
while the rate falls.
⚠ A parse rate is not a coverage figure. A command that parses can hide the
ones inside it; "$( … )" was opaque while every report looked clean.
⚠ A bucket is only as good as the guess that fills it, and a guess with no
test does not announce itself. opacity sniffs the language of a heredoc body
from a mark; one line opening import filed TypeScript, Kotlin, Swift and Lean
alike as Python, and 731 of the 1,154 bodies under that label were not Python.
Nothing looked wrong, because a census prints the same shape of number either
way. --why <label> opens a bucket and names the mark that filed each body.
Three ways a figure has misled, each written up where it happened: a rate hiding
a trade (reader/examples/tree-sitter-probe.rs), a census counting text already
read, and a census mislabelling what it counted (both reader/src/bin/opacity.rs).
Mining is offline; scripts/sync.sh pushes the artefacts to the pod.
cargo run --release --bin agents # → ~/.claude/memview/cache/agents.json + doing.json + effects.json
cargo run --release --bin couse # → ~/.claude/projects/-Users-user-Code/couse.json
cargo run --release --bin bash-corpus > ~/.claude/memview/cache/bash-corpus.jsonl # rows carry session + turn, the episode
cargo run --release -p reader --bin reading-json # → ~/.claude/memview/cache/reading.jsonreading-json is the survey, mined rather than computed on request. It
takes ~13 seconds over 146k commands — fine for a report somebody waits on,
wrong for a page — and the artefact is 8 kB, so both servers hold it in memory
and answer instantly. claude-sync.sh runs it in the nightly, after the corpus
is mined, because it reads what that step writes: running it first would
describe last night's corpus and stamp it with tonight's mtime, a staleness
nothing downstream could detect.
It carries counts and command NAMES, never a command line — effects.json is
where verbatim text lives, and this file is small enough to embed in a page.
The reports take any corpus file, and default to
~/.claude/memview/cache/bash-corpus.jsonl. ⚠ That file is re-mined nightly, so two
figures either side of a night are not comparable — when a figure has to hold
across a change, copy the corpus somewhere for the duration and pass the copy.
⚠ Reports need -p reader; a plain --bin from the workspace root fails with
no bin target in default-run packages.
cargo run --release -p reader --bin shell-report -- <corpus> # grammar
cargo run --release -p reader --bin shell-files -- <corpus> # semantics
cargo run --release -p reader --bin activity-report -- <corpus> [--sample KIND]
cargo run --release -p reader --bin python-report -- <corpus> [--why|--sample]
cargo run --release -p reader --bin javascript-report -- <corpus> [--why|--sample]
cargo run --release -p reader --bin opacity -- <corpus> [--why|--dump <label>]
# what the SQL touched, and what it still cannot read
cargo run --release -p reader --example sql-corpus -- <corpus>
# the parse tree for one SQL script, when a clause silently fails to match
cargo run --release -p reader --example sql-probe -- "SELECT 1 FROM t"
# the same for one Python program: which rule bailed, and what the reader made of it
cargo run --release -p reader --example python-probe -- "for p in glob.glob('c/*.json'): open(p)"
# do the two readers agree about what ran? — and where they do not
cargo run --release -p reader --bin projection -- <corpus> [--show <n>] [--only <bucket>]
cargo run --release -p reader --example roundtrip-probe -- <corpus>
cargo run --release -p reader --example unparsed-probe -- <corpus>
# which nested scripts will not read, by construct AND by who handed them over
cargo run --release -p reader --example nested-why -- <corpus> [--reason NAME]
# what changed when a container payload stopped being joined into a script
cargo run --release -p reader --example remote-argv-check -- <corpus>
# is there Python we never noticed was Python? — and does what we found parse?
cargo run --release -p reader --example python-calls -- <corpus>
# does a real runtime agree about what never ran? (both readers)
cargo run --release -p reader --example python-raised -- <corpus> [--all]
cargo run --release -p reader --example javascript-raised -- <corpus> [--all]
cargo run --release -p reader --example tree-sitter-python-probe -- <corpus>
# the syntax tree: coverage, the ranked refusals, and all three gates
cargo run --release -p bash-oracle --bin syntax-report -- <corpus> [--oracle] [--why SUBSTRING]
# the Python tree: how much reads, whether the round-trip law holds, the ranked refusals
cargo run --release -p reader --bin python-syntax-report -- <corpus> [--show LABEL N]
# the Python the shell tree runs, found by node, against what the flat chain finds
cargo run --release -p reader --bin python-embed-report -- <corpus> [--show tree-only|flat-only N]
# the Python tree against CPython's own parse, and CPython compiling our print
cargo run --release -p bash-oracle --bin python-gate -- <corpus> [--show VERDICT N]
# every distinct Python program the corpus runs, one JSON string a line — to measure with CPython
cargo run --release -p reader --example python-sources -- <corpus> > programs.jsonl
# which sequences of acts recur across the calls of one instruction, with the author's verb beside each
cargo run --release -p reader --bin episode-report -- <corpus> <said.jsonl> [--n 3] [--show 25]--why <substring> prints the commands behind every use of a matching path, and
settles arguments. opacity asks the inverse question: of the text these
commands carry, how much does nobody look inside, and who handed it over.
⚠ Run these rather than trusting any written-down number. Chaining with ; cat
reports the cat's exit code.
The chain reads through the tree; the pest grammar is kept as a second answer.
shell.rs and syntax/ answer the same first question — which commands does
this script run — from grammars that share no code, so each is an oracle for the
other. project.rs puts the tree into Simple, the flat reader's own shape, and
--bin projection diffs them command by command over one corpus.
⚠ Keep both, and keep them apart. The moment the flat grammar becomes a call
to the tree, nothing checks either of them again — which is why the switch was
made at the chain's entry points and not by redefining shell::parse. The
comparison is worth its keep: run once, it found six defects and five of them
were live, taking agreement from 79.3% to 96.3% — of 134,622 distinct commands
then, and the rate has held as the corpus grew past 145,000.
grep -E "\s+" had been read as grep -E "s+" in 21,481 of them. Each rule is
written at the code that carries it; the report ranks what is left.
How to read a bucket. A disagreement is not a defect until you look, and it can belong to either side:
- A spelling difference is not a reading difference. An argv string holding
$(a|b)is one reader's source text and the other's reprint of a parsed tree, so2>/dev/nullcomes back as2> /dev/nulland${x}as$x. Nothing reads inside an expansion — its value is undetermined either way. Roughly 2,600 commands, andreader/tests/suite/projection.rsasserts they stay spellings. - One systematic spelling difference outnumbers every real misreading, which
is why the buckets are named apart and why
--onlyexists. - The largest real one is structural: 1,048 loop bodies whose
&&condition the flat grammar cannot carry across the;beforedo, becausedois a word to it. That gap is not fixable there — having the loop is what the tree is for.
⚠ project is what a script says; run_out is what it did. One command
per command written, against loops the text determines run out into their
iterations with a zero-times body demoted. The evaluation is the whole of the
difference. Do not move the zero-times rule into project: it is a statement
about running, the flat chain draws the line in the same place, and putting it
earlier reports a disagreement that is only a difference of stage.
⚠ A flat outer parse cannot feed a tree nested one. shell.rs hides a
heredoc body inside its own delimiter so a nested re-parse can still see it, and
only that reader decodes the marker — mixing the two silently loses every nested
python3 - <<PY. Read the outer and inner scripts with the same reader.
A payload is text only when a shell is on the far side. ssh host '…' joins
its words and hands them over, because ssh really does; bash -c and
nix-shell --run take a script. kubectl exec -- prog, docker exec,
subprocess.run([…]), spawnSync and execFileSync reach exec() with no
shell — nothing re-splits their words and nothing removes a quote — so their
payload stays an argv to classify, never text to parse. That costs no second
parse and therefore cannot fail one, which is the same reason Verb::Carries
exists.
⚠ Getting that wrong was 700 of the 769 nested refusals, and it looked like a
grammar gap for as long as nobody asked who carried the text. The count is
ranked by the construct that stopped the parse, and at 813 occurrences 766 of
them were Grouping — unmatched ( ). nested-why grew a second table, by the
command that HANDED the payload over, and 686 came from one shape:
kubectl exec … -- <a program that is not a shell>. Joining those words back
into a string put SQL and JavaScript in front of the shell grammar, where
SELECT ROW_COUNT() AS deleted is unmatched grouping and so is every node -e
body with a { in it. Kept as an argv and classified: 813 → 73.
The rule that generalises: a refusal names the construct that stopped the parse, never the reason the text was there. Rank by carrier as well as by construct.
The exchange was measured rather than assumed, because "fewer refusals" and
"fewer findings" can be one change seen twice. --example remote-argv-check
computes both readings of all 3,034 container payloads: same subjects for 3,026,
four paths gained that the old reading lost inside refused payloads, one
dropped that it had invented (/p, the tail of a sed address range the join
split at a ;), and one recovered by teaching it that su -s /bin/sh -c names
its shell in a flag. memview#1028.
What is left refusing is 73, ranked as before; an earlier reading of that list —
when it stood at 405 — found 20 distinct scripts, 14 of which bash -n refuses
too, mostly a \' inside single quotes that ends the string early.
Three questions. Does what we found parse? tree-sitter-python reads 14,982
of 15,074 programs (99.4%).
Did it run? A different question, and the one that decides whether a file
operation is a fact. python.pest accepts a broken program as happily as a
working one and hands back the paths it mentions, which are then recorded as
work that happened — so python::did_not_run refuses the one shape confirmed to
raise, an escaped outer quote inside an f-string replacement field, and the
reader keeps nothing from it.
⚠ The over-claim direction is the one that costs. Flagging a program
discards everything it named, so a false positive destroys knowledge while a
false negative merely fails to gain any. --example python-raised hands every
program to real CPython and answers both ways: of 12,240 distinct programs
3.12.14 refuses 72, the reader flags 39, and it flags nothing CPython
accepts. Of the 33 it lets through, 19 hold an unexpanded shell variable and
ran perfectly well once the shell substituted it — two rules meeting, not a
defect — and 14 are genuinely broken, mostly a heredoc cut short.
⚠ And tree-sitter is not the authority for this. It accepts 10 of the
programs CPython refuses; an editor's parser is built to keep going. An earlier
version of the rule fired on any backslash in a replacement field and threw away
two programs that worked, because PEP 701 permits f"{'\n'.join(x)}" on 3.12.
Ask the interpreter that ran the corpus.
And is there Python we never noticed was Python? That one a coverage figure
cannot answer, because its denominator is what we found — a call the verb table
does not recognise is absent rather than wrong. --example python-calls asks
it from outside the table, by the shape of the argv: 17,453 commands name an
interpreter and none is missed. It found one gap, python3.12, which matched
neither literal spelling the table carried.
⚠ That probe must stay looser than shell_ops::is_python. It is the
instrument and the table is what it measures; if it asked the table's own
question it could never find a spelling the table has not been taught. Which is
also why python313 — a nixpkgs attribute in nix-shell -p python313, 68 of
them — is matched and then shown to run nothing, rather than filtered out early.
Image.open(p), wave.open(p) and Store.open(p) read exactly like p.open()
and are the opposite shape: the receiver is a library and the argument is the
file. The reader asked whether Image was a path, so 323 operations named
nothing while the path sat resolved one slot away — in 263 of them a literal,
in the other 60 a loop variable that is unknowable either way.
⚠ The rule is three pairs the corpus writes, not "an imported name may be
called". webbrowser.open takes a URL, which has a / and an extension and
so passes every path test ever written. The corpus was asked before the rule was
written: it holds no webbrowser at all, and window.open, MatDialog.open
and caches.open are the JavaScript side rather than import-bound Python.
⚠ A qualified name the table does not know keeps its old reading, and that is
what protects the denominator. Reading every call on an import as a library
call would take OUT.write_text(s) out of the file-operation count entirely —
a rate rising because operations stopped being counted, which is the trap this
reader is judged on. So the qualification is one attribute deep and applies only
when callable already answers for the pair.
| before | after | |
|---|---|---|
| file operations | 26,536 | 26,536 |
| named a file | 20,304 (76.5%) | 20,567 (77.5%) |
| named none | 3,351 | 3,088 |
| one of a known set | 2,881 | 2,881 |
The denominator did not move and bounded did not move, so the rise is
263 subjects newly named and nothing else — the check that says which half of
the rate moved. open fell by exactly 323 as the three qualified names took
their own rows.
A loop variable was the largest unnamed shape left, and most of those loops say
what they range over. for p in glob.glob('captures/*.json') binds p to one
of captures/*.json — a language whose members the filesystem of the day
chose, with a locus that is certain even though the leaf is not. A literal list
is the same object with the language written out.
file operations 26,536 → 26,536
named a file 20,567 → 20,573 (+6, and not from this rule — below)
one of a known set 2,881 → 3,186 (+305)
of them located 1,312 → 1,514 (+202)
named none 3,088 → 2,777 (−311)
⚠ The header has to hold the iterable, and until now the grammar threw it
away. binder stopped at the name, so for p in xs reached the reader as
three unrelated statements and the only place the language appears was gone.
shell.pest emits a loop head for the same reason, and project.rs says so.
Four things that had to be right, each of which reads as a detail until it is a wrong answer:
:is abinop— it has to be, for dict entries, slices andlambda— so an unguardedvaluereadsfor p in xs: open(p)as one value of two operands and swallows the loop body, which then never reaches the reader as a statement at all. The iterable stops at the colon.- Once the header owns the iterable, nothing else reads it.
for line in open(p)opens a file whatever the body does, and the first version dropped that use — a shrinking denominator, the one direction a coverage figure must never move on its own. scopetook binder names as "everything after the keyword", which after the change bound the loop variable to the words of its own iterable.- One target name, bound exactly once.
for k, v in d.items()yields a pair, so a language over the sequence belongs to neither name; and a name the program also assigns has left the loop's space by the time it is used.
sorted, list, set, tuple and reversed are transparent, because order
and duplicates are not part of a language — and 250 of the 531 glob loops are
written sorted(glob.glob(…)). ⚠ enumerate and zip are not: they yield
tuples, so reading through one binds the wrong name to the language.
Deliberately left: a locus with no language. for f in os.listdir(BACKUP)
knows the directory and nothing about the leaf, and Program::located is an
annotation on bounded rather than a second account — filing this there would
count an operation that is in neither. 50 uses, and it needs the field to grow a
meaning first.
⚠ trailer includes call, so in (…) parsed as a call to a function named
in. In for line in (base / 'g.log').read_text() the reader saw a call to
the keyword in — which the table answers "does nothing" — and .read_text()
lost its receiver. Six reads over the corpus were discarded that way, and they
are the whole of the named a file rise above: the loop rule contributed no
names at all, which is what it was built to do.
Found because the +6 did not match the prediction and was chased rather than
accepted. ⚠ Two comparison runs said the readers agreed and both were
wrong: a worktree at the old commit shares one cargo target directory with the
checkout, so cargo build reported Finished in 0.10s and left the new binary
in place. The reliable instrument was two binaries copied aside and checked
against a fixture that exercises the change.
Of the 3,203 unresolved operations reaching record, 81% are a plain bare
name, so what is left is a question about what the reader knows about names
rather than about syntax it cannot chew.
901 28.1% a loop variable (`for`)
875 27.3% assigned an expression
601 18.8% not a bare name
335 10.5% never bound in this program
323 10.1% bound by `import`
168 5.2% mixed bindings
⚠ Two censuses of this reader classify different things and their rows must not be read against each other. The 2026-08-24 one classified how a name was BOUND; this one classifies the SUBJECT EXPRESSION the reader looked at. Reading the second's rows as the first's is what produced a hypothesis — that "not a bare name" and the library-call gap were one shape — which the measurement then refuted.
Both censuses above were temporary probes on record's failure branch,
reverted after they were read — which is why sizing each slice began by
rebuilding an inventory the last slice had already had and thrown away. The
reason is now a field. Every missed operation carries a program::Why, filed
beside the by-call unresolved count and summing to the same total, and the
--why line of python-report/javascript-report prints it:
named none 3154, and why — each row names the rule that would shrink it:
from outside the program 425
a name bound to a computed value 1721
a loop over what this cannot read 645
an expression with no value here 363
⚠ Each row names the rule that would shrink it, and one row names none.
Computed, Loop and Expression are backlog — more assignments read, more
iterable languages, more value rules. Outside is the boundary: a function
parameter, a name bound where the program cannot see, a value that never was in
the text. It does not shrink, and measuring it is the honest floor under every
coverage figure this reader reports. The rows are not comparable with the
2026-08-24/25 ones above — different denominators, and a corpus change between
them — which is exactly the trap the by-reason keying removes, since it is
regenerated from the live corpus rather than transcribed.
What the loop row iterates is the next thing to build, and most of it needs no new analysis: 531 of the 901 loops range over a glob, whose locus is certain even though its leaf is not, and 130 over a literal list, which is a bounded set. Both are machinery that already exists.
text returned None at the first {, and the comment there — "names a file
whose name is not here" — is true of the hole and false of everything beside it.
f"data/{name}.stream" is not unknowable: it is data/*.stream, the same
object a glob.glob argument produces, with a certain locus and an uncertain
leaf. So shape renders the literal text as a glob and record files it as
bounded, never as a use.
one of a known set 3,308 → 3,499 (+191, and 1,548 → 1,636 located)
named none 2,946 → 2,755 (−191)
named a file 22,154 → 22,154
Conserved to the unit, and the middle column is the point: no operation gained a
path, 191 gained a language. open accounts for 141 of them, glob.glob 20,
Image.open 16.
⚠ Most f-strings are not paths, and this is the rule's whole risk. 94.3% of
the corpus's 7,826 interpolated f-strings are print formatting — *=*,
Bearer *, #* [*] * — and filing those as bounded subjects would invent
thousands. path_shaped refuses whitespace, a :// scheme, a literal that is
nothing but separators, and anything with neither a literal directory nor a
literal extension.
⚠ Sizing it against every f-string would have priced the rule at 2.6%. The
population is f-strings in a file operation's path argument, where the
distribution inverts: 44.3% carry a literal directory, 33.9% a certain filename.
--example fstring-shapes is the instrument, and it deliberately does not call
path_shaped — an instrument that asks the implementation's own question cannot
show the implementation being wrong.
⚠ text is unchanged and must stay so. It serves every string literal, not
only paths; returning a pattern from it would change every caller. The shape is
tried only where a Value is being built, and reaches record as Pattern.
The same defect as the f-string, in all three spellings of a join — os.path.join,
Path(a) / b, a.joinpath(b). One unknown part returned Unknown and discarded
every known one, so os.path.join(BACKUP, name) said nothing at all when it says
BACKUP/*. joined_shape renders each part, an unknown one as *, and hands the
result to the same path_shaped the f-string rule uses.
one of a known set 3,499 → 3,600 (+101, and 1,636 → 1,655 located)
named none 2,755 → 2,654 (−101)
named a file 22,154 → 22,154
file uses 22,317 → 22,317
⚠ Only 19 of the 101 gained a directory; 82 gained a filename. The corpus's
commonest join is os.path.join(root, r) — both parts a variable — which is
refused, so what fires is join(dir, 'meta.json'): */meta.json, a certain name
under an uncertain directory. The opposite balance to the f-strings, and worth
knowing before anyone sizes a locus-only rule from this number.
⚠ Adjacent unknowns collapse to one run. join(a, b, 'x.ts') is */x.ts,
never */*/x.ts, which would claim a depth nobody wrote down.
⚠ A set becomes * rather than being multiplied out. Joining onto a choice
needs the choice made and this reader does not multiply sets, so the part is
widened — which claims less than the set did and more than the whole call being
Unknown.
Two of the five tests are guards rather than gains, and both still pass with the rule removed: a join of nothing known stays unresolved, and a join of entirely known parts is still an exact path, not a widened one.
The third language, added 2026-08-22, and it is python.rs with the nouns
changed: a grammar for the syntax, a module for the meaning, a report ranking
what it could not read. The types both readers answer in are shared
(program.rs) so a figure from one can be read against a figure from the other.
It is here because the ranking that kept it out was measured on the wrong
denominator. The "not done" row said node -e is a query tool, not an editor,
on 724 calls with 23 writes. Counted again over the whole corpus: 11,748 Bash
calls name a JavaScript runtime, 3,824 carry a program in a flag, and inside
them are 1,790 readFileSync, 1,909 require, 670 import and 214
writeFileSync. A projection is mostly about reads, and reads had not been
counted at all. Standing now: 2,306 programs read, 4,347 file operations, 86.0%
of them naming a file, 1,907 uses kept as paths.
Three things Python does not have had to be answered.
⚠ A regex literal and a division are the same character, and position is
what tells them apart: a / where an atom is expected cannot be division,
because division needs a left operand. The grammar gets this for free by trying
regex only in atom position. did_not_run does not, and its first version
read the ' inside .replace(/['"]/g, "") as an opening quote, declared the
string unterminated, and threw the whole program away — the direction that
destroys knowledge. It now tracks the last significant character and applies the
same rule. Caught by a test, not by the corpus.
⚠ An expression may cross a newline, which in Python it may not. This corpus
writes raw.filter(…)⏎ .map(…), and a grammar that ends the statement at the
line break turns .map into a new one — which is how map, filter and then
appeared on the worklist. Newlines are allowed before a . and inside brackets,
and nowhere else: at statement level p = 'a' and q = 'b' on two lines
would otherwise read as one value of two operands, and a value of two operands
is computed, so every constant in the program would quietly stop being one.
⚠ A bare module specifier is a package, not a file. require("@angular/ compiler") has a slash in it, so it passes any "looks like a path" test ever
written — and it names nothing on disk. Node's own rule is used: a specifier is
a path when it starts with ./, ../, / or ~. Before that rule, 1,126 of
the recorded uses were the string fs.
Did it run? The same question the Python reader answers, checked the same
way. javascript::did_not_run refuses text whose quotes, template or brackets
never close — a heredoc cut short — and the reader keeps nothing from it, so a
false positive destroys knowledge while a false negative merely fails to gain
any. --example javascript-raised hands the programs to this repository's
pinned node: the reader discards 2 and node refuses both, and of the 1,025
it keeps node accepts 1,016. Seven of the nine refusals are TypeScript run by
tsx, which node cannot check and which the grammar handles by design; one
holds an unexpanded shell variable and therefore ran perfectly well; one is
genuinely broken.
The fourth language, added 2026-08-23, and the first that names something
other than a file. python.rs and javascript.rs answer which paths did this
touch; this answers which tables did it read, and which did it change.
⚠ It contributes nothing to any file count, and that is a measurement. Over
5,727 corpus commands carrying a SQL client there is no INTO OUTFILE, no
LOAD DATA INFILE and no sqlite .read/.output — SQL in this corpus names a
file exactly never. Those forms are read anyway, a rule apiece, because the cost
of missing one is a write nobody sees. But a table is not a file, and folding
2,747 table reads into reads would be 2,747 files that do not exist.
Standing: 2,434 commands carried statements, 2,747 table reads, 264 changes,
151 distinct tables. The 28 that carried statements and yielded nothing all
hold $1 — passed in at runtime, so unknowable rather than unread.
Two entries in the table were wrong before this existed, and neither was visible as wrong:
sqlite3was anInterpreter, so it read its DATABASE as a script it ran: the right file, recorded as the wrong kind of use, and its statements never read.mariadb/mysql/psqlwereNoFiles, filed under "build tools that take targets". True of the operand —mariadb healthis a database name on a server, not a path — and false of everything they carry.
⚠ The direction of a table is decided by the VERB, never by the clause.
SELECT … FROM x reads x; DELETE FROM x empties it. A reader mapping FROM
to "read" would report every deletion in the corpus as a lookup. The same rule
settles the sqlite database file: sqlite3 x.db 'SELECT …' and
sqlite3 x.db 'DELETE …' are identical in argv shape, so the file's direction
comes from parsing the payload.
⚠ A table name and a function call are the same shape, which is why this is
a grammar and not a regular expression. FROM datetime(ts, 'unixepoch') names
no table; a regex for FROM (\w+) answers datetime. Measured against a real
one: the regex reported 291 distinct tables where the grammar finds 151 —
about half its answer was fabricated. qualified refuses a name followed by
(, which is a decision a scanner cannot make.
⚠ Four separate rules were written wrong the same way, and the corpus caught
what tests did not. In a NON-atomic pest rule, implicit whitespace is inserted
between every pair of expressions — so ^"from" ~ !ident_char skipped the space
and tested the table name, and no clause matched at all. Then qualified ~ !"(",
then an explicit WHITESPACE+ that had nothing left to match, then a
dot_command that ate across the newline and swallowed 50 scripts whole. Any
rule whose meaning depends on adjacency has to be atomic.
⚠ Nine tests failed and four passed, and the four were the tell. All four were negative assertions — a function is not a table, a stream is not a file — which a reader recognising nothing satisfies perfectly. Had the suite held only the guards against fabricated subjects, which is what the module is for, it would have been entirely green over a reader that did nothing.
A carried program that runs a command runs a shell's worth of work, and until
2026-08-22 all of it was invisible. subprocess.run was the largest single
thing either carried reader could not read — 443 calls, twice the next entry
on the Python worklist. Program::ran now carries what a program handed to the
system, and shell_files::carried follows it at the shell's own directory, so
bash -c 'python3 -c "os.system(…)"' reads all the way down and what comes back
may be another Python or JavaScript program in turn.
Which of the two a call hands over is decided the way What a payload is says —
by the call, never by the shape of the argument. One case only this side has:
subprocess.run("ls -la") without shell=True is neither script nor argv.
Python looks for a program of that whole name and fails, so reading it as a
script would credit the program with work it did not do.
⚠ One unknown word makes a whole argv unusable. ["ffmpeg", "-i", f] with
f computed would otherwise read as an ffmpeg call over a file named -i.
What it bought, measured across the corpus: file uses 408,348 → 410,063 reads and 71,359 → 71,497 writes, and 95 paths nothing had named before.
⚠ "Not understood" and "touches no file" are different claims, and the
worklist is only as good as the difference. On 2026-08-22 the unread list was
headed by task at 12,761 calls — three times the next entry — which is the work
queue's own CLI, a store behind a server with no flag that names a file. It and
seven others (ping, dig, nc, journalctl, dmesg, nixos-version,
mariadb-admin) were checked one at a time against how the corpus actually calls
them and moved to Verb::NoFiles: 24,490 executions off the unread list,
which is the sum of those commands' own counts — the check that the drop is what
was classified and not something else moving underneath.
The same pass taught four commands that do real file work and were contributing
nothing: ffmpeg (368 calls of the recall pipeline's audio), ffprobe, unzip
and zstd — +524 reads, +110 writes, 88 paths nothing had named. ffmpeg is
the one shape here where the output is positional and the inputs are not, and
the path guard is what makes "the last operand" safe: -f null - ends in a
dash, a probe ends in a number left over from an undeclared flag, and neither is
a path, so neither becomes a write. An archive's members are the mirror image —
unzip x.zip 'FS/data/**' -d out names something inside the zip, which passes
every path test ever written and is not a file on this machine.
⚠ Checked one at a time, and screen is why. It sits in the same part of
that list and 222 of its calls are -X hardcopy /tmp/…, a real file written, so
sweeping the neighbourhood would have deleted them silently — Op::Nothing is a
claim nothing downstream re-examines. It writes what hardcopy and -Logfile
name and nothing else. journalctl went the other way for the same reason:
--file exists, and this corpus never uses it.
dhall-to-json (444) and lean (377) came with it. The converter is the shape
worth naming: both of its ends are flag values, so unlike ffmpeg it has no
operand at all — and this repository's own gate is a caller, gate.dhall the
source and gate.json generated from it, a dependency that appeared in no
projection until now.
md5 (323 calls), openssl and wg closed the same pass: the first two read
what they are given, and openssl x509 -noout -enddate reads
from a pipe, where the guard is what stops x509 becoming a filename. wg is
an interface in all 371 of its calls — ⚠ wg setconf <file> would read one and
does not appear, which is where this would go wrong quietly. Understood stood
at 99.2%, and the unread list topped out at 1,215.
That pass stopped where the corpus stopped: what was left was a container runtime and four of this fleet's own binaries, and the ticket recorded that their file behaviour lives in their sources rather than in how they are called. That turned out to be false for three of the four, and the fourth is not a binary.
--example unread-shapes was written to settle it — the population comes from
Extract::unhandled, the same field the rank is built from, and only the TEXT is
printed. ⚠ A hand-rolled grep '^lares' was tried first and disagreed with the
reader by 2x in BOTH directions: it counted the name inside grep patterns and
inside pasted Rust, and missed calls inside bash -c payloads. A worklist read
off the wrong instrument sends the work to the wrong place.
| calls | what settled it | |
|---|---|---|
ss |
294 | 20 spellings, all flags over a socket table. Verb::NoFiles, beside wg. |
mysqladmin |
284 | 5 spellings, all ping. mariadb-admin under its old name. |
verified_cli |
336 | its source: every arg is a subcommand, data via stdin |
replay |
1,076 | its source, which CORRECTED the call shapes |
⚠ replay is the case for reading a source, and the case against reading only
calls. replay --words <dir> reads as a valued flag; it is not one.
scanner/server/src/bin/replay.rs shows --words, --slots, --tables,
--pdf, --paper and --bands are bare mode tests and the session directory is
the only positional — with --page N the single valued flag, whose 2 would
otherwise have resolved into a file that nothing touched. So a binary's own
source does count as evidence here, and the reason is not convenience: it is
the only evidence that disagreed with the guess.
What it bought: understood 99.2% → 99.3%, unread 20,073 → 18,083 — a drop
of exactly 1,990, which is the sum of the four commands' own counts. +721
reads and 28 paths nothing had named, all of them replay's session
directories; replay had 411 writes recorded against it from redirections and
zero reads, which is the shape of a tool the table half-knew.
Two were declined, with the measurement. k3s (1,215) carries
crictl exec "$C", whose target is a variable — an entry would move calls from
unread to read nothing, which is not progress. lares (378) was a Mac-side
Rust CLI deleted in its own repo (~/Archive/lares, commit 4a7968d, "delete
Mac-side rust/python/web"); the calls are real history, but the evidence is now
git archaeology on a tool that no longer exists.
⚠ probe (418) is not a binary at all — it is a shell function, and all four
of its distinct spellings are probe() {. See below: it was one of seventy-eight.
A="adb -s host"; $A logcat runs adb with three arguments. The reader made it
one command whose name was adb -s host, because [shell_ops::expand] returns
one word where bash word-splits, and Simple::argv has already dropped the
quotes that decide it. The tree knows — Parameter::quoted is a field on it
for exactly this — so project.rs now records the answer per word in
Simple::split, and shell_files splits on it.
⚠ "$A" logcat must NOT split, and bash is the reason: it looks for a
program whose whole name is adb -s host and fails. The old behaviour was
already right for the quoted case and wrong only for the unquoted one, which is
why this waited for the tree rather than being patched at the expansion.
simple commands 1,362,930 → 1,363,869 (+939)
understood 1,353,212 → 1,354,571 (+1,359, still 99.3%)
not in the table 8,150 → 7,730 (−420)
a local function 1,261 → 1,261
named by a variable 307 → 307
+1,359 − 420 = +939, and the two accounts that can never be worked did not move:
the gain is commands that became readable, not commands that changed drawer.
Nearly every verb rose with it — cat +6 reads, chmod +10 writes, perl +11,
one more database and one more table.
⚠ The flat grammar cannot fill split, so it is empty there. Read it with
get(i); a missing entry means "do not split", the answer that claims less. The
projection builds its own Key from named fields and never compares Simple by
equality, so the second reader is unaffected.
Splitting moved 161 reads off the local index, and the movement is now accounted for to the unit. Both binaries were run over the same corpus — the nightly rebuild changes the denominator, so an arm measured against yesterday's corpus would mix a code change with a data change — and the per-path counts diffed:
reads lost 230 across 9 paths, every one of them to zero
reads gained 59 across 38 paths that ALREADY existed
reads gained 10 across 7 paths that did not
net −161
No surviving path lost a single read. All 230 come from nine "paths" that are not files: multi-word blobs of 2 to 26 words, produced by resolving an unsplit expansion against the working directory.
213 of the 230 are one variable, in four spellings that differ only by the directory they were glued to:
GEB="ssh -o UserKnownHostsFile=/tmp/geb-known-hosts -o StrictHostKeyChecking=no root@…"
$GEB cat /etc/hostname expanded to a single word, and a word that is neither a
flag nor absolute is resolved against the cwd — so <cwd>/ssh -o … root@…
entered the corpus as a file this Mac read. That is the whole of the run a script fall and the reach another machine rise, and why a host that had never
appeared arrived with it: the reads were always remote, and were filed as local.
The +59 are the same correction seen from the other side. A blob of 26
memory filenames and one of 6 recall sources were each one unmatched path; split,
every member is a real file that was already known, each gaining its own reads.
Pinned by several_files_in_one_variable_are_counted_separately.
⚠ The +10 are a new overcount, and smaller than the one removed. Seven paths
made of awk/sed program text — /^#, 1\/{p=1;next}, a .pg"p} fragment —
carry 10 reads and 1 write. Splitting a word inside a program body produces
them. Left on the record: an unread call is an undercount and an invented path
is a lie, so this is the kind that matters, and 10 against 230 is the trade.
Fixed below.
⚠ The refusal recorded here before was wrong, and the reason is mechanical.
"No corpus variable holds an ssh command" rested on
grep -oE '[A-Z_]+="ssh [^"]{0,60}"' returning nothing. The corpus is JSONL,
so the shell's " is stored as \" — ="ssh cannot match, at any length
bound. GEB=\"ssh matches 59 rows. A grep for shell quoting over a JSON corpus
finds nothing and says so in the same voice as a true absence: the shape of the
measurement, not the shape of the corpus.
The other refusal stands: odin:/tmp/full-stage.log did not change host, it fell
below a ranked list's top-N cut.
The seven were not awk's fault, which is what filing them as "a word inside a program body" assumed. The word that made five of them is one shell word:
awk '/^# page 1\//{p=1;next} {print > "'$S'/'$v'.pg"}' $S/$v.words
Single-quoted literals with two expansions spliced in. splits() is right that
the word contains an unquoted expansion — but bash word-splits the characters
an expansion PRODUCED, never the literal text beside them. $S and $v hold
no whitespace, so bash runs one awk with one program; the reader cut the word
at the spaces inside the single quotes and filed /^# and 1\//{p=1;next} as
files.
The second cause is the same rule seen from the other end. In
du -ch $(sed 's/^/.\//' $S/list.txt | tr '\n' '\0' | xargs -0 echo)
the substitution's text is not its output, and nothing here can evaluate it —
so cutting it up produced list.txt), closing paren and all.
[shell_ops::expand_marking] now returns the byte ranges it substituted into,
and split_produced cuts only inside them. A substitution nobody evaluated
produces no span, so none of it splits.
distinct paths 40,998 → 40,990 (−8, every one invented)
file uses 238,840 → 238,827 (−13 reads, −1 write)
⚠ Three of those 13 are real paths, and losing them is the point. HEAD
found …/inner_only_files.txt and a /nix/store lean4 directory only by
shredding a substitution it could not evaluate and finding a $S-shaped fragment
inside — the same shredding, in the same word, that invented
/Volumes/Backup/cache/cache/'s/^/.\//'. One accident was producing three right
answers and eight wrong ones. Each of the three keeps its other reads, so no path
left the index; what left is a reason to believe them.
⚠ Splitting inside a substitution is not a capability this gave up — it
was never one. Reading $(sed … file) means parsing the inner command, which
the nested reader does elsewhere and does not do here.
$BIN --version reaches the worklist as a command called $BIN, and no entry
anybody writes could ever match it: it is a different program in every script
that sets it. The same argument as #1124's local functions, and the same answer
— a third outcome, in commands() and not in handled.
| before | after | |
|---|---|---|
| simple commands | 1,362,930 | 1,362,930 |
| understood | 99.3% | 99.3% |
| not in the table | 8,457 | 8,150 |
| named by a variable | — | 307 (47 names) |
307 out, 307 in, the total unmoved and the rate unmoved. That equality is the
check, and it failed the first time: the worklist fell by 307 while the new field
held 215, because Extract::merge copies each account out of a nested shell by
hand and a field added without a line there is a silent loss. The 92 missing
were every $BIN inside a bash -c.
⚠ Nothing surfaced, and that is worth saying. #1124 freed arp, claude
and magick from under 2,475 phantoms; here the top 25 is identical before and
after, because these 307 sat in the tail. The gain is that 307 calls across 47
names have stopped being described as commands the table has no entry for.
⚠ The other 503 were deliberately NOT filed here. A="adb -s HOST"; $A logcat
resolves, and comes back as a command with its arguments glued into one word,
because shell_ops::expand returns one word where bash word-splits an unquoted
expansion. That is a misreading of a knowable name, and filing it beside $BIN
would hide a defect behind an accounting fix. Telling $A from "$A" needs
the quoting Simple discards — "$A" logcat genuinely makes bash look for a
program of that whole name and fail — so the split belongs in syntax/ and the
503 stay visible until then.
⚠ continue there drops the timeline row. The activity is pushed at the end
of that loop, so short-circuiting after recording the count removed every
variable-named call from doing.rs while every total still balanced. Caught by
an existing test, not by the new ones.
⚠ local short-circuits too and is RIGHT to — measured rather than assumed.
A declared function has a BODY, and its activities are already recorded at the
declaration: save() { echo x > /tmp/o.txt; } then save yields one edit row,
and letting the call through as well would make it two. A command named by a
variable has no body, so the call is the whole of the work and dropping it loses
all of it. The two look identical at that line and are opposite, which is
why the fix went to one and not the other.
probe was not the case, it was the symptom. Measured the same day by
--example defined-here, then again by the reader itself: 2,475 calls across
77 names, which was the largest single category on the worklist — bigger than
k3s. Every one of them work nobody could do: r, p, A, maxid, render
mean something different in every script that declares them.
⚠ It cannot be a name list, and the numbers say so. check is a real program
in ~/Code/check AND a local helper — 112 of its 114 calls are declared in their
own text and 2 are not. run splits 185/10 the same way. The question has to be
asked of each command text, which is why Ran::defines is a property of the
SCRIPT and shell_ops::classify is not asked at all: from the call alone —
same word, same argv — it is not decidable.
A third outcome, not a reclassification into either existing one.
Extract::local is neither unhandled (there is no entry to write) nor
handled (the reader did not follow the call, and what it passes as arguments
goes unread). Both neighbours would have been a claim.
| before | after | |
|---|---|---|
| not in the table | 20,073 | 15,608 |
| a local function | — | 2,475 (77 names) |
| understood | 99.3% | 99.3% |
⚠ The rate is unchanged, and that is the check. local is in the
denominator of understood() and not in the numerator, so moving 2,475 calls out
of unhandled cannot raise it — nothing more was read. Verified by diffing the
report: understood 2428358 (99.3%) is byte-identical across the change, and
only the two lines below it moved. A coverage figure that improves because calls
left the denominator is a different fact from one that improves because the
reader learned something.
What it bought is a worklist that is true. With 2,475 phantom entries gone,
arp (167), claude (164) and magick (160) surface — real commands that were
buried under helpers nobody can teach.
⚠ That table is an ABLATION and its counts are over the pre-#1130 corpus,
which held one era twice. It is left as measured, because a before/after diff of
the same corpus is exactly where the duplication cancels — rewriting the numbers
would falsify the record of what was run. The standing figure today is 1,261
local calls across 78 names, and understood is still 99.3%: the change
survived the corpus being halved under it, which is what a ratio is for.
⚠ Still unread, and named rather than hidden: what the CALL passes.
render "$src" out.svg binds $1 and $2 in the body, and the body's file uses
are recorded at the declaration under Reached::Sometimes — with the arguments
unbound. Reading them needs argument binding, which is why this pass stops at
making the number true.
⚠ md5 was the macOS spelling of something already in the table, and adding
"the family" duplicated five entries the Verb::Read list has always had —
md5sum, shasum, sha1sum, sha256sum, cksum. Read the list before
extending it; rustc will say so, but only if something asks it.
The gate said so under -D warnings and the pre-commit check in that session did
not. ⚠ Run clippy the way the gate does — -- -D warnings — and read its exit
status. Grepping its output for the word warning cannot tell "clean" from
"did not run", and without -D warnings it exits 0 either way, so no exit-code
test bolted onto that form would help.
The tidy explanation for the miss — that cargo had cached the diagnostic — was tested and is false: the same pipeline reports it on a fresh crate and a stale one alike. The miss itself stays unreproduced, which is worth more than a plausible story about it.
What is left there is a different kind of thing and is not guessable from the
corpus: k3s (1,215) carries a command, and replay, probe, lares and
verified_cli (2,208 between them) are this fleet's own binaries, whose file
behaviour is in their sources rather than in how they are called.
⚠ A wrong reading costs more than a missing one, and perl -pi -e was both.
It was read as an interpreter running a script, and it is a rewriter changing a
file. Both halves were wrong at once: the direction (a read recorded against a
file being written) and the kind (a script run, where the operand is a source
file being edited). It is the corpus's fourth
commonest command shape — -0pi -e 2,114 calls, -pi -e 1,028, -i -pe 38 —
and correcting it moved 4,044 uses from read to write, "run a script" 53,621
→ 49,663 and "transform (in place)" 1,729 → 5,473.
Two things it turned on, both of which read as details until they were not:
-iis written in a cluster.-0piis-0,-pand-i, and the test wasstarts_with("-i"), which matches none of the 3,142 calls that spell it that way. The cluster testVerb::Removealready used for-ris the one that was needed.- A program in a flag is still a program. With the text in
-e, theOp::Transformwas built with an emptyprogram, so the opacity census — which decides what to read next — held no perl at all. "transform program, rewriting" went 137 kB → 751 kB when that was fixed, without a single new call being read.
⚠ ruby, deno and bun were checked at the same time and left alone, and
that is a measurement rather than a shrug: ruby -e appears zero times in
this corpus, deno eval/deno run zero, bun -e/bun run zero. perl script.pl
is zero too — the 288 apparent cases are all nix-shell -p perl …, where the
word is a package name.
reader/src/reading.rs holds the accumulation; --bin shell-files prints it,
/api/reading serves it to both apps, and the two views draw it.
⚠ The extraction exists because a SECOND consumer is where a duplicated
calculation stops being free. The survey was thirty mutable locals inside the
report's main, which is fine while nothing else asks — and the moment the API
asks, the alternative is two answers to how much is understood that drift
apart with nothing to say so. The refactor was checked by diffing the report's
output before and against after: byte-identical across all 209 lines.
| view | where | shows |
|---|---|---|
| the report | --bin shell-files |
everything, ranked, for somebody reading a terminal |
/reader |
the viewer | coverage, the verb histogram, files, databases, the fleet, the work queue |
/reader |
the console, behind the burger menu | the headline pair and every shape, on its own screen |
⚠ Coverage and its ceiling are drawn together, on both. 99.2% of commands
understood beside 4.5% of file uses whose subject the text cannot determine —
different denominators, commands against uses, which is exactly why neither can
stand in for the other. A page showing the first alone is advertising. For the
same reason not understood is a bar INSIDE the histogram rather than a note
under it: it is the size of the hole in that very chart.
⚠ A 404 says "not mined here" rather than drawing zeroes. "Nothing has been mined" and "the survey found nothing" are different claims, and a default-filled summary states the second. The console's screen says it in one muted line, which is the correction to a first version that drew nothing at all — a component that vanishes is indistinguishable from one that was deleted.
⚠ It is a screen you go to, not a strip in the way. The survey sat above the session list, where it was the first thing read and the least often needed; it moved behind the burger menu on 2026-08-23. Given its own screen it stopped having to fit, so it shows every shape rather than the five that fitted.
What the text fixes about a value it cannot name. A located language — a
language L paired with a locus D, the directory the answer must live in —
which is one object with two halves, and the shapes below differ only in which
half is precise.
⟦*.log⟧ = some S ⊆ L(*.log) ∩ Files(dir, t)
An unknown finite subset of a known language: cardinality unknown, possibly
empty under nullglob, possibly the pattern itself where nothing matched.
bounded_by() resolves the pattern against the cwd, so what is recorded is
/abs/dir/*.log and not *.log — *, ../* and /* are three different
spaces, not one shrug.
Measured by --example opaque-shapes over the union corpus, 2026-08-24 — the
first reading after memview#1130 collapsed the duplicated era — of the 4,566
subjects the reader could not name:
| uses | shape | L |
D |
|---|---|---|---|
| 2,513 | a bare name, bound outside the text | — | — |
| 612 | locus known, leaf unknown — Verified/Geo/${s%%:*} |
— | ✓ |
| 338 | derived — ${f%.ts}.js, $(basename …) |
✓* | * |
| 338 | an environment directory — $TMPDIR |
— | ~ |
| 314 | unclassified | — | — |
| 273 | a located finite set — $(find $d -name '*.ts') |
✓ | ✓ |
| 143 | a substitution with no locus | — | — |
| 29 | a positional parameter | — | — |
| 6 | arithmetic — never a path |
Plus the 490 already bounded, which have both. Of the 4,560 that are genuinely path subjects: a locus is known for 19.4%, a language for 26.8%.
⚠ Every count in that table roughly halved on 2026-08-24 and the two RATES did not move — 19.0% → 19.4% and 26.7% → 26.8%, against counts that fell 48%. Nothing was learned between those readings: the corpus stopped holding one era twice, so there was less to count and no more known. It is the cleanest demonstration in this file of why a moved rate has to say which half moved, and the reason the shape of the table is the durable part of it and the counts are not.
⚠ Two of those rows moved for opposite reasons on 2026-08-23, and telling them apart is the whole discipline ([[feedback_a_threshold_carries_its_denominator]], memview#1079). The arithmetic row fell 458 → 12 and a row of 67 program bodies went to zero because the READER stopped offering non-paths as subjects — the denominator was wrong. The locus rate rose 17.7% → 19.0% because the CENSUS learned to read a locus it already had — the numerator was wrong. Only the second is more understood; the first is less overstated. Neither is progress on the reader's actual limit, which is the bare-name row — 4,886 when this was written, 2,513 over the corpus of 2026-08-24, and the same fraction of the whole either way.
The census had required the variable to be in the leaf — split at the last
/, and the directory literal throughout — so Code/$p/node_modules (56 uses,
the largest single shape in the unclassified bucket) counted as having no locus,
though Code is exactly where the answer must live. It now takes the literal
text ahead of the first $. Guarded on whitespace, because a one-line jq filter
carries a / and a $ too, and widening without that would move a program
fragment into locus known — the direction that flatters the census.
⚠ Regular is the right lattice because the class is closed under what shells
do to paths. dirname, basename, ${f%.ts}.js, ${f%%:*} each map a
regular language to a regular language, so a derived value keeps a describable
space instead of collapsing. That closure is what makes S ⊆ L compose, and
composition is what makes it checkable.
⚠ Past and future are the same object with a different t. For history L
and D are exact and only t is gone — so the honest output was always the
space. For a command about to run, t is now, so S = L ∩ Files(D, now) is one
directory read. That is the prediction half
execution-model.md names, and it must live above this
library — reader touches no filesystem, and that property is worth more than
the convenience.
Extract::located records the directory a subject is rooted at when the text
writes one out ahead of the first expansion. 611 uses over 89 distinct loci,
2026-08-24 — the census's locus known, leaf unknown row, moved out of the
shrug and into an answer.
⟦Verified/Geo/${s%%:*}⟧ = some path rooted at /abs/Verified/Geo
The same object as bounded with the language half missing. A glob gives
L and D; this gives D alone, because ${s%%:*} is a transduction the
reader will not build an automaton for. A glob bound is therefore tried FIRST
and wins — filing a bounded subject as merely located would throw away the half
that makes it falsifiable.
⚠ ROOTED AT, not contained in, and the difference is a .. nobody can see.
The word resolves from that directory, so a real run touches something under it
unless the expansion climbs out. An absolute-looking expansion does not escape
— a/b//c is a/b/c — only .. does. Stated that way it stays falsifiable in
the direction that matters, exactly as S ⊆ L is for globs.
| before | after | |
|---|---|---|
| shell, by word | 4,566 | 3,705 |
| shell, located | — | 861 (128 loci) |
| subjects not named | 11,974 | 11,974 |
Two shapes reach it. A written-out directory ahead of the first expansion,
611 uses; and a finite-set generator over a directory the text names —
$(find . -name '*.ts'), $(git ls-files) — 250 more.
⚠ The generator is tested before the whitespace guard, because a generator is
nothing but whitespace. That guard exists to keep one-line jq filters out, and
it would have thrown every find away with them.
⚠ git ls-files and git diff look alike and are not one rule. ls-files
with no pathspec lists what is tracked at or below the working directory,
printed relative to it, so the cwd is its locus. git diff --name-only and
git status print relative to the REPOSITORY ROOT wherever they run, and this
reader does not know where that is. One rule for both would root a use at a
directory it never walked. Measured: 251 located sets have a nameable
directory, and this refuses one of them on purpose.
⚠ The not-named total is byte-identical, and that is the check. Nothing more
was named; the reader says more about the same subjects. A locus is a better
answer than a shrug and is not an answer, so it stays in the denominator for the
same reason bounded and local do.
⚠ The census's own row had to be repaired in the same commit.
--example opaque-shapes takes its population from by_word, so those 611 left
it — its locus known row now reads 1, and its locus RATE would have fallen on
the day the gap was closed had by_locus not been added back to both the
numerators and the denominator. Verified by diffing the whole census: every
summary line is byte-identical, 885 (19.4%) and 1,223 (26.8%) included.
What is left in the census: one locus known — amun:~/Photos/$f, a remote
path whose directory is on another machine — and 23 located finite set, being
22 that walk a computed directory ($(find $d …)) and the one git diff. All
three shapes have a test, because they are what somebody reading those rows will
try to "fix", and each fix would name a directory the command never walked.
derived is 338 uses, and the obvious reading is that it is the third of this
domain still owed. It is not, and the measurement is the whole finding: only
30 of the 338 rest on a name a glob actually bound. The rest derive from a bare
name, and a transform of something with no space has no space either — there is
nothing there to win.
Of those 30, 27 are one idiom:
for d in */; do cat "${d%/}/gate.json"; doneThe pattern is */ and the expansion strips the very / it ends with, so the
truncation is syntactic: deleting a known tail from a known string, not
reasoning about a language. ${d%/}/gate.json over */ is */gate.json — a
whole pattern, both halves, so these become bounded rather than merely located.
shell, by word 3,705 → 3,676
shell, bounded 490 → 519
⚠ The guard the rule rests on: a suffix holding a glob metacharacter is
refused. ${f%*} strips the SHORTEST match of *, which is the empty string —
so a pattern ending in * would be "truncated" to something the shell never
produces. Only a literal suffix removes exactly the text it names. ${f%%:*}
stays opaque for that reason and has kept its test since before this rule.
⚠ %% gets no separate rule and no wider claim. Longest and shortest match
coincide when there is no wildcard to be greedy with.
What is deliberately left: $(basename $f) over archive/*.md, one use. A
leaf-only transform needs its own rule about where the result is then resolved,
and one use does not pay for it.
reader/tests/suite/oracle.rs is the only test that catches a wrong reading rather
than a missing one. Shims go first on PATH, each logging its argv and cwd
before exec'ing the real tool, so the log is what bash did — globs expanded,
variables substituted, loops iterated.
Two properties:
- sound — never predicts a command the shell did not run;
- exact where determined — where the text determines everything, the prediction equals the log. Stops a reader passing the first by predicting nothing.
⚠ It runs only the fixtures in that file, in a scratch directory it removes. No corpus command is ever re-executed.
A live call is checked against its own outcome. When the console predicts a call before it runs, it compares the prediction with the files afterwards; a divergence is a defect, kept with its command and both texts (evaluator.md). It observes a call that ran anyway, so this is still no re-execution.
Asked and settled twice — the user 2026-08-13, and again 2026-08-23. Do not "fix" the reader into soundness.
Sound AI over-approximates so a property holds for all inputs: γ(abstract) ⊇
concrete, joins at merge points, widening at loops, and every undetermined value
becomes ⊤. Applied here, rm "$f" with $f undetermined has exactly one sound
answer: may have deleted any file.
⚠ The accurate statement is not "the reader is unsound". It is sound where it
speaks — S ⊆ L is an over-approximation — and it refuses to say ⊤ where
soundness would require it. The artefact is therefore three parts, not one
bound: a lower bound (what was named), a described middle (S ⊆ L,
sound and falsifiable), and a counted remainder (23,309, printed rather than
hidden). That is strictly more informative than either bound alone.
Four reasons, in increasing order of force:
- The question is about one trace, not all traces. Over-approximation exists for "for every possible input". Exactly one execution happened and its inputs are mostly in the text, so γ is a singleton nearly everywhere; widening discards information that is present.
- ⊤ absorbs, and aggregation is the product. Every artefact joins over commands — what a session touched, what an agent changed in July. Join anything with ⊤ and the result is ⊤. With 20,073 commands not in the table and 102 unparsed calls, one anywhere in a turn makes that turn's answer may have touched any file, and the timeline becomes rows of identical nothing.
- The error would run in the expensive direction. An undercount fails to gain; a fabrication corrupts every count downstream. ⊤ is the maximal wrong reading — it asserts files that were never touched.
- It would be unfalsifiable, which is disqualifying here.
S ⊆ Lis checkable, andoracle.rsabove checks it: run the fixture for real, every path touched must match the pattern. A sound over-approximation contains every observed run by construction, so no oracle can ever disagree with it. A claim no observation can refute is not evidence — the same principle as a gate that cannot fail not being a gate.
Each decided from a measurement kept with the thing it decided.
| not done | why | numbers |
|---|---|---|
| a third-party parser | swapping loses more than it gains | reader/examples/tree-sitter-probe.rs |
| parsing regexes | biggest by volume; a regex names no file | reader/src/bin/opacity.rs |
| opening scripts on disk | what deploy.sh held then is unrecoverable |
— |
deno and bun inline code |
a handful of calls, and neither spells its flags as node does | reader/src/shell_ops.rs |
| a policy-refusal channel | the refused bucket had one member | memview#820 |
confirming || from a non-zero exit |
reachable by arithmetic, and tiny | reader/src/shell.rs |
2>/dev/null is a write redirect, and activity.rs read it as a file change —
so ls -la x 2>/dev/null was filed on the timeline as an edit. 82% of every
recorded edit was this: edit 229,492 → 41,113 over that day's corpus, with
all 188,379 reappearing under another kind or on the worklist, balanced to the
unit.
⚠ The two dimensions had disagreed all along and neither could see it.
[shell_ops::resolve] refuses /dev/* — it calls /dev/null the busiest path
in the corpus and keeps it out of the file index — so no file was ever recorded
while the work was still called editing. A defect visible only from outside
both.
⚠ What found it was the description corpus, on its first run
(concept-model.md). The edit kind's commonest stated
intents were read, check, find and list; sampling them showed ls. The
authors were right and the reader was wrong — which is what an independent
second reading is for.
⚠ An unresolvable target is still a write, and that is deliberately not the
same rule. > "$LOG" wrote something whose name cannot be known, the same
call Op::Remove makes for rm -rf "$BUILD". Only the device refusal is
shared, because only that one says this was never a file.
The timeline takes tool calls as well as the shell since 2026-08-17 — Read,
Write, Edit, Grep, Agent and the two web tools each produce a row, and
agents::TOOLS is the list, taken from the corpus rather than from the tool
list anybody remembers: Task, MultiEdit and NotebookEdit appear in it
zero times. It records everything that happened rather than the notable part of
it, and doing.rs says why.
Rows group into episodes — the stretch of work one instruction produced,
bracketed by user turns. The boundary is observed, never inferred from a gap or
a change of subject: inference can merge two instructions into one episode, and a
merge is unrecoverable downstream, where a duplicate bracket is only noise.
doing.rs carries the rule.