The action-wire entry recommended deleting the handshake this branch keeps and finishes; refusing a pairing is not supporting it. The bindAlways count was wrong in both directions and neither shape is legacy. One of the five legacy-labelled comments does not belong on the list. pollUntilStable was a code defect rather than a comment defect, and the Bounds question is settled against what the three drivers emit.
55 KiB
code audit, august 2026
Eleven parallel audits of the tree at 9b4ff5f, one per subsystem, each required to cite
file:line and to verify claims against the repository rather than recall. Detailed
per-slice findings sit alongside this file's source notes; what follows is the reconciled
result, including the places where the audit contradicted the brief it was given.
What has been acted on
The branch action-wire-and-test-scaffolding finished the action-wire handshake, fixed the
rest of the vacuous-pass class, corrected the load-bearing false comments, and landed the
first shared test scaffolding. What it changed is marked through the sections below; three
findings were corrected in the process and say so where they sit. What it deliberately did
not touch, each still owed its own change: relocating cmd/internal-tools, collapsing the
four DOM fact producers, narrowing DeviceDriver, trimming the vendored protocol, the
remaining false comments outside the load-bearing set, and the 51 table clusters.
One correctness finding was added to the list during the work rather than found by the audit.
Chrome's Metrics filled CPUPercent with a zero, which is the same answer an idle app
gives, so every web step recorded a CPU reading nobody took. It is the same defect as the
three iOS reads and it is fixed the same way: driver.Metrics.CPUPercent and
trace.Metrics.CPUPercent are now optional, and absence is how a driver says it did not look.
Two findings were reported to this file rather than fixed, because both are decisions rather than defects.
The campaign record still has three declarations. Collapsing them was considered and
rejected as too large for that change: they are not one type with drift. The writer emits
value ints with omitempty; analyze/load.go deliberately declares Actions and
UnattributedActions as *int so a runs.jsonl written before those counts existed is
refused rather than read as an arm that acted zero times, and two of its tests assert that
refusal; confusion-matrix reads a different subset again, including RunDirectory. One
shared type has to break one of those. If it is done, the shape is a type in a package all
three import, carrying pointers for every field whose absence has to be distinguishable from
zero, with the writer always emitting them, so the duration_millis shim moves in with it as
a named accessor rather than being rediscovered per reader.
The two settle primitives now disagree, on purpose on one side.
sidecar/.../DriverBackend.kt no longer charges a read's own duration to the stability
streak, because its doc said it did not and the code did. internal/driver/ioscompanion/settle.go:58-64
charges it deliberately and says so, arguing that a traversal-based read would show mid-read
churn as a byte difference. That argument is not unreasonable and it is an argument for a
weaker guarantee, so it should be settled on the evidence rather than adopted by drift.
Verdict
The complaint that prompted this audit was that the repository is padded with unnecessary code and worthless tests, and that a peer project solving a similar problem does it in a third of the lines. The size gap is real and the causes are not what the complaint assumed.
Three conclusions, in order of how much they change what should be done next:
-
The test suite is not the problem. It is large, 1.42:1 over source in the runner and driver packages and 1.65:1 in the verifier core, but only about 5 to 10 percent of it is genuinely deletable. Most of it asserts on
trace.jsonlbytes on disk, carries explicit control arms, and names the bug class in the test name. Measured rather than assumed: dropping the ten most redundant-lookinginternal/hierarchytests moves statement coverage from 91.9 percent to 91.9 percent, and the three next-worst clusters lose seven statement blocks between them, five of which are one-line getters. The waste is structural, the fifth copy of a pattern that was never folded back into the first, and tests that were never deleted after a stronger test subsumed them. -
The bulk is in research tooling and a vendored protocol, not the product.
cmd/is 20,565 lines, of which the shipped CLI is 1,475. The other 19,090 sit incmd/internal-tools, which is 14.6 times the size of the product CLI and 30 percent of the whole engine. Eight of its eleven binaries were created in the HEAD commit, have a one-commit history, and five of them cite specifications that live inpaper/, a separate git repository. The Makefile builds none of them. Separately, the vendored iOS protocol generates 12,283 lines from 97 messages, of which 14 are ever called. -
Where the two projects do the same job, they cost the same. The browser target is 5,927 lines against the peer project's 5,777, and the peer's does more inside that budget. None of the size difference is this tool being worse at the work it exists to do.
The rules in CLAUDE.md were not uniformly ignored, and the pattern in how they failed is
the most useful finding in the audit. Rules an agent can check while typing a single line
were followed, completely: zero em dashes in the entire tracked source tree, zero TODO,
zero FIXME, zero HACK, zero emoji headings, no badges and no contributing boilerplate
in any of the five READMEs. Rules that require standing back from a finished artefact were
not followed at all: is this comment needed, is this commit too large, does this README hold
more than a description. That is a review-gate problem, not a discipline problem, and it is
fixable with checks rather than with resolve.
Numbers
Non-blank lines, identical exclusions applied to both repositories, counted against the tree rather than recalled.
| this repository | peer project | ratio | |
|---|---|---|---|
| source, non-test | 56,708 | 19,065 | 3.0x |
| test | 49,277 | 6,693 | 7.4x |
| total | 105,985 | 25,758 | 4.1x |
| comparable product code only | 68,060 | 23,597 | 2.9x |
| files | 506 | 226 | 2.2x |
| test to source ratio | 0.87, or 1.11 excluding generated | 0.35 | |
| doc comments in non-test Go | 3,512, 15.9% of code | ||
| inline comments in non-test Go | 1,164, 5.3% of code | ||
| test functions | 1,280 | 136 | 9.4x |
| driver interface width | 18 methods plus 6 capability interfaces | 6 methods, 3 associated types | 4.5x |
| non-test files touched by one action verb | 34 | 8 | 4.3x |
Static analysis is clean and the suite is green. go vet ./... reports nothing across all
38 packages. staticcheck ./... reports one finding in source. The full suite runs in 24.9
seconds wall, 1,530 tests passed, 0 failed, 6 skipped, all six env-guarded rather than
broken. Average cyclomatic complexity is 4.26 with 7.2 percent of functions over 10, and
the excess is concentrated: internal/runner/runner.go, internal/ltl/evaluator.go and
internal/verifier/worker.go own 8 of the top 30.
Duplication runs opposite to the assumption. Over 12-line normalised windows, source-only duplication is 1,036 lines across 23 cross-file blocks, 4.7 percent. Including tests it is 1,831 lines across 48 blocks, 3.1 percent. Tests are 62 percent of the code and produce 43 percent of the duplicated lines, so per line they are about half as duplicated as source. 18 of the 23 source blocks are one pair of packages.
Where the size difference actually goes
| bucket | here | peer | delta | legitimate |
|---|---|---|---|---|
| research and experiment tooling | 18,424 | 885 | +17,539 | yes, but it is not the tool |
generated protobuf and the .proto |
13,292 | 0 | +13,292 | half |
| core engine | 20,092 | 8,859 | +11,233 | partly |
| iOS target | 10,502 | n/a | +10,502 | yes |
| Android target | 6,296 | n/a | +6,296 | yes |
| browser target | 5,927 | 5,777 | +150 | yes |
| terminal target, peer only | 0 | 2,143 | -2,143 | |
| web UI and replay server | 6,333 | 2,477 | +3,856 | no |
| spec language | 6,122 | 1,407 | +4,715 | partly |
| fixture apps and example specs | 5,868 | 322 | +5,546 | mostly |
| LLM action policy | 4,036 | 0 | +4,036 | yes |
| second JS runtime and its parity tests | 3,785 | 0 | +3,785 | no |
| CLI and orchestration | 3,349 | 1,066 | +2,283 | partly |
| E2E harness and page fixtures | 1,618 | 1,868 | -250 | yes |
Roughly 44,500 lines of the gap is legitimate scope: two mobile targets the peer does not have, an LLM policy it does not have, and two fixture applications. Roughly 8,500 is mechanically removable generated code. Roughly 27,200 is structural.
Correctness findings
These are defects, not style. They come first because several of them cause the tool to report success while checking nothing, which is the worst failure mode a property-based checker can have.
A run that spent half its budget outside the app enters the survival analysis as clean
data. cmd/internal-tools/campaign/summary.go:50 writes precondition_failures, and
neither reader declares the field: analyze/load.go:32 and confusion-matrix/checker.go:56
each redeclare the campaign record independently. summary.go:188 counts those steps into
Steps, and the exclusion logic at analyze/load.go:190-204 filters on four conditions,
none of which is this one. summary.go:45-49 states the requirement in its own words,
"counting it as a run that explored and found nothing puts a harness failure in the same
column as evidence", and then the pipeline does exactly that. The record schema being
declared three times is why: analyze/load.go:45 already carries a duration_millis
back-compat shim for an earlier break that one shared type would have prevented.
iOS answers three contract methods with fabricated success.
internal/driver/ioscompanion/driver.go:1115-1125 returns [] from RecentLogs, zero from
Metrics, and a hardcoded Ready: true from Health. The preflight at
internal/runner/runner.go:1576 therefore checks nothing on iOS, and every state.logs
property holds vacuously with no machine-visible signal that it never looked. The fix is
not to implement them, it is to move them off the mandatory interface so that silence is
representable.
A truncated page is served as a 200. internal/replay/server.go:245 reimplements
io.ReadAll and swallows every error except fs.ErrInvalid, returning a partial buffer
with a nil error. One caller, 18 lines, and a half-rendered index.html is indistinguishable
from a whole one.
Actions render as raw text in the replay UI. replay-ui/src/lib/action-format.ts:9-19
lists textPrefix and classPrefix, which are not in the canonical key list at
pkg/spec/test/fixtures/selector-keys.json, and omits testTag, data-testid, identifier
and accessibilityIdentifier. internal/verifier/worker.go:893-915 writes testTag: labels
and pkg/spec/src/web-runtime.ts:1124-1135 writes data-testid:. testTag is the primary
Android identification route, so the most common case is the one that renders wrong.
A failed CDP round trip is indistinguishable from a page without the API.
internal/driver/chrome/driver.go:983-985 swallows its own error and returns zero metrics
with a nil error.
541 lines of tag-gated tests never run in CI, and 167 of those run nowhere at all.
Four test files sit behind build tags. internal/sidecarassets/embed_withsidecar_test.go,
167 lines including a four-goroutine torn-write race test, is gated on withsidecar, and no
target anywhere runs go test with that tag: withsidecar appears at Makefile:59,71,80,93
under go build, go install and go run only. It has never been compiled into a test
binary. The other three, internal/driver/ioscompanion/smoke_test.go at 85 lines and the two
embed_withcompanion_test.go files at 146 and 143, total 374 lines and are reachable only
through make test-companion at Makefile:148, which CI never invokes: ci.yml runs make test, make test-folio and make test-browser and nothing else that tests Go.
Separately and less severely, 671 lines are compiled but skipped at runtime for want of an
environment variable, 105 in internal/driver/ioscompanion/transport/integration_test.go
needing SANDERLING_IOS_INTEGRATION, which is set nowhere in the repository, and 566 in a
subject-specific draft needing a spec directory named by an environment variable. Those two are guarded
by design; the 541 are not. This is a coverage gap wearing bloat's clothing and it outranks
every deletion in this document.
One test does not test its subject.
internal/runner/runner_test.go:2104, TestRunner_InternalApplyErrorMarksTransitional,
asserts only err != nil and summary.Steps < 2. Deleting the entire transitional-marking
branch leaves it green. Its helper at :2084 also carries a comment saying the first
InputText call fails, while the method it overrides is TapSelector.
One test is racy by construction.
internal/driver/ioscompanion/supervision_test.go:47 sleeps 100ms hoping a shell has
installed trap "" TERM. If it has not, plain SIGTERM works, the escalate-to-SIGKILL path
the test name promises is never exercised, and the test passes anyway.
Structural findings, ranked by payoff
Move the research tooling out of the product module. 16,491 lines leave cmd/, and
internal/tracecorpus at 235 lines goes with its only two consumers. They compile and test
today only because go test ./... sweeps them up, at a measured 23.3 seconds per run.
docs/development/decisions.md:49-53 is the only written policy governing this directory,
it says the directory holds two dev and debug binaries and should stay "for now", and it now
holds eleven. Two of the eleven should not simply move: bundle-check is the only one a
spec author wants and should fold into the product as sanderling spec check, since it
already runs the product code path; hier-check should be deleted, as nothing references
it, its usage string at main.go:12 promises <dump.xml> while it parses JSON, and its
main_test.go never calls its own code.
Delete the page-side JavaScript and keep one producer of DOM facts. DOM fact derivation
and selector resolution exist four times: pkg/spec/src/web-runtime.ts; roughly 350 lines of
untyped JavaScript inside a Go raw string literal at internal/driver/chrome/driver.go:578-934
where prettier, tsc and noUnusedLocals are all blind to it; internal/hierarchy/hierarchy.go
with its own key list at line 302; and the replay-UI copy above. The evidence that this is a
standing hazard rather than a tolerable copy: every one of the nine commits that has ever
touched web-runtime.ts also touched driver.go or hierarchy.go. Nine out of nine. It is
currently held together by roughly 2,500 lines of parity tests, four probe entry points and
about 40 cross-reference comments. The drift postmortems in the file all report the same
outcome, at web-runtime.ts:389-393, :555-559 and :590-594: the two sides disagreed and
properties passed vacuously rather than failing. :161-166 records that the worked example in
docs/manual/spec-language.md named a control by a key that matched nothing on web, and passed
having checked nothing. Having the Go dump call the already-injected bundle removes the copy
and retires most of that parity suite.
Trim the vendored protocol. 43 RPCs are declared, 9 are called, and 2 of those 9 are
dead: internal/driver/ioscompanion/runner.go:302 says so in its own comment, "the driver
does not call these today; they complete the Companion interface". Generating only the 14
messages actually used removes roughly 8,500 lines with no behaviour change, and dropping
install and uninstall removes 133 more including tarGzipDirectory.
Collapse the twin sweep binaries. corpus-sweep and implementation-sweep share 331
exactly-identical meaningful production lines and 163 more between their two end-to-end
tests, which are 301 and 276 lines respectively, the two longest functions in the
repository. Ten of eleven flags are identical. The entire difference is whether an
implementation needs a build step first, which is a boolean.
Narrow DeviceDriver from 18 mandatory methods to 12. Screenshot has zero callers:
all three Snapshot implementations use their own path at chrome/driver.go:807,
ioscompanion/driver.go:1057 and sidecar/client.go:269. It exists for symmetry with
Hierarchy. RecentLogs, Metrics and Health become optional, which is what stops iOS
having to lie. Launch's env map becomes a capability that only the sidecar implements,
since iOS rejects it and chrome silently drops it. The six optional capability interfaces
are the right shape and should not be collapsed: each has a real consumer in the runner and
a real non-implementer, which is that pattern working correctly.
Stop maintaining two ASTs for one language. internal/verifier/bindings.go:28-58
declares a 10-value specKind enum and an 8-field formulaSpec with integer child edges
that mirror ltl.Formula one for one, and internal/verifier/worker.go:259-341 is an
83-line translator back, containing eleven copies of the same three-line child-and-error
block. Storing ltl.Formula directly deletes around 120 lines and, more importantly, takes
adding one operator from eleven edit sites to one. The isolation argument does not apply:
worker.go:287-294 already constructs ltl.EventuallyFormula literals by hand, so the
boundary is already crossed.
Replace the hand-rolled ordered-JSON encoder. internal/verifier/marshal.go:392-570 is
179 lines guaranteeing field order and unescaped HTML across the two hosts. encoding/json
already emits declaration order, omitempty already handles presence, and
SetEscapeHTML(false) is already in use at marshal.go:520. Tagged structs plus
goja.TagFieldNameMapper do the same job in roughly 90 fewer lines.
Merge the two asset packages. companionassets and runnerassets are the same 140-line
package twice, differing in 8 cosmetic hunks, and four dead exports fall out of the merge.
Both also validate tar member paths through safeJoin but do not validate header.Linkname
before os.Symlink.
Deduplicate the smaller copies. decodeDump's body is pasted byte for byte inside
MapHierarchy in the same package, input.go:420-433 against hierarchymap.go:77-88.
Bundle at internal/bundler/bundler.go:36 and BundleWeb at web_bundle.go:30 are 132
code lines differing in three expressions, and web_bundle.go:37 calls filepath.Abs and
discards the result before calling it again at line 44. The route-transition rule exists
three times with three different key sets, at hierarchy.go:654 with one key, settle.go:123
with five, and chrome/driver.go:902 with one, and they already disagree.
Tree.FindBySelectorPath and Node.FindBySelectorPath differ in three lines, all t against
n, as do the FindAll pair. parseBounds at hierarchy.go:1029-1053 contains the same
11-line body twice, once per regex.
Retire the dead instrument. Tree.UnreadableFlags at internal/hierarchy/hierarchy.go:106
is computed at :554, serialized twice, and read by zero production callers. Its two JSON
keys disagree with each other, unreadableFlags against unreadable_flags at :120, and
every json tag on Tree is dead anyway because Tree has a custom MarshalJSON.
Undocumented escape hatch. SANDERLING_SIMULATOR_COMPANION=legacy at
internal/driver/ioscompanion/driver.go:404 appears in no document, Makefile or CI config,
and gates roughly 313 lines: pasteText, findAllowPasteButton, pasteLanded,
grantPasteboardAccess and resolveInputField. Document it or delete it. Leaving it
undocumented and live is the worst of the three options.
Dead code, tool-backed. deadcode -test=false ./... reports 48 unreachable first-party
functions; with test roots included that falls to 1. So 47 of 48 are reachable only from test
binaries, which is over-exported test scaffolding rather than dead product code. The single
genuinely dead one is Cache.Root at internal/replay/runs_cache.go:29. staticcheck -tests=false reports 12 U1000, eleven of which are one statistics island in
cmd/internal-tools/analyze that deadcode independently flags, so both tools agree.
Confirmed zero-caller exports in the product: ltl.Describe at formula.go:235,
ltl.EventuallyBefore at :158, ltl.EventuallyWithin at :162, ltl.Thunk,
ltl.Eventually, Evaluator.Observe at evaluator.go:81, and
trace.ActionSourceSeeded at writer.go:118. The ErrorFormula arms in reduce
(evaluator.go:395-399) and pushNot (nnf.go:49-50) are unreachable, since ErrorFormula
is built only at worker.go:771 for serialization and never re-enters an evaluator.
Two more the tools did not reach, both found while correcting the comments beside them.
isWDADrop in internal/runner/runner.go and its two call sites classify an error string
nothing can produce any more: WdaRecovery is constructed only in its own test
(DriverBackend.kt:1557, WdaRecoveryTest.kt:16), so the sidecar no longer throws it.
testrun.preflightDevice was dead in a way that also made it wrong, and is deleted: it was
called only after web and both iOS branches had returned, so it never saw a platform other
than android, and it returns nil for anything but ios. The comment over its call site said it
was Android's java check, which is in runPreflight, and its own premise was wrong because a
physical iOS device is runner-only over usbmux and needs no JVM.
One dead feature spans four layers. trace.Step.Snapshots at internal/trace/writer.go:27
has no producer, yet replay-ui/src/panels/SnapshotTable.tsx renders a tab for it, wired at
RunDetail.tsx:141. Every step of every run shows an empty panel. Separately,
replay-ui/src/panels/Timeline.tsx, 142 lines, is never rendered: its only importers use
import type, and the RunHistory.lanes tail it feeds has zero readers.
VERB_SUPPORT is a no-op. pkg/spec/src/verbs.ts:18 maps every verb to all three
platforms, so supports() cannot return false, which makes warnUnsupportedOnce,
resetWarnings, Host.reportUnsupported at three call sites, one pick.ts:103-106 branch
and the whole of verbs.test.ts unreachable, along with 26 pointless resetWarnings() calls
in pick.test.ts.
runner.go is not mostly ceremony. At 1,818 lines it is 1,233 code and 500 comment, and
it does five jobs in one file rather than 500 lines of work in 1,818. It splits along its
existing seams into runner, foreground, dispatch, observe and trace encoding, largest piece
around 420 lines, with no logic change. Run is 473 lines with a cyclomatic complexity of
60, the highest in the repository.
One abstraction is justified by a false statement. internal/runner/source.go:41 says the
interface is declared there "rather than folded into driver.WebDriver so the mobile drivers
stay untouched", but WebDriver is web-only and no mobile driver implements it, so the error
branches at source.go:102-116 are unreachable. idleTimeoutFloor at runner.go:630 cites
the same false reason. Similarly, ltl.PredicateLabel at internal/ltl/formula.go:17-22
exists so that formula.go:394 can type-assert a ThunkFormula that cannot fail the
assertion, replacing a single field read.
One correction to an audit finding. The {"op":"now"} serialized node was reported as
having no consumer. It has one: replay-ui/src/types.ts:106 declares it in the ResidualNode
union and replay-ui/src/components/ResidualNode.tsx:41 renders it. The agent searched for
web/src, which does not exist here, and concluded the UI did not either. NowFormula is
still the identity function at every evaluator site, but the wire node is load-bearing and
must not be dropped.
The test suite, measured
| package group | test | source | ratio |
|---|---|---|---|
| runner, drivers, testrun, cmd | 32,886 | 23,166 | 1.42 |
internal/runner alone |
7,270 | 2,589 | 2.81 |
internal/verifier |
5,218 | 3,179 | 1.64 |
internal/hierarchy |
1,886 | 1,063 | 1.77 |
internal/ltl |
1,595 | 1,045 | 1.53 |
Genuinely deletable across both audited scopes is roughly 2,600 lines, about 5 percent, not the 40 percent the size ratio suggests.
Every one of the 1,281 test functions was then classified mechanically by the condition
guarding its assertions. 47 functions, 658 lines, assert only on nil, on err, or on a length
against zero. 896 functions compare actual values and 315 more compare against a golden file
or by deep equality. The residual "no assertion" bucket is 23 functions and is almost entirely
an artefact of the classifier, since these tests embed spec source in raw string literals
containing a brace at column zero; the genuine case is mock_test.go:123. So the mechanically
detectable garbage is roughly 800 lines, which reproduces the reading-based estimate from a
different direction.
That figure is a floor, not a ceiling. The method cannot see tautology:
internal/driver/ioscompanion/keymap_test.go:202 compares two values and is worthless,
because the values are constants declared four lines away in keymap.go:32-33. Tests of that
shape score as healthy. The honest statement is that the suite's assertions are overwhelmingly
real, and that an unmeasured remainder above 800 lines is tautological.
The bloat is not in the tests. Go test files hold 43,435 lines, of which 29,770 sit inside a
test function. The other 13,665 lines, 31 percent of the suite, are scaffolding: 256 test-only
helpers, 65 test-only types, 26 one-off wrapper drivers in a single package, traceLine
redeclared fifteen times, and four separate spec bundlers in one package's tests. Consolidating
the harness is worth several times what deleting tests is worth, and it costs no coverage. What is actually wrong is repetition of shape:
- 26 one-off wrapper drivers live in one package, ten of them in
runner_test.go, six of which overrideTapSelectorto return a different error on call N.mock.goalready hasFailures map[ActionKind]error; one per-call hook retires all 26, about 280 lines. type traceLine structis redeclared inline ten times insiderunner_test.goand five more times across the package, whilereadTraceLinessits atrunner_test.go:1595andtrace.jsonlis opened by hand in 24 places. Two fields ontraceStepLinecollapse all fifteen, about 250 lines.- 57 of 98
internal/hierarchytests have bodies of 12 lines or fewer, nearly all one row of an unwritten table. Nine clusters, roughly 650 lines, collapse into tables. Four properly table-driven tests already sit in the same files, so the pattern was known and not applied. - 72 of 98 hierarchy tests write
tree, _ := Parse(...), so a fixture that stops parsing panics on a nil dereference insideFindinstead of reporting a parse failure. - Four separate spec bundlers exist in
internal/verifier's tests, differing only in the alias map. - Confirmed subsumed:
chrome/driver_test.go:140,:202and:264are superseded byfact_parity_test.go:92, which reads the same fields with forced both-polarity coverage and cross-checks the real web runtime, while the middle of the three does not call the web runtime at all despite its name. 163 lines. Alsorunner_test.go:351by:374,runner_test.go:240byllm_source_test.go:837, andrunner_test.go:1226byscroll_distance_test.go:52.
Three tests cannot go red at all. internal/driver/mock/mock_test.go:123 is a compile-time
assertion wearing a func Test hat, with no assertion and no use of t.
internal/driver/ioscompanion/keymap_test.go:202 asserts two constants equal the literals
they are declared with four lines away in keymap.go:32-33.
internal/verifier/llm_test.go:674 installs a stub returning "sampled" and asserts the
function returns "sampled", under a name claiming it draws from a corpus that is never
reached.
Two structural gaps matter more than any of the above. The two-host parity tests at
internal/verifier/marshal_test.go:166 and :299 assert that the two encoders agree with
each other, with no golden anywhere, so a field rename landing on both encoders at once,
which is the likely change, passes silently. The same gap exists at redaction_test.go:174
and :193. And cmd/internal-tools/hier-check/main_test.go:11-21 tests hierarchy.Parse
and tree.FindAll directly, so if hier-check's own formatting or exit codes break, nothing
notices.
Rule compliance
| rule | verdict | count |
|---|---|---|
| zero comments first, WHY never WHAT | ignored on volume, followed on quality | 7,534 comment lines under no linter mandate, 35% WHAT in a 60-block sample |
| never use an em dash | followed in source | 0 tracked, 7 in commit messages, 43 in gitignored research/ |
| never write a directory tree | one violation | docs/manual/runs.md:23-27 |
| README under 15 lines | partly | 3 of 5 over budget, 277 lines total |
| lowercase kebab-case doc names | partly | 11 flagged, 1 committed and discretionary |
| no abbreviated identifiers | ignored | 683 occurrences, 51 distinct identifiers |
| commits 1-3 files, under 20 lines | ignored on lines, followed on files | 1,216 of 1,864 commits over 20 lines, 194 over 3 files |
| no filler, every line load-bearing | partly | 123 of 1,565 comment blocks restate the code |
Two of these need care rather than a sweep. AttrFilter, AttrSelector and the Attr
field are baked into the public selector API and leak into pkg/spec/src/types.ts, so that
abbreviation cannot be removed without a breaking change; it should be scheduled, not
grepped away. And ctx context.Context is idiomatic Go and was excluded from the count.
Go convention does not ask for fewer comments in general, it asks for doc comments on exported symbols and for inline comments to be rare. Measured against that, this repository is close to correct: 3,512 doc-comment lines against 22,155 lines of code, and only 1,164 inline comment lines, 5.3 percent. Three quarters of the comment volume is the convention being followed. Comparing a single total against a project that writes few doc comments overstated the problem, and the earlier 20.8 percent figure conflated the two kinds.
The comment rule failed hardest outside the engine.
examples/folio/sanderling/predicates.ts is 61.7 percent comment, 472 comment lines to 293
code, with two inline blocks of 13 and 17 lines wrapped around about 12 lines of guard
clauses. pkg/spec/src/action-tree.ts is 47 percent comment. Inside the drivers the
opposite holds: nearly every comment records a measured symptom, and the genuine
WHAT-comments there total only 60 to 80 lines, most of them on the legacy paste path that
should be deleted anyway.
Comment rot is the single largest defect in the repository's prose, and the first pass of
this audit understated it badly. A per-block classification of 1,565 comment blocks, 95
percent of every comment in non-test source, finds 78 that are false: they disagree with
the code beside them. The first pass named two. Detail is in the comment section below.
internal/runner/runner.go:185 claims a returned error propagates to every sibling read,
while all three goroutines return nil at lines 198, 202 and 207, and line 209 admits the
Wait error is always nil, so the errgroup is a sync.WaitGroup plus a dependency plus an
untrue comment. internal/testrun/testrun.go:93-99 stacks three doc comments over the wrong
function, leaving Execute and buildRunMeta undocumented. The worst single instance is
internal/runner/runner.go:760-768, nine comment lines above one constant, eight of them
narrating a rejected design and two emulator measurements. The measurement is worth two
lines; the rest is a lab notebook checked into a runner.
Repository hygiene
Nothing untoward is tracked at HEAD. 674 files, 5.32 MB, no secrets, no build artefacts, no
vendored bundles, no dist, no node_modules. The problems are all around it.
.git is 659 MB for 5.32 MB of content, a ratio of 118 to 1. The largest object in the
repository is a 138.9 MB compiled binary carrying the dead pre-rename project name, blob
9788374f, reachable only from stash commit 6bb190f, "untracked files on feat-inspect-ui".
A git stash swept up a compiled binary and refs/stash has pinned it ever since.
.git/lost-found/other holds a further 152 MB of git fsck salvage that no ref points at,
and there are 12 packs, 479 prune-packable objects and 65 local branches against 42 remote.
Separately, a 35.4 MB compiled binary is permanent in master history, added in 88db0cb
and removed in 2a1b263, along with 426 KB of UI bundles: 35.8 MB total, about 6.4 times the
entire legitimate tree and roughly 70 percent of what a fresh clone downloads. Ignoring these
afterwards stopped recurrence but every clone still pays.
keys/ holds live Apple distribution secrets: an AuthKey .p8, a distribution private key
PEM, a .p12 and a mobileprovision. History is clean and nothing has leaked. The only thing
standing between them and a commit is one line, keys/*, appended at .gitignore:87 with no
section comment, and git check-ignore keys shows the directory itself unmatched. Signing
material belongs in the keychain, not in a git working tree. scripts/ is currently
untracked and unignored, so it is one git add -A away from being committed, which is one
reason the project bans that command.
The working tree holds 9.16 GB of untracked output: 4.3 GB under conformance/runs, 2.7 GB
of run artefacts under examples, 92 MB of Gradle jars under sidecar/build, 141 MB under
companion, 37 MB each for bin and a stray sanderling binary, 25 MB in runs. Five
empty idb-* directories and an empty memory/ directory have sat there since April and
June and are not ignored.
Twelve of 33 make targets are dead or undocumented, conformance/gates.sh at 574 lines with
a self-test mode and committed fixtures is referenced by nothing, and there is no CI format
check despite five fmt-* targets existing.
Documentation is accurate where it counts and stale in three places. Twelve of fifteen
spot-checked claims verified against the code, including every one of the 22 CLI flags, which
are all documented in docs/manual/cli.md with no undocumented and no dead flag. The three
failures: docs/manual/action-space.md:15 says PressKey has 8 keys where
pkg/spec/src/types.ts:209-218 has 9, missing escape; action-space.md:14 documents a
durationMillis? field on Scroll that does not exist; and docs/development/decisions.md:25-27
says marshal.go moves to internal/replay/, which never happened, while :47-49 describes
a runs.go split as pending that has already been done. README.md is 40 lines against its
own 15-line standard, with lines 36-40 byte-identical to docs/index.md:25-29, the manual
link list repeated four times and the pitch three. examples/folio/README.md is 150 lines
with nine headings that are all docs/ content. decisions.md and action-space.md are
orphaned, stale and linked from nowhere.
What must not be touched
A cleanup driven by line count would destroy the best work in the repository. The following were examined and found to be load-bearing.
internal/ltl/evaluator.go in full: reduce, the three-valued finalize, the bounded
always and eventually duality at :528-540, collapse's describe() key with per-thunk
identity, nnf's implication rewrite and isOneShotRoot. It is a real standalone evaluator
carrying both step and wall-clock bounds, where the peer project's equivalent carries
duration only. It is the intellectual content of the project and it is dense because the
problem is.
internal/verifier/redaction.go in full, including the three-valued secureFact that treats
an unreported target as a credential, which is the correct default. The
attributeAliases and selectorKeys tables, recordableValue, Node.scopedNodes' spatial
fallback, innermostMatches, and the ActionWireContract handshake mechanism, which is
versioned precisely because the format already drifted once and every scroll silently
travelled zero distance.
In the runner: clampGestureToSafeArea, structuralShape, the transitional-skip and action
hold-back at runner.go:254-356, confirmFocus and otherElementHoldsFocus, the
extractor-override consistency checks at :309-324, fetchSyncedState's unchanged-JSON
early exit, resolveIdleTimeout, applyBound, runOutcome's three error types, and all of
internal/seedspec, internal/llmclient and internal/replay/watcher.go.
In the drivers, roughly 60 items flagged as battle scars: the 354 to 616 pixel fling range, the 136ms Compose accessibility lag, the four-minute XCTest wedge, and the rest of the measured device quirks. Chrome and iOS are dense because those platforms are hostile. The optional capability interfaces are the right pattern for heterogeneous targets and should survive the narrowing of the mandatory set.
In the tests: the cross-host golden tests, parity_test.go, host_parity_test.go,
selector_keys_test.go, extractor_encoding_test.go and policy_parity_test.go. They
assert wire bytes against committed goldens, they name the bug class, and several clearly
went red once.
Order of work
Correctness first, because these make the tool lie: the precondition_failures exclusion in
the analysis pipeline, the three fabricated iOS health responses, the partial read in the
replay server, the selector vocabulary drift in the replay UI, the swallowed CDP error, and
wiring the 541 lines of tag-gated tests into CI.
Then the removals that carry no behaviour risk: move the research tooling out, regenerate the
protocol from the 14 messages actually used, delete the dead Snapshots panel and Timeline,
delete VERB_SUPPORT and its unreachable tail, merge the two asset packages, collapse the
twin sweep binaries.
Then the structural changes, each of which needs its own branch and its own green CI: one
producer of DOM facts, a 12-method mandatory driver interface, ltl.Formula stored directly
in the verifier handle, encoding/json in place of the hand-rolled encoder, and runner.go
split along its five existing seams.
The test suite comes last and is a consolidation, not a purge: one per-call failure hook on
the mock, one traceStepLine, nine table collapses in hierarchy, and goldens where two
encoders currently only agree with each other.
Doing all of it puts the tool at roughly 60,700 lines with a further 18,400 in a separate research module. That is 2.4 times the peer project's own tool, which is earned by two mobile targets it does not have, an LLM policy it does not have, two fixture applications, and a test ratio deliberately kept above its 0.35.
Second pass: how the tests are built, not what they assert
The first pass measured whether tests could be deleted without losing coverage and answered "mostly no, about 5 to 10 percent". That answered the wrong question. Deletability and engineering quality are independent axes, and on the second axis the suite fails its own standard comprehensively. The rule says tests are first-class code held to the same or higher standard than the code they cover. Measured against source, they are held to a visibly lower one.
Go, 135 test files, 41,988 lines, 1,259 test functions:
| measure | value |
|---|---|
| test-only helpers | 457, totalling 4,560 lines |
| of those, pure duplication | about 1,860 lines, 41 percent of the helper layer |
| test-only types | 67 top-level plus 20 function-local |
t.Run subtests across 1,281 test functions |
108 |
t.Parallel() anywhere in the repository |
0 |
cmp.Diff / testify / reflect.DeepEqual |
0 / 0 / 11 |
| hand-rolled comparison assertions | about 97.8 percent |
| clusters that should be tables | 51, covering 355 tests and 5,758 lines |
The specific duplications, each verified: five spec bundlers differing only in an alias map
(verifier_test.go:42, setup_action_test.go:18, spec_integration_test.go:16,
action_encoding_test.go:36, and internal/runner/runner_test.go:131, of which the first
and last are character-identical apart from the name; the last is in a different package, so
four of the five sit in internal/verifier); traceLine redeclared 14 times in
internal/runner while readTraceLines and traceStepLine sit at runner_test.go:1586-1595
and are a superset of nearly all of them; 27 wrapper driver types in one package, of which
snapshotFailFirst:1925, tapSelectorFailFirst:1998 and internalApplyErrorFailFirst:2087
are the same seven-line method with a different error constant, while mock.go:64 already
has Failures map[ActionKind]error and needs only a call-count field; 43 byte-identical
eight-line Chrome launch preambles across four files, 344 lines; 48 identical
context.WithTimeout plus Run(ctx, Options{}) blocks, 576 lines, beside a harness struct
at runner_test.go:78 that has four fields and no methods; and 347 identical lines shared
between corpus-sweep/end_to_end_test.go and implementation-sweep/end_to_end_test.go,
including six byte-identical helpers.
A shared internal/testsupport package holding file and run-directory writers, one trace
reader over one wide step type, a tree builder, a Chrome page helper, and the spec bundler
removes about 1,860 lines without touching a single assertion. Collapsing the 51 table
clusters removes about 1,700 more. Roughly 3,560 lines in Go, with the suite asserting
strictly more than it does now, because the next test becomes cheap instead of becoming
another copy.
The same shape holds in the other two languages. In Kotlin, StubDriverBackend is declared
in production source at sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt:507
and wired as a live backend fallback at Main.kt:93, carrying an injectable commandRunner
seam and five @Volatile spy fields that ship in the production jar. Eight test files import
it as their fake. The suite has no fake of its own because the fake was moved into main.
Alongside that, sidecar/build.gradle.kts:56-62 declares kotlin.test, JUnit 4.13.2, JUnit
Jupiter 5.11.3 and the vintage engine to run 4 under 5, with 12 files importing
org.junit.Test against 3 importing kotlin.test.Test. countRouteScreens is tested from
two files with two separately invented tree builders, and 15 sidecar test files target two
production files and should be about 7.
In TypeScript, .each is used zero times while 26 files hand-roll for (const x of [...]),
and 61 percent of the 450 cases carry exactly one assertion.
folio-submit-balance-predicate.test.ts is 625 lines over 40 cases, all driving
const submitOn = "testTag:LedgerScreen > testTag:TxnSubmit" at line 9. TxnSubmit exists
in exactly one place in the application, AddTransactionScreen.kt:88, and LedgerScreen does
not contain it, so that selector cannot match anything real. Its sibling
folio-submit-window.test.ts:11 declares the same constant correctly. A duplicated fixture
constant, copied rather than imported, edited to something fictional, with 625 lines built on
top, all green. The predicates under test are pure, so the arithmetic is still exercised; the
failure is that the fixture claims to model the application and does not.
Inline fixture bloat, which the first pass expected to find, is not a real problem in any of
the three languages. Kotlin has zero literals over 30 lines. TypeScript has zero standalone
data literals over 30 lines. Go has three raw strings over 30 lines totalling 120. The
committed testdata corpus is 88 files, 264 KB, 3,497 lines, and is the best-managed part of
the suite: conformance/testdata is 11 gate fixtures regenerated by a committed
generate.py, and .github/scripts/testdata is six captured golden runs consumed by CI.
Near-duplicates across all of it total about 90 lines. The growth is in test code, not test
data. What Go does have is 395 hand-written hierarchy JSON nodes across 28 files with no
shared builder, and testdata/ in only 6 of 33 packages;
internal/hierarchy/hierarchy_test.go is 1,823 lines with the largest inline corpus in the
tree and no testdata/ directory at all, while eight other packages already have one.
Second pass: the growth pattern
Across the last 50 commits, test lines added 51,444 against 3,891 deleted, a ratio of 13.2 to
- Source over the same window is 5.4 to 1. Tests are removed at roughly a third the relative rate of source.
In the last 30 commits, 43 test files were added into a package that already had a file
testing the same unit. internal/runner took 14 of them alongside the existing 3,181-line
runner_test.go, and they carry the duplication that decision implies: five near-identical
web-target fakes each redeclaring the same five methods, and five hand-rolled trace scanners
each doing bufio.NewScanner with an 8 MB buffer beside the existing readTraceLines.
internal/driver/chrome took four, carrying four byte-identical probe installers, four
matching probe entry files and four fixture pages. test/browser has four spellings of one
harness: runFixture, executeFixture, launchConsoleFixture, and servePage plus
runBinary. pkg/spec/test took eight folio files against three that existed, so
countSubmitsInWindow is now tested from five files and readHomeCards from four, each
redeclaring its own builders.
Only three commits in the last 60 have a negative test delta, and all three are subsystem
removals: the in-app SDK, the JVM iOS backend, and a rename. Exactly one commit in 60 deleted
test content because the tests were bad, faebfe3, which cut
examples/folio/sanderling/spec.ts by 37 added against 122 deleted, removing properties that
could not fail.
The commit named test: full test-suite refactor sweep (#61) is the clearest single case. It
is net plus 2,674 lines. Its body carries 34 bullets of which one is a deletion. Of its 125
deleted test lines, one is a genuine rewrite, internal/ios/ios_test.go at minus 53, beside
a real extraction of pure simctl parsers; the remaining seven files give up 8 lines or fewer
each. It also added eight new test files. A refactor that deletes nothing has not refactored
anything.
Two counterexamples belong on the record, because the discipline has been demonstrated here
and simply not repeated: #52 rewrote api.test.ts at plus 125 against minus 134 and
defaults.test.ts at plus 23 against minus 93, both in place, both deleting more than they
added. cmd/internal-tools/analyze is properly layered, with one writeCampaign and five
thin scenario wrappers delegating to it. The pattern was available in this repository the
whole time.
One nuance argues against a naive purge. internal/driver/chrome/fact_parity_test.go
supersedes four tests in driver_test.go, but parity asserts that the two fact producers
agree, not that either is right, so those four still pin ground truth. The correct move was
to fold them into the parity fixture. Neither folding nor deletion happened, and
element_state_test.go then repeated the whole pattern a third time on a fourth fixture page.
Second pass: compatibility code in an alpha project
README.md:25 states the project is alpha. Genuinely deletable compatibility machinery is
454 source lines and 558 test lines, rising to 493 and 632 once a stale trace corpus goes.
The largest single item was reported as the action-wire/2 revision check,
internal/verifier/marshal.go:598 plus four files, on the grounds that it never reads
revision 1 and so buys only a better error message. That reading was wrong and the entry is
withdrawn. Refusing a pairing is not supporting it: the check exists because
@sanderling/spec 0.0.3 and earlier serialized an authored Scroll with the container's own
point as both endpoints, and this binary reads pre-computed endpoints as authoritative, so
every such scroll dispatched successfully as a 250ms press and hold that travelled zero
distance and no run reported it. Without the handshake the two halves run to completion and
the campaign is void. It stays, and this pull request finishes it. The first item is SANDERLING_SIMULATOR_COMPANION=legacy, gating about 330
lines and appearing in no document, Makefile or CI configuration. Third,
cmd/internal-tools/analyze/load.go:43 carries a duration_millis shim for campaign records
written before two clocks were split: across the committed corpus, 0 of 308 campaign records
carry duration_millis and 308 of 308 carry monotonic_millis, so it defends against
nothing. load.go:55 makes Actions a pointer to refuse older files whose field is present
on 308 of 308 records.
The more important finding is the opposite of the brief. Four comments label current,
documented, dominant behaviour as legacy or backward-compatible, and acting on them deletes
live surface: internal/verifier/bindings.go:217, internal/driver/chrome/translate.go:16,
internal/hierarchy/hierarchy.go:8 and cmd/sanderling/doctor.go:27. A fifth,
cmd/internal-tools/analyze/load.go:62, was listed and does not belong: it explains why
UnattributedActions is a pointer, which is a live refusal two tests assert, and it never
calls anything legacy. All four of the real ones are fixed. The clearest is bindAlways, whose comment calls a
predicate function the "legacy shape" and a formula handle the "new shape". This was first
recorded as the predicate shape being used 8 times and the formula shape 0, which is wrong in
both directions. Counted against the tree: all six properties in examples/folio/sanderling/spec.ts
and examples/folio-web/sanderling/spec.ts pass a formula, the two shipped default properties
in pkg/spec/src/defaults/properties.ts pass a predicate, and pkg/spec/test/api.test.ts:189
passes a predicate. Both shapes are current and both have real callers, so neither is legacy
and the comment was wrong to rank them at all.
Ordering matters for the trace-format items. 342 of 698 committed trace.jsonl files carry
no trace_version and no depths, and the gates currently refuse them. Deleting the version
machinery first turns those refusals into nil-root trees that resolve no selectors, which is
silent wrong data. Delete the stale corpus first, then the code.
Second pass: comments, classified
1,565 comment blocks were classified against the code beside them, 95 percent of every comment block in non-test source.
| bucket | blocks | share |
|---|---|---|
| godoc on an exported identifier, correct | 540 | 34.5% |
| why, carrying a fact not recoverable from the code | 751 | 48.0% |
| restates the code | 123 | 7.9% |
| compensating for a bad name | 8 | 0.5% |
| compensating for a function that is too long | 9 | 0.6% |
| design essay, notebook, or history | 56 | 3.6% |
| stale or false | 78 | 5.0% |
The volume complaint does not survive this. Four blocks in five are godoc or a
non-recoverable fact, which is what Go convention asks for, and the 20.8 percent line-share
figure overstates the problem. About 1,332 of 6,458 comment lines, 21 percent, should be
deleted or refactored away. internal/driver/chrome is the strongest package in the tree, 68
of its 123 blocks being measured browser quirks with no bad-name, too-long or essay findings
at all. examples/folio/sanderling/predicates.ts at 61.7 percent comment is the worst.
The real defect is falsity, and several of the 78 are load-bearing:
internal/hierarchy/hierarchy.go:9 documents id:<suffix> as a substring match on
resource-id. The code at :412-414 is ResourceID == value or HasSuffix(ResourceID, ":id/"+value),
so id:Button does not match com.x:id/saveButton. desc: at :417 is worse than reported:
it is exact-or-prefix-before ", ", not a substring match at all. This is the package doc a
spec author reads first, and docs/manual/spec-language.md:60,63 has it right, so the package
doc was the only wrong copy. Its "(backward compat)" label on id: was wrong too: id:
appears about 375 times across the tree against 16 for idPrefix:, so the form marked legacy
outnumbers the other by 23 to 1.
hierarchy.go:43 declares Bounds an inclusive rectangle while Width() returns
Right - Left, so every width, height and centre in the tool was off by one against its own
stated semantics. Settled against the producers: all three emit exclusive right and bottom
(sidecar/.../DriverBackend.kt:1624-1626 states it outright, chrome/driver.go:696 writes
getBoundingClientRect's right and bottom, ioscompanion/hierarchymap.go:190 writes
left + width), and runner.go reads screen.Width() as the viewport width, which only
holds under the exclusive reading. The code was right, the comment was the only wrong copy,
and a test now pins the arithmetic on the abutting and one-pixel cases the two conventions
disagree on. Separately, the two parseBounds pattern comments named the wrong producers in
both directions: the paired [x1,y1][x2,y2] form is what both device backends emit, and the
flat one is chrome and the sidecar stub.
internal/driver/driver.go was documented from the Android sidecar and never revisited for
Chrome, producing six false statements on the interface every driver implements: :52 says
an empty minLevel defaults to "E" while chrome/driver.go:823 returns every level; :47
says Snapshot runs under a backend-side mutex while chrome/driver.go:802-812 takes no
lock, and that claim has already been copied into runner.go:1430; :58 calls HeapBytes
RSS while Chrome feeds it usedJSHeapSize; :101 says the gate prefers FocusedWindowChecker
while runner.go:836 requires ForegroundChecker; :175 names a WebAction type that
exists nowhere.
iOS stopped routing through the JVM sidecar and 11 comments still say it does, including
three that name an IosDriverBackend type which does not exist. Eleven TypeScript comments
are anchored to Go symbols that were deleted. verifier/types.go:22,26,29 deny Scroll its
endpoints and duration and deny DoubleTap and LongPress their coordinates. marshal.go:95
and runner.go:1289 now state opposite rules for the same selector-and-coordinate conflict.
DriverBackend.kt:141 documents pollUntilStable as not charging reads to the stability
streak, but now is sampled after snapshot() returns, so the code permitted exactly the
failure its own worked example presents as impossible, in the settle primitive every Android
step depends on. This one turned out to be a code defect, not a comment defect. On the
comment's own numbers (streak 750ms, interval 250ms, read 500ms) the poll returned having
watched 250ms of quiet. The comment arrived in b02e86b with the streak parameters and the
now-after-snapshot sampling was left as 88db965 wrote it. Fixed by sampling the read's
start; the existing test measured from the end of the first read, so it included an interval
plus a whole read and passed against the bug at any read length. runner.go:95 says the caller is responsible for launching the app, and
Run launches it at :913.
The bad-name and too-long buckets are small but they are the ones where the comment is a
symptom rather than the disease. runner.Run at 473 lines carries five multi-paragraph
signposts, 62 lines, that exist only because there is no function boundary; extracting
observeDevice, verifyStep, applyChosenAction, recordStep and settleAfterStep deletes
all five. Four foreground readers with four failure policies carry 40 doc lines between them,
one of which is a 10-line comment over a single delegating line. held := skippedVerification
at runner.go:352 is a pure alias that exists so that a 15-line comment has somewhere to
live. In ioscompanion, a type named runner collides with the XCUITest runner, the
xcodebuild session and internal/runner, which is why driver.go:1211 has to write "the
text runner"; renaming it textInputBackend retires the comment.