Every open issue: an action that lost something now says so - #43
Merged
Merged
Conversation
Three defects, one shape: the call did less than it was asked to do and the field a caller reads said everything was fine. fs.list sliced at max and returned a bare array (#37). Three of twenty-one entries came back with no truncated, no complete, no scope — a caller could not tell "this directory has three files" from "I showed you three of twenty-one". It now takes max+1 as the overflow witness, the same rule rg-stream already owns for search, and carries the envelope. An unreadable directory during a recursive walk used to vanish into a bare catch; it is reported as a partial reason, because it may hide a subtree. searchMany returned ok:true when a requested since filter was never applied (#38). #32 disclosed the dateFilter object and the test asserted it, but ok was still items.every(i => i.ok), so an unfiltered result set arrived as a successful filtered search. ok keeps its meaning — no query threw — and a new complete answers the different question: was anything requested not delivered. The transport was the trap. src/sandbox.js decided whether metadata could cross the guest wire from the ACTION NAME: search.* or exactly fs.grepFile. Decorating fs.list without touching that list would have passed every host test and arrived bare in the guest, because the name list cannot know about an action added after it was written. The decorator now brands the value and the wire reads the brand. Ordinary guest data shaped like {rows:[...]} is still never mistaken for an envelope: only the decorator brands, and the wire side still validates. Signatures updated so actions.describe stops understating both returns. 983 tests, 982 pass, 0 fail. Verified through runCode as well as the host object: fs.list(src, {max:3}) reports 3 of 21 with complete:false on both paths.
…e down Six closed issues were one sentence repeated: the call did less than it was asked to do, and the field the caller reads said everything was fine. Each was fixed with a symptom test. None of them stated the rule, so the seventh and eighth instances shipped under a green suite. This writes the rule down, plus the two things that are easy to get wrong when adding a disclosure: read max+1 so a cap can be told apart from an answer that is exactly max long, and brand the value rather than matching the action's name, because a name list cannot know about an action added after it was written. It also records what this tool cannot prove. complete:true on an empty result means the walk lost nothing; it does not mean the string is absent, since a walk can be pruned before it ever sees a match. Two axes cannot be proven at all: no --encoding is passed, so legacy-encoded text is never matched, and ripgrep skips binary files with a silent zero while search.files still lists them. A tool that says it cannot prove something is more useful than one that guesses. fs.read and read_file are named exceptions: they return the primitive string the catalog promises and the suite asserts, so their truncation marker is display text and not the contract. structure:check exit 0. INDEX.md regenerated, never hand-edited. 983 tests, 982 pass, 0 fail.
… named for search src/search-result.js had five production consumers spanning search, the filesystem, sandbox serialization and worker restoration. fs.grepFile has imported it for a while and fs.list now does too, so a file named for search owned filesystem completeness — the naming lie that invites the next drift. Its brand was already generic (codemode.resultEnvelope); the filename was the last thing still claiming otherwise. Ten executable import expressions across nine files, plus one source comment, the structure manifest area and the contract doc body. structure/INDEX.md was regenerated, not edited. Exported symbol names are deliberately unchanged. Renaming them in the same commit would add API churn without improving module ownership. Two references are PRESERVED on purpose, and a global replace would have been wrong: evidence/bench-260918/mcp-completeness.md cites the source path from a measured run, and evidence/review-hardening-20260913.json keys a hash to a recorded testedCommit. Rewriting either to a path that did not exist at that commit would falsify provenance. So the completion gate is not a repository-wide zero but zero LIVE references plus exactly one preserved occurrence in each. Known cost: package.json packages the whole src/ tree with no exports map, so any unsupported external deep import of the old path moves. Nothing in the repository documents that path as API. structure:check exit 0. 983 tests, 982 pass, 0 fail.
A default search returned eight of fifteen matching files and reported
complete:true. The flag was not lying — it means the walk that ran lost nothing,
and it was right about that. The question nobody could ask was whether the walk
had been allowed to see everything.
So complete is unchanged, and twenty-plus existing assertions that call a clean
fixture search complete stay correct. What is new is scope.coverage: one entry
per pruning mechanism, each off, on or unknown. An absence claim needs
complete:true AND every entry off.
Two boolean designs were tried first and both were wrong in the dangerous
direction. Counting ignore files between the target and the root misses a
.gitignore in a descendant directory and global excludes entirely. Reading the
policy flags misses ripgrep's silent binary suppression and a symlink census
that stops at depth three. An enumeration of the pipeline then turned up
mechanisms nothing can observe at all, so any "nothing could hide" boolean would
have carried unknowable terms. A record can say "unknown"; a boolean cannot.
Three silent losses are fixed in passing, each one its own small bug:
- a binary option passes --text. ripgrep skips binary content SILENTLY, so
search.content returned zero rows for a string that really is inside
bin/rg.exe while search.files still listed the file. That zero meant nothing
and looked like everything. Opt-in, because always passing --text would
spend the result budget on bytes nobody asked for.
- both swallowed JSON.parse sites now count what they could not read, and a
non-zero count reaches partial, which lowers complete through the predicate
that already existed. A record the parser drops may have been a match.
- the symlink census sets depthCapped. capped only ever covered the entry
bound, so a symlink below depth three left dirs:0, files:0 and nothing
saying the scan had not looked. This does not lower complete, on purpose;
it makes coverage.symlinks say unknown instead of off.
encoding stays permanently unknown: no --encoding is passed and the line reader
decodes UTF-8 only. An all-off record is therefore unreachable today, which is
the honest answer rather than a defect.
The discovery surface was the expensive part. The resident tool description has
a 2048-byte budget with 29 bytes of headroom, so the coverage rule REPLACED a
vaguer sentence about retrying with noIgnore rather than being added to it —
same size, checkable claim. The mechanism list and the binary trap live in the
skill reference, which is the split the 56-line block budget enforces.
Caught by the repository's own guards along the way: catalog-drift flagged the
binary option as undeclared in three signatures, and the READMEs quote the
description's exact byte count in four places.
993 tests, 992 pass, 0 fail. structure:check exit 0. Measured on the issue's own
fixture: defaults still find 8 of 15, and now say ignoreRules:on,
hiddenFiles:on, excludeGlobs:on.
Six closed issues were the same sentence — the implementation did less than the caller asked, and the field the caller reads said everything was fine. Each got a symptom test. None stated the rule, so a suite of 978 passing tests shipped the seventh and eighth instances. This states it: if the implementation skipped, capped, filtered or could not apply something requested, the documented completeness field must not report full success. Placed on a scratch worktree at 86f6c94 the same file reports 5 pass / 6 fail — fs.list (#37), searchMany (#38), search.count's binary suppression, the unclassified binary option, the missing contract document, and the positive boundary that cannot pass while fs.list has no complete field. The anti-false-green mechanism is that the classification table is written by hand HERE and the catalog is checked against it, in both directions. Deriving both sides from the registry is the self-consistent green this repository has already been bitten by: search-schema.js records includeExcluded being executable while absent from the catalog. An audit of this guard found a false green inside the guard, which is the whole lesson repeated one level up. readAxis compared coverage[key] === 'off' and expected false, so DELETING the key gave undefined === 'off' → false and the probe passed while the disclosure was gone. A guard that survives the removal of the thing it guards is not a guard. It now asserts presence and value domain first, proven by mutation: commenting out the binaryContent line takes the file from 18 pass / 0 fail to 16 pass / 2 fail. The same audit showed UNREACHABLE was a false claim — all ten actions are forceable through injection seams this repository already has. The set is renamed DEFERRED_TO_WP9, each seam is cited, and one of them is a KNOWN present-day false green rather than merely untested: a fake browse.exec child can return ok:true with snapshot.truncated:true, because nested truncation is ignored and top-level complete is assigned from run status alone. Also from the audit: classification values are validated and canonical cap names are forced to may-omit, with the heuristic's real gap written down — it misses any lossy option whose name is outside the list, including binary and noIgnore today. And every probe now has a no-loss counterpart with key parity asserted, because the first version covered only two actions, so four could have reported incomplete unconditionally and passed. fs.read and read_file stay named exceptions: a primitive string cannot carry a field, and a third test cross-checks that the exception list is what structure/result-completeness.md actually names.
#39, #35) #33 closed the INPUT direction of catalog drift and this suite has guarded it since. The OUTPUT direction was open by construction: the drift test cuts the return type off before comparing arguments, which was the right fix for #33 and left the other half unchecked. browse.attach was the sharpest case. It declared ten keys and returned nineteen, and the one it omitted was `text` — the page body itself. An agent that reads the signature and not the source has no way to find the content. Eleven signatures are corrected here; the issue named five, and 001's inventory found fifteen or more. The new output-direction check has two halves, because most browse actions cannot be called without a browser. For the callable ones it compares runtime keys against the declared return, with an allow-list where each entry must carry a reason — an unexplained allow-list is how drift returns wearing a permission slip. For browse.attach and browse.exec it asserts the DECLARATION, which is what a reader depends on. Proven by mutation: removing `text` from the signature fails the test. Two parser subtleties the first version got wrong. A decorated array's own keys are numeric row indices, so the comparison uses its wire envelope. And an array action legitimately declares TWO shapes — the row and the serialized envelope, as fs.list does — so reading only the first brace group reported the envelope's own keys as undeclared. #35, narrowed: the resident description now names the 64 KiB cap and says it is on the RETURN path, pointing at fs.write + fs.grepFile. Its budget is 2048 bytes and wp2 had left 3 to spare, so this was paid for rather than added: the symlink line dropped a field list that my own depthCapped change had already made stale, and two sentences were compressed. 2,034 bytes now, and the four README quotes in two languages follow. The issue's spillPath and truncatedSide requests are not implemented, and its own follow-up comment is why: this repository does not write the spill file. That belongs to Aside. 1015 tests, 1014 pass, 0 fail. structure:check exit 0.
#35's surviving complaint, after its own author retracted two thirds of it, was a DISCOVERY gap rather than an envelope gap. The reporter fed a 1.3 MB page into code mode, got a clipped result, and concluded code mode could not handle large pages. The body had arrived whole the whole time — only the RETURN was capped, and nothing on the surface said which side had been clipped. The skill reference already carried the sentence that would have prevented the detour, and no test asserted it, so it could have been edited away without anyone noticing. Now three things are pinned: the reference says the cut is on the envelope and to check `chars`, the same page names fs.write + fs.grepFile as the way around it, and the resident description names the 64 KiB cap on the return path. A warning without a reachable escape hatch only teaches the reader to give up. browse.leakedTabs also stops claiming `checked` unconditionally: it is absent on the failure shape, which returns code and error instead. 1017 tests, 1016 pass, 0 fail.
…d for (#36) browse.readText on a patch-diff.githubusercontent.com .diff URL returned ok:true, HTTP 200, format markdown and 7,308 chars of GitHub's signed-out interstitial. Nothing in the envelope said so, and the body is SMALL, so a size check passes — "only 7 KB, must be a small PR" — and the agent proceeds on fabricated content. Two of the issue's three requests are deliberately not implemented, and the test file says why. format comes from Content-Type, not the extension: the commit .patch path returns text because GitHub sends text/plain there, and making format extension-driven would reverse #29's fix. Detecting the interstitial by body text is refused because policy.js already states that a detector which flags our own output is worse than none — "Sign in" appears in documents. What is implemented is the checkable part: a URL whose extension promises a diff, answering with a body that carries no diff marker, is a mismatch the tool can state without claiming to know why. The marker cost three audit rounds, and every branch of it is a finding rather than a guess. The first version accepted git's header and a 40-hex format-patch preamble and REJECTED a plain unified diff — which is what diff -u, SVN and Mercurial all emit, so every one of them would have been called fabricated. The second used \n only, so a CRLF unified diff failed at the \r: every patch made on Windows. And {40,64} accepted an invented 48-hex preamble as a real one. Calling a real patch fake is the failure that gets a warning switched off, and a warning people have switched off is worse than the silence it replaced. complete is now on EVERY return — success, mismatch, HTTP refusal, login wall, no browser, failed fallback — because a key that appears only on the bad branch is a key callers forget to read. The consumers matter more than the field. An incomplete body could still become the cached answer for that URL, a browse.watch baseline (so the interstitial reads as "changed" and the real diff as "changed back"), and a warmed prefetch. All three are gated, on complete === false rather than falsiness so injected observations without the key keep working, and the cache namespace moves v2 to v3 so a genuine pre-upgrade interstitial cannot be reused. Thirteen new cases, including all five real-diff shapes as explicit false-positive guards and a seeded v2 entry proving it cannot suppress a fresh fetch. Named residual: a binary-only diff body carries no marker and is reported incomplete; matching prose to fix that would bring back the false-positive class. 1030 tests, 1029 pass, 0 fail. structure:check exit 0.
…read only bodies AGENTS.md opens by saying this repository does not publish the machines that built it. Four files at the root were named for four of them: four <hostname>side-fleet-probe.mjs files, one per machine. All four were byte-identical (md5 b4409a9911a416eae514a603390f117b), nothing referenced them, and their CONTENTS were clean because they resolve paths through os.homedir(). Only the names leaked. The guard could not see them. Both existing scans take rel from git ls-files and immediately pass it to readIfText(rel), so a path is never compared against anything. The suite passed 978 tests with four host names in the index. My first fix for that was theatre, and an audit caught it. Extending the host-derived scan to paths catches ZERO of these four: knownMachineNames() drops two of the four before any comparison, because neither carries a hyphen or a digit and both are under seven characters, while the other two survive that filter and then fail the word-boundary test — the character after each is 's', from "side". Loosening the boundary is not the answer either: it would make a short name match an ordinary word and the scan would be abandoned in a week. AGENTS.md already says that scan is "a filter, not a proof". So the assertion names the shape instead: the only tracked path ending fleet-probe.mjs must be scripts/aside-fleet-probe.mjs. That is red with four entries and green with one, independent of hostnames, ssh config and whichever machine runs the suite. The host-derived path scan is added too, but for the NEXT filename, and its blind spots are written in the test comment so nobody reads it as enforcement. The probe itself is unchanged — same md5 — and it stays out of the npm payload: package.json lists individual script files rather than all of scripts/, and npm pack still reports 82 files. Broadening that allow-list would ship it. .gitignore also re-ignores evidence/bench-260918/*.json and run-agent-baseline.py. The directory negation added in 86f6c94 re-exposed everything inside that folder, because evidence/* only matches one level, so files deliberately excluded for carrying absolute home paths were merely untracked and one git add -A away from staged. *.raw.json is omitted as redundant; *.json covers it. NOT done, and it needs a human: these four names are already on origin/dev. AGENTS.md says a leak is fixed by rewriting the unpushed commits, and these are pushed. This removes them going forward; the history question is the user's. 1032 tests, 1031 pass, 0 fail.
… documentation (#42) The baseline decision that landed in 86f6c94 is right and stays: POSIX grep and find are what an agent actually reaches for, and ripgrep is the ceiling, not the baseline. Every measured number, the protocol, and the 128-second 3.23 GB grep -r row are untouched. What it offered as evidence cannot be checked. Searching the installed app bundle for Grep, Glob, ripgrep, search_files, grep_search or codebase_search finds nothing — because that directory contains no JavaScript at all. It holds Assets.car, app.icns, the manifest and scripting.sdef. Aside's agent is native, in Aside Framework.framework with its resources in aside_resources.pak, and a strings scan of both returns no tool names and no ripgrep reference either. So the absence of a tool is UNPROVEN, not proven, and AGENTS.md says this repository does not write down what was not measured. The honest claim is about DOCUMENTATION, which was observed, and it is all the baseline argument needs: there is no documented way for an agent to reach the binary. What is measured and stays: ripgrep 15.2.0 with pcre2 at 6,476,288 bytes; runtime/bin/rg is a three-line shim; env.sh puts that directory first on PATH; the same directory holds pdftotext, pdftoppm, pdfinfo, python3 and node, so it is the skill runtime's utility bin rather than an agent tool surface; runtime/manifest.txt records native_tools_source=homebrew; and the documented exec tools are read_file, write_file, edit_file, bash and repl, none of them a content search. The evidence note now also records what the void method could not settle, so the next reader does not repeat it. Two guards, because banning the phrase alone can be satisfied by saying nothing and the baseline would lose its reason. Nine exact phrasings are forbidden in both languages across both READMEs, BENCHMARKS.md and the evidence note; and the observed claim is required to be present. Bare "does not expose" is deliberately NOT banned: README uses it correctly about the guest API not exposing require, process or fetch, and a substring ban would fail on honest prose — which is how a guard gets deleted. Proven by mutation: restoring "no tool that calls it" fails both tests. This was unreleased. origin/main is still at 1cb2416, so the claim had not reached npm; publishing 0.9.0 would have been its first release.
…t what was inside one itemStatus answers "did this job run" and item.ok is a correct answer to that even when the tree it brought back was cut at a cap. Nobody asked the second question, so browse.exec reported complete:true over a sliced snapshot, a post-action observation that failed, and a ref read that came back ESTALEREF. A reviewer built each of those as a fake child and got status:'completed', complete:true, partial:[] from all three. itemLoss() names every way a finished item can still be missing something, and the batch folds it at the one place the top-level verdict is assigned. complete now needs three things rather than one: the run finished every job, nothing inside a finished job was cut, and no loss marker was raised. partial has been carrying two kinds of thing under one name. PARTIAL_ADVISORY names the ones that are not losses - currently navigated-during-actions, which says WHICH document a successful observation describes. suspect-empty is not one of them: browse.searchMany already lowered completeness for that exact marker, and one marker meaning two things in two producers was the defect rather than the fix. The after-snapshot's cuts are derived host-side from refCount and the tree length, because the generated script is a command-line argument with a 30,000 character ceiling and a 1,000 character reserve, and a test measures that reserve. Both facts were already on the wire. Seven consumers were erasing the field as fast as it was set: captureMany derived complete from status alone, recipes.run projected the run without it, report.build built a fresh result out of pageBox, attach had no completeness field at all, and downloadMedia, watch and prefetch left an undelivered item visible only in items[].code. api.batch's cap is ours, not the provider's - limit defaults to 5 here. A search returning exactly five was indistinguishable from a search that had exactly five to give, so the adapter asks for one more and reports saturated. The extra row is evidence, not data. Plan and its three audit rounds: devlog/_plan/260918_issue-sweep/090_wp9_remaining_consumers.md
…ow proven DEFERRED_TO_WP9 is gone. It was an honest list - it named the seam each action would be forced through rather than calling them unreachable - but a list of ten is a list of ten untested actions, and one of them was a known false green rather than merely untested. Each former member now has a negative probe that forces ONE deterministic loss and a positive counterpart that is genuinely lossless. Without the second an implementation that reports incomplete unconditionally passes every negative probe, which is the trap the file already warned about for its first five. LOSSY_BEYOND_OPTIONS carries the three actions whose loss does not come from one of their own declared options: browse.approve and recipes.run project another action's envelope, and api.batch inherits a cap that lives inside a nested argument. The may-omit sweep cannot see any of them, so they are listed by hand - and every member must have both probes, asserted, so the set cannot go stale the way the one it replaces did. Checked by mutation, not by the green: reverting the six producers turns seven of these probes red - browse.exec, browse.approve, browse.attach, browse.downloadMedia, browse.prefetch, report.build and api.batch.
…m were mine
A reviewer read the push range and the tarball before anything was pushed.
browse.readText hardcoded complete:true on its browser-fallback branch, so a run that
answered {complete:false, truncated:true, lostTo:['text-truncated']} reached the caller as
{ok:true, complete:true}. That is wp9's own defect one consumer further out, and it shipped
under a green suite because the sweep only exercised the malformed-diff path. Its discovery
signature omitted complete and contentShape in BOTH places that define it, which is exactly
what #39 was filed about.
The identity guard republished what it guards. test/no-machine-identity.test.js named five of
this fleet's machines in its own comments to explain why the host-derived filter caught zero
of four, and then exempted itself from both scans. The explanation is worth keeping and can be
written about the shape of the names instead. The exemption stays, and now says which of the
two things it is: knownMachineNames() accepts any ssh alias of seven characters or more, and
this file's prose contains the words machine, identity and repository - on a developer whose
ssh config names a host after one of them, the guard would fail on its own explanation.
The README introduced every number that followed as one 2026-09-18 measurement. The table
under it was a different method on a different date, and the CDP section carried a page of
claims nobody measured. The provenance sentence is narrowed per block, and the unmeasured
claims are deleted rather than given a note that says reported - which is how an unmeasured
number becomes a cited one. The Korean README still made the claim #42 removed from the
English; the ban list was bilingual and missed the phrasing, so both are fixed.
BENCHMARKS.md carried four byte counts that this branch changed. Re-measured after the
discovery fix, not before it: 2,034 / 2,433 / 44,330 / 8,249.
package.json published structure:index and structure:check while files/ omitted both the
script and the structure/ directory it reads, so every installed copy had two commands that
could only fail. Verified by unpacking the tarball and running it.
The release workflow's header claimed the gate refuses a SHA whose ci run is not green.
skip-ci-check makes that false. The input stays - it is a deliberate escape hatch - and the
header now names it, with a test that fails if the two disagree.
… did Minor rather than patch, and the reason is not the feature list. In 0.8.1 browse.exec and browse.captureMany derived complete from the run status alone, so a batch that finished every job and handed back a tree cut at maxTreeChars answered complete:true. In 0.9.0 the same result answers complete:false, truncated:true, and names the cut in lostTo. A caller branching on complete will get a different answer for the same page. No field is removed and no signature loses a key, so it is not a major; but that change of meaning is exactly what a minor is for, and it belongs in the release notes rather than in a diff. The rest of the range: scope.coverage on the search surface, a binary option, contentShape on readText, a decorated fs.list, eleven corrected output signatures, and a completeness probe for every action that can lose something.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
This was referenced Sep 18, 2026
Closed
…a line ending .gitattributes gives .yml no eol rule, so `text=auto` hands the Windows runner a CRLF checkout of release.yml. Six of the seven assertions in this file search for content and did not care; the seventh anchored its input block to \n and was red on windows-latest while four other platforms were green. Reproduced before fixing: the same regex over a CRLF copy of the real workflow does not match, and matches after normalising. The file is read normalised, because every assertion here is about what the workflow says and none of them is about how its lines end.
`an unawaited import leaves a race` pins both sides of that race, and the comment above it
says why: an earlier version asserted the result always won, which held on two platforms and
lost on a third. The rewrite was right about the shape of the problem and wrong about the
envelope - the refusal branch asserts out.error.code, and this envelope puts the code at the
top level and leaves error as a string. The test two above it already reads out.code.
Only the winning side of a race executes, so that branch had never run. A CI scheduling
difference let the refusal land first on one runner and it failed reading a property off a
string, on the same commit that another runner had just passed.
Shape confirmed by forcing a refusal rather than by reading the code: a refused run answers
{ok: false, code: '<CODE>', error: '<message string>'}, so out.error.code is undefined for
every refusal, not just this one.
A branch that is never taken is not a branch that passes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #35, #36, #37, #38, #39, #40, #41, #42.
All eight open issues shared one defect: an action that skipped, capped, filtered out or could not apply something the caller asked for still reported full success. Six of them had already been fixed once each, with a symptom test and no written rule, which is why a seventh and an eighth shipped under a green suite.
What changed
The rule is written down.
structure/result-completeness.mdstates it, andtest/completeness-invariant.test.jschecks it from an independent hand-written classification rather than from the registry, so the catalog and the guard cannot agree with each other while both are wrong.Every action that can lose something now has a probe and a counterpart. The negative probe forces one deterministic loss; the positive one constructs a genuinely lossless situation, because without it an implementation that reports incomplete unconditionally passes every negative probe.
completeon a browse batch now means three things instead of one. The run finished every job, nothing inside a finished job was cut, and no loss marker was raised. Previously it came from the run status alone, so a batch that handed back a tree sliced atmaxTreeChars— or a post-action snapshot that failed, or a ref read that came backESTALEREF— answeredcomplete: true.truncatedandlostTosay which cut it took.partialcarried two kinds of thing under one name.PARTIAL_ADVISORYnames the ones that are not losses.suspect-emptyis not one of them:browse.searchManyalready lowered completeness for that exact marker, and one marker meaning two things in two producers was the defect rather than the fix.Seven consumers were erasing the field as fast as it was set —
captureMany,attach,downloadMedia,watch,prefetch,recipes.runandreport.build.api.batch's cap is ours, not the provider's, so the adapter asks for one more than the limit and reports saturation.Breaking-ish, and why this is 0.9.0
A caller branching on
completewill get a different answer for the same page. No field is removed and no signature loses a key, so it is not a major; but that change of meaning is what a minor is for.Audit trail
Every phase was reviewed before it was built. The reviews caught, among others: a deferral list that had gone stale and was hiding a passing action; an advisory/loss split that was exactly inverted; a ref-loss predicate that would have turned ordinary selector absence into a failure;
browse.readTexthardcodingcomplete: trueon its browser fallback, which is this PR's own defect one consumer further out; an identity guard that named five machines in its own comments to explain why it could not catch them; and a publishedpackage.jsonwhosestructure:*scripts could only fail becausefilesomitted what they read.1,060 tests, 1,059 pass, 1 skipped, 0 fail.
structure:checkexit 0. Verified by mutation as well as by the green: reverting the six producers turns seven of the new probes red.