agent-desktop/docs
Lahfir adf2c36bde
feat: windows resolution and live locator (sub-phase 2.5) (#120)
* docs: plan the windows resolution and live locator work

Eight units grounded in the probe corpus, the shipped 2.4 resolver, the
macOS reference implementation, and core's live-locator contracts. The
research reframed the work honestly: 2.4 already shipped a binary
exact-evidence resolver, so 2.5 upgrades it to graded three-state matching
with the fingerprint fallback Electron's measured 0% AutomationId coverage
demands, implements resolve_locator_anchor so find stops failing, and
builds the five live readers over the machinery earlier sub-phases
hardened.

A five-persona review ran before commit and its findings are folded in:
the KTD3 composition rule (core's identity_match alone would undo the
A7-3 wrong-target defense - two core verdicts compose, with
stable_text_match as the sanctioned visibility promotion), mid-search
element-vanish routed incomplete-retryable rather than terminally stale,
the reader-path liveness corroboration given a real data path through the
element wrapper, the Electron path-survival measurement added before the
tier that relies on it, the path fast-path eligibility gate and the
positive-area geometry gate, shape-only ambiguity details kept against the
macOS pattern that would violate the redaction gate, and the secure-field
question split read-side/action-side with the action remainder routed to
its owner.

* docs: close the second review round on the resolution plan

The round hunted second-order defects in the first round's fixes and found
real ones. The retryability stamp is one write, not two - a details bool
that IS the typed channel - so the completed-search stale ref now mirrors
the macOS complete-absence shape instead of a blanket non-retryable that
would have disabled core's hydration retry and failed find permanently on
first churn. The anchor variant settles path misses immediately rather
than replaying a stale path inside the adapter loop, so core's fresh
re-observation - the actual cure - keeps the deadline. The KTD3
composition gains the no-text-identity gate that kept icon-only refs from
spinning retryable, keyed on the live field rather than the verdict. The
degenerate-hash ref class is named unresolvable-by-design with the census
attributing it, the handle-payload change names its downcast consumers,
the KTD10 write-back joins U8's enumerated list, the Electron probe leg
gets its repo-controlled vault and unmeasurable branch, and four
edit-introduced contradictions from round one are reconciled.

* docs: align the resolution plan's done criteria with the second review round's decisions

* feat: measure the windows resolution unknowns (probes 17-resolution)

Stand up the A17 probe family for sub-phase 2.5 so no resolution or live-read
fact ships unmeasured: find_all versus the shipped walk on both fixtures, the
live 0/1/N candidate counts over duplicate-evidence pairs, the single-element
strict-resolve and shared-read timing envelopes, and the ambiguity census over
the WPF and Win32 trees. The WinForms fixture gains an equal-count ListBox
swap and a duplicate-evidence button pair; the swap arm triggers the mutation
by WM_APP because the custom AutomationId provider suppresses Invoke. The
Electron path/geometry leg stages a repo-controlled Obsidian vault; the first-
contact shell and an unstageable marker tree are recorded per row with U7's
judged rate as the measurement of record. A17 rows added to the ledger with
the pre-committed branches taken, the hunk index re-synced to the measured
diff, and the probe wired into the capability-probe workflow for the second
environment.

* feat: make windows resolution matching three-state

Replace the boolean candidate comparator with core's composed verdicts so a
candidate that could not be read is incomplete, never a non-match. The role
list and stable-text rules live in core only; the intended visibility
promotion exposes stable_text_match and the Windows resolver composes
identity_match for the id tier with stable_text_match corroboration only when
the stored ref carries a stable text identity (KTD3). The search gates on role,
flags incomplete on an unreadable candidate or role, and the resolver returns
an incomplete-and-retryable AppUnresponsive instead of a settled STALE_REF
when everything readable found nothing. A7-3's wrong-target pin is carried
through the composition, and the blank-cannot-refute and vulnerable
mutable-value name edges are pinned both ways.

* feat: add the graded resolution fallback to the windows resolver

The identifier-free Electron shape now resolves through two graded tiers
before the broad search: a window-rooted path fast-path (path is a locator,
never identity - a miss or refuted candidate falls back to the broad search
and is never settled on), and a geometry promotion that resolves an unreadable
candidate on a unique live bounds-hash match. The promotion mirrors macOS's
predicate with the A17-7-measured addition that the stored hash must come from
a positive-area rectangle - offscreen and virtualized elements collapse to
shared zero-extent bounds and never promote. The search family is split into
resolve_search (the plan's named seam) to hold the three files under the
400-line cap. Live fixture tests prove a blank secure ref resolves through
path and geometry on an unchanged tree and the secure value reaches no error.

* feat: classify settled absence and retry only the genuinely incomplete on windows

Resolution and live reads now carry a typed complete/retryable pair over a
three-way read-path classifier (the kAXErrorIllegalArgument lesson ported): a
settled absence like not-supported or invalid-argument is never retried, a
transport or timeout failure and a vanished element are incomplete and
retryable within the deadline. UIA_E_ELEMENTNOTAVAILABLE keeps the granularity
split - settled stale for a read of the resolved target, incomplete-and-
continue for a node vanishing mid-descent, window-not-found at root. The
completed-search STALE_REF mirrors resolution_complete_absence with retryability
derived from the code default, and resolve_element_strict runs a deadline loop
that retries only the stamped-incomplete, re-verifies process liveness through
resolve_window_root on every attempt, and stamps deadline_elapsed onto the last
diagnosis on expiry rather than discarding it for a bare timeout. Classifier
arm-by-arm tests, retry call-count pins, and the redaction pin land with it.

* feat: add the shared single-element live read and the five readers on windows

One read bends all five adapter readers: a resolved NativeHandle is downcast,
corroborated against the verified process identity stamped into the handle
payload (the KTD7 change - the resolver attaches pid and generation token at
every resolution return, and a dead provider's empty-successful reads become
STALE_REF rather than a fabricated absence), then read through
properties::read_live plus the walk's own read_label/walk_vocabulary/
into_locator_evidence composition. Essential evidence (role, value, states,
bounds, actions) must all be non-Unknown or the read fails retryable
AppUnresponsive, never a partial bundle claiming completeness. get_live_value,
get_live_state, get_live_actions, get_live_element and get_element_bounds are
thin projections; enabled and offscreen read UIA's own flags, secure values are
withheld at the reader path, and FFI ad_get('bounds') stops hard-failing once
get_element_bounds answers. Live fixture tests pin each reader, the secure
withholding, and both directions of the dead-token rule; pure tests pin the
completeness gate.

* feat: resolve the locator anchor on windows for live find hydration

Selected-match hydration (find, wait --selector, materialized queries) calls
resolve_locator_anchor; it was not_supported and every hydrated find errored.
The anchor is the path-only variant of resolve_element_strict with the inverse
classification: the path is exact from the just-walked tree so there is no
broad-search fallback, and every step is descent along a stored path that
churn makes permanently wrong, so a step that lands nowhere, a role-refuted
candidate, or a vanished node settles the attempt immediately - a completed-
search STALE_REF with one attempt and the deadline intact for core's single
fresh re-observation. Only the unresponsive/transport class runs through the
bounded retry loop, which re-walks the same cheap O(depth) path. The adapter
wires the method and the settle-vs-retry split, and the live fixture tests pin
an exact anchor path resolving, a wrong child index settling stale without
resolving a neighbour, and a role-refuted landing refusing.

* docs: dogfood the windows live resolution loop against real applications

Drive the release binary's live loop against classic Notepad on a scratch
file, Explorer on a scratch directory, the WinForms and WPF fixtures and
Obsidian (Chromium/Electron), reading the JSON: snapshot, find round-trips, and
get/is live reads all answer. The WinForms fixture's stored ref re-resolves
correctly after a real WM_APP content swap (the A7-3 wrong-target defence holds
live), and Obsidian on this box presents the first-contact shell - 4 of 6
sampled shell refs go STALE_REF on a fresh client, 2 resolve - which is the
honest, judged Electron rate, while the identifier-free fallback's web-content
rate stays owned by the 2.12 shell per A17-8. Report committed with the
environment header, per-target matrix, ambiguity corroboration against U1's
census and residuals with owners.

* docs: correct phases.md and concepts for what the windows resolution work proved

Corrections in place, never annotated: the 2.5 scope's resolve_query bullet is
rewritten to the measured truth - core owns the LocatorQuery evaluator and the
adapter ships resolve_locator_anchor plus evidence completeness (KTD2); the
snapshot value slot's secure-field behaviour is split so the read side closes
here (content-free fingerprint evidence, IsPassword-gated candidate reads,
reader-path withholding) and the action side is owned by 2.6/2.7; 2.6's scope
line no longer re-budgets the enabled/offscreen ElementState fields 2.5 ships;
and the two new scope bullets record get_live_actions' lack of production
callers and that the in-web-content STALE_REF rate is measured on the 2.12
shell (A17-8). CONCEPTS.md gains Graded Resolution and Settled Absence, both
distinct from Evidence Tri-State. Shipped source comment references to plan
decision and unit ids (KTD, U, bare sub-phase numbers) are scrubbed to what is
true about the code, keeping only the ledger row citations the phase-reference
scan permits, and the FINDINGS hunk index is re-synced to the measured diff.

* style: rustfmt the resolution and live-read modules

* fix: close the code-review findings on the resolution and live-locator work

A nine-lens review with an independent plan trace and a simplification pass
found the branch red on the repo's own gates and carrying four correctness
defects. All are closed here with tests that fail when the fix is reverted.

The resolver's anchor collapsed a tri-state: a role read that failed was
indistinguishable from a role that differed, so a transient hiccup while
walking a stored path settled a permanent stale ref in the one resolver
built to survive churn. The retry loop never retried a provider's own
timeout, because that classifies Timeout while the predicate only accepted
AppUnresponsive. A window that was merely busy carried no retryable stamp,
so it read as settled and disabled every retry layer above it. And the
live read corroborated process liveness only before its property reads,
which on this build return empty success for a corpse - the reader now
re-corroborates after the reads and settles stale on a target that
vanished, instead of burning the deadline as unresponsive.

The gates themselves were the root cause of the rest. check-rust-file-size
hardcoded python3, which does not exist on a Windows developer box, so the
comment half of the gate could never run outside CI; twenty-seven comment
violations and five oversize files reached the branch behind it. The gate
now resolves its interpreter and both halves pass. The five files are split
by responsibility, the search context is bundled to fit the parameter cap,
and a UTF-8 round-trip that mangled three ledger rows is repaired.

Three scenarios the plan named but nothing exercised are now pinned: the
selector-wait poll, count-versus-materialized agreement, and the FFI bounds
path. Tracing the first disproved the plan's own framing - the selector
wait never reaches the locator anchor - and the dogfood report records that
correction alongside the dedup deliberately left unapplied.

* fix: bind a stored window ref to the process that owns its handle

A ref stored only pid + generation token and never asked whether the HWND
it carried still belonged to that process, so a recycled handle let
resolution walk another application's tree while the verified-handle stamp
re-asserted the same stored pair it had assumed. verify_stored now checks
live handle ownership through GetWindowThreadProcessId before the token,
resolve_window_root routes through that evidence instead of its own weaker
inline gate, and the window inventory propagates the verification it used
to discard. The locator anchor inherits the gate at its existing call site,
pinned by an inverted run rather than read off the call graph.

Also closes the remaining review findings on this work: a deadline expiry
mid-read now records Unknown instead of dropping unread properties, which
had been leaving the secure-field gate open; an unreadable role in the
anchor no longer collapses into a confident mismatch; and a Timeout is
retryable only when it is explicitly stamped so.

* docs: state what window identity proves and what it still cannot

The decision record claimed a recycled handle on a different process is
caught by the window gate and one reused by the same still-running process
is caught a level down by exact-evidence element resolution. The first is
true only now that ownership is verified; the second is not true at all,
because two instances of the same dialog present identical evidence, so
the sole-candidate arm resolves one of them with no geometric
corroboration. Both are corrected in place, and the residual is written
into the sub-phase that owns it rather than left in a report.

The dogfood harness is corrected the same way: its identity judgement read
role off the stored entry, which cannot distinguish a correct resolve from
a silently-resolved neighbour, and it is scoped to what the fixture can
actually show given that its ListBox exposes no items to a COM client.

* refactor: walk a resolver descent once and honour the deadline inside it

The anchor and the strict search each carried their own sibling loop and
their own path walk, so neither consulted the deadline between cross-process
calls: up to the sibling cap could run before the budget was next checked,
and one wide step could eat a whole hydration budget. The main tree walk had
had the check in the right place all along.

Both loops and both path walks collapse into one descent parameterised by
the caller's failure classifier and its expiry policy, which genuinely
differ: the search marks its list unfinished and retries, while the anchor
surfaces a timeout, because a settled miss there would tell core an element
is gone when the truth is that time ran out. Each classifier arm is now
pinned through both consumers, as is the axis wording the two resolvers
report, and the anchor draws its budget from the same helper as the search
so the bound cannot drift below the walk that issued the ref.

The sibling cap keeps reporting a truncated list as whole, now deliberately
and documented: the widest node measured anywhere is 27 children against a
bound of 10,000, the walk that issues refs enumerates under the same cap and
refuses an incomplete tree, so no ref can name an element past it, and the
unfinished flag is subtree-wide rather than per-node - marking cap-hits
unfinished would turn every correct settled miss elsewhere in the window
into a retry loop.

* test: pin the resolution gates that passed while broken

Nine gates across the resolver, the shared live read and the HRESULT
classifier had no test that failed when they were weakened. The classifier's
drift test was worse than absent: it compared the projection to the record it
is defined as, so it could not fail - proven by running a byte-identical
replica against three deliberately broken builds, including one with a
denial-class code flipped to retryable, where it passed every time.

Now pinned, each verified by breaking the exact line it guards and watching
the test fail: the live read's completeness gate, whose deletion previously
left the whole suite green; the AMBIGUOUS_TARGET branch, including that its
error stays shape-only when duplicate evidence is what produced it; the path
fast-path's fall-through on a refuted landing, which must reach the broad
search rather than settle; and every arm of the classifier, whose disposition
and retry stamp are asserted separately because Terminal and SettledAbsence
share a stamp and a flip between them is invisible to it.

The live read also stops issuing the label's cross-process calls once the
deadline is spent, which the bounded read above it already respected. The
label read is injected so a test can pin that no call is made at all; a label
that could not be read reports as failed rather than absent, since the
absence claim is one nothing measured.

Two classifier codes stay terminal with their reasoning recorded rather than
their names trusted: an enumeration's end of list cannot reach the success
catch-all, because a UI Automation error only carries an HRESULT when it is
negative; and the no-clickable-point code describes obscured or unscrolled
view state, which is what scroll-into-view exists to remedy, so settling it
would drop candidates mid-descent as a complete empty child set.

* fix: fail a probe run that did not measure anything

Four probe harnesses reported success while measuring nothing. A pass whose
binary exited non-zero wrote a placeholder capture, the run reported ok and
exited 0, and the placeholder satisfied the workflow's artifact gate - so the
guard failed by going green, which is indistinguishable from working. A pass
that never ran at all was worse: no capture, no complaint, same green.

Only a noisy failure was caught, and by accident: a binary writing to stderr
trips the terminating-error preference. A silent non-zero exit - a missing
DLL, an access violation, an explicit exit code - went straight through.

Placeholders now say they are not measurements, each harness declares the
captures a CI run must contain, and a CI-labelled run fails naming what is
missing or degraded. A dev box still records an honest placeholder and stays
green, and a genuinely negative measurement still passes everywhere, which is
the distinction the gate has to keep: recording that nothing was found is
data, recording that nothing was looked at is not. The gate lives in the
shared module rather than four times over.

The captures now survive a red run, since a failing probe's placeholders name
what did not measure and are the whole evidence trail. The artifact step no
longer errors on finding nothing, because a probe that dies before writing
would otherwise stack that on top of the real failure and read as its cause -
what asserts a run measured something is the per-probe gate, which names the
capture instead of firing only when every one in the job is absent.

* docs: give two cross-platform duplications a receiving sub-phase

Both had been carried from plan to plan since 2.3 with no owner in the
product's own scope document, which is where the next sub-phase's planner
reads and where this repo requires a deferral to land.

The resolver error payload is mirrored per adapter rather than shared: the
same details object core reads back to derive retryability, restated on both
sides. A key renamed or a value changed on one turns a retryable incomplete
into an unretried failure on one OS only, and both crates' tests stay green
because each asserts against its own constructor.

The evidence-completeness rule has the opposite alarm and is worse for it.
macOS restates core's rule clause for clause, and core's copy has no
production caller anywhere - only its own unit test - so the rule core
presents as canonical is inert. Tighten it and core stays green while the
macOS walk keeps grading by the old rule.

Both land in the hardening gate, for reasons that differ: the first needs a
core touch this sub-phase had already spent and changes the GA adapter; the
second needs no core change at all, but is a behavioural edit to the macOS
walk and so needs the macOS review, e2e and perf baseline that only that gate
runs. The second also carries a prohibition, since Windows consumes the same
requirements type and would find local reimplementation the demonstrated
pattern.

* fix: fail a probe that declares nothing to measure

The measurement gate is only as strong as its declaration. A probe that never
declared its mandatory captures - a leg added later, a refactor that dropped
the call - had an empty expectation set, so the gap check found nothing and
the run passed having asserted nothing. That is the defect the gate exists to
catch, one level up.

The check sits inside the gap computation rather than beside it, so it needs
nothing from the caller: every probe already asks that one function whether
the run measured what it promised, and a probe still carrying the old
hand-rolled tail fails on an empty declaration too. The four hand-copied
tails collapse into one shared assertion, so the reason reported can no
longer disagree with the reason that fired, and an unknown label fails the
parameter bind instead of silently skipping the gate.

Emptiness now reads as not-measured wherever it appears: an empty list, an
empty object and a blank string assert exactly as much as the absent result
that already counted as unmeasured. Values that are merely negative stay
measured - a bare list, a plain string, a zero and a false are answers, and
false is the measured negative this gate exists to protect.

* docs: let the integration gate decide rather than also implement

The hardening gate had accumulated a measurement it had to stage itself, a
core schema change, a macOS refactor bound to no platform phase, and the
release merge of the whole branch. Wanting full-branch review and having to
ship inside the release-merge commit are different requirements, and treating
them as one is what collected the rest.

The window-handle wrap rate moves to the sub-phase that builds the fixture
and registers the first interactive runner, which is the first rig that can
stage the churn to observe it. The schema addition that consumes that rate
becomes its own sub-phase, inserted after the one it depends on rather than
renumbering successors - reusing a number would leave existing references
pointing silently at different work, the way phases 1.5 and 1.6 were inserted
for the same reason. The field is required to be optional by construction,
since stored refs are serialised and a mandatory addition would turn a
corroboration fix into a fresh stale-ref failure.

The completeness-rule collapse is not a task for any platform phase but a
standing rule: a rule core owns and no production path calls is inert, and
tightening it changes nothing while the adapter that reimplemented it keeps
grading by the old one. It has a closed precedent in the accessible-name
work and an open instance now, so it moves to the cross-phase requirements
where the next platform's planner reads it.

What is left is decisions, one promotion that genuinely needs both adapters
reviewed together, verification and the merge. The two contract decisions
state the same boundary explicitly: the gate records what was decided, and a
decision to normalise ships as its own PR beforehand, because it changes
behaviour on an adapter that is already released.

* fix: carry a walk's unread regions into the resolution verdict

The path fast-path recorded whether its walk finished, and nothing read it:
the broad search declared a fresh flag beside itself, so only its own gaps
reached the verdict. A transport fault or a vanished node met while walking
the stored path therefore left no trace, and the attempt settled STALE_REF -
the element is gone, take a new snapshot - off a walk that never finished.

That path is the only tier that descends past the search's depth cap, so for
a deeply stored element it is the sole tier that can reach it at all, and its
gap is the only evidence anything went unread. One flag now spans the whole
attempt, declared before the fast path so a second one cannot be introduced
beside it.

A missing child index still settles on the first attempt, because landing
short of a stored index is a real answer rather than a failed read. What now
retries is a faulted or starved enumeration, which is bounded by the same
deadline and ends as an unresponsive app rather than a false certainty.

* fix: tell an agent what the error actually was

Three agent-facing errors said something other than what happened.

A stale ref reported "not found in current RefMap" for a ref that resolved
fine and whose live evidence simply did not match - the core constructor
takes a ref id and formats that clause around it, and both Windows sites
passed it a whole sentence, so the shipped text was also two sentences run
together. They now build the error directly, mirroring the macOS shape while
keeping the not-delivered disposition macOS omits, and both go through one
constructor rather than restating it.

A missing window root carried no disposition at all. An unstamped error reads
as retry-permitting, so a caller polling a window that was already destroyed
would spend its whole budget waiting for it. It is now stamped from the
classifier's own table at the granularity that applies: a window root that is
gone never answers the same handle again, which is settled, not merely
unavailable the way an element mid-descent is.

An ambiguous target carried no stamp either. It is complete, because the only
route to that verdict is a search that either finished or stopped where no
further candidate could disambiguate; and it is settled, because replaying
the same walk against the same stored evidence cannot answer differently.

A property read failure was the one case where the reported defect was not
real: wrapping it left the typed retryability untouched, since that is only
re-derived when the new details carry the key. The wrap now merges rather than
replaces so the classifier's pair also survives into what a caller and a trace
segment read, and the two failure families keep the separate answers they had.

* fix: catch the plan references the reference gate could not see

The gate matched decision and unit ids and phase numbers, so shipped source
citing the delivery plan in prose passed it - a doc comment saying the plan
pre-committed to a split, or naming a Verification Contract row, tells a
reader nothing they can act on and stops being true when the roadmap moves.
It now also catches the plan quoted as an authority: the possessive, the plan
paired with a prescriptive verb, a preposition leading into it, the plan
document by name, the spaced prose form of the delivery report, and plan
section vocabulary. Ordinary English is left alone - a path is a location a
reader can open rather than an authority, and a coordinate origin is not a
requirements document.

Four comments it found are rewritten to say what is true of the code: why a
prefetch split is conditional, why an ambiguous error carries shape only, why
a role and state divergence is deliberate and what it costs a caller, and why
a vocabulary walk repeats three times.

Both gates gain a self-test that runs on every invocation, sharing the rule
under test with the fixture so the fixture cannot drift from what ships.
Building them surfaced four holes: a file was measured with a newline count,
so a file whose last line was unterminated read one short and a file one over
the cap slipped through; a capitalised sub-phase escaped both the sub-phase
rule and the phase-number rule, which needs a digit; the generated-file
exemption hung on which side of a pipeline finished first; and the interpreter
probe accepted a version whose syntax error would have read as a violation.

* fix: stop a probe panicking away its own evidence

A probe that panicked wrote no capture and left its hosted fixture process
running, so the measurement gate reported a missing capture rather than the
error that caused it, and the corpus's no-surviving-process rule broke with
it. Six panicking calls are replaced by propagation or a recorded failure; a
walk that could not run now reports that it did not measure instead of taking
the process down. Driven both ways against a real provider fault.

A survival capture recorded element names verbatim. The gate every capture
round-trips checks operator identity - user, machine, profile paths, SIDs -
and has no rule for a content name, and the helper that reduces one is a
call-site reducer a Rust probe cannot reach, so nothing downstream noticed.
The names are recorded as a digest and a length, which still pairs an element
across a relaunch, and the contract is written down where the next Rust probe
will look: reduce a content name where it is recorded.

The A17 timing rows claimed more than one environment supports. The CI leg is
wired but no capture from it was ever committed, so every number rests on the
dev box alone - the third time this corpus has taken a single environment for
a settled fact. The rows now say so, separate the ordering they do establish
from the milliseconds they do not, and name what closes them.

* docs: give the mutation classifier and a mangled error an owner

The mutation-path delivery classifier was deferred to a pair of sub-phases
without either one taking it, so no scope said what to build. It lands in the
sub-phase that first invokes a UIA pattern and already owns the typed delivery
report the classifier feeds, with the outcomes and their codes named and the
rule that matters stated: the shipped read classifier calls transport failures
retryable, which for a write is how one click becomes two.

The stale-ref constructor takes a ref id and formats "not found in current
RefMap" around it. Fourteen call sites across core and the macOS adapter pass
it a sentence instead, so the message reads as two sentences run together and
blames a lookup that never happened. Windows is corrected here; the rest are
recorded against the gate that can change core and both adapters at once,
where a promotion of the same error family already opens two of the files.

The window-handle work in the observation plan still pointed at the merge
gate after the measurement and the schema change were split apart.

* fix: self-test the file-size gate without deleting anything

The gate's self-test built its fixtures on disk and removed them afterwards,
which tripped the harness contract that forbids a shell script under scripts
from deleting artifacts at all. The ban is right and the fixtures did not need
a filesystem: the cap rule reads its argument in one forward pass and never
seeks, so each fixture is now a stream. Nothing is created, so nothing has to
be cleaned up.

The interpreter probe's fixture no longer empties the search path, which was
never quite the thing under test and is awkward where a resolvable name can
carry an extension. The candidate list is injected instead, so the real
resolution loop and the real failure branch both run, and a second rejection
now has a fixture: a command that resolves but answers the version probe with
a failure, which is what an old interpreter does and what the emptied path
could never reach.

The rule stays the single owner of the cap, called by both the tree scan and
the fixture, and the fixture stays wired into every invocation. Each of the
six ways to break it was driven and observed: the cap that never fires, the
cap that rejects everything, the count that measures newlines instead of
records, the exemption dropped, and both interpreter rejections.

* docs: refresh the learnings against what the code now does

An audit of all twenty-one learnings found the advice sound everywhere - no
recommendation had to be rewritten - but nine had drifted in what they point
at, and two were wrong in ways that would mislead a reader following them.

The envelope contract's trigger list covered a success becoming an error and
not the inverse, which is what the last bump actually was: a snapshot that
exhausts its budget now succeeds and reports incompleteness in a field rather
than failing. A reader applying the doc literally would have concluded no bump
was needed. It gains that trigger and the list of surfaces a bump has to reach,
two of which were stale and are corrected here.

The gate doc justified its portability advice by saying the gates run on
several systems in CI; they run on one, and the machine most likely to author
them never executes them, which is why a pattern that worked only under one
toolchain survived review. It gains the dimension all of this week's evidence
kept pointing at: test a check's ability to run at all, because one that finds
nothing and reports success is indistinguishable from a clean tree.

The rest is currency. The private-file section is written in the present tense
about hardening that has since shipped and satisfied it, including the
technique that made one constraint tractable - prove an ambiguous sentinel by
first making a control call that must succeed. Dogfooding against real
applications is described as the standing practice it became rather than a
recurrence. The allocator doc names the one sanctioned second recursion and
draws the boundary around it. The reliability contract's verification layer is
stated per-platform now that a second platform has one.

The pointer-actions documentation gap is closed, so its doc goes; the one
observation not mirrored anywhere else - that the actionability battery probes
several points while the pointer path checks only the one it resolved - moves
to the doc that owns pointer policy.

CONCEPTS.md gains the read-outcome taxonomy, whose only defined member had
been leaning on three siblings that did not exist, and the evidence vocabulary
that separates measuring nothing from finding nothing. Entries carrying file
paths, type names, or a roadmap owner are rewritten to stand on their own.

* docs: show the snapshot envelope the shape it actually has

Four places documented the snapshot response with a data object that predates
the current envelope: they carry the right version number beside a shape from
before it. Since that version, every snapshot reports whether it finished, and
a tree too large to walk succeeds and says so in a field rather than failing
with a timeout - so an integrator following any of these examples would branch
on an error that no longer arrives.

The C header is regenerated from its source rather than edited, and the two
examples that were also missing the snapshot id gained it: a shape known to be
wrong in one field is not worth publishing to be right in another.

Left alone deliberately: the error envelopes, which the version did not
change, and the entries that assert nothing about the data object. The
traversal timeout row stays true, because a drill-down from a stored ref still
returns one - it observes a subtree whole or not at all, so it never reports a
partial result the way a full snapshot now does.

* test: measure the adapter's own withholding, and the gate that guards it

The secure-control probe read the provider directly, and the provider blanks
those slots itself - so the arm passed for the platform's reason and would
have kept passing with the product's withholding deleted. It now takes two
readings of the same control: the provider's, whose recorded numbers are
unchanged, and the adapter's own composition. The pass is gated on the
provider having published something first, because an adapter that published
nothing is otherwise indistinguishable from a provider that offered nothing.
One target publishes no readable slot at all, and that pass now says so
rather than counting itself as evidence.

The reader test gains the same treatment. A marker pin alone would not have
caught this, because the marker never reaches the slots the provider blanks;
what catches it is comparing what the platform offered against what the
reader passed on.

The measurement gate - the check that fails a run which measured nothing -
had no self-test of its own, so a regression returning "no gap" would have
let every run report success after asserting nothing. It now carries one that
runs whenever the shared module is loaded, drives the shipped functions
rather than a copy of their rules, and covers both directions: a placeholder,
a pass that never ran, an empty declaration, and the negative result that
must still pass. Building it exposed two holes in the fixture itself, one
silent - the decision was extracted so a test can reach it without ending the
run, and the alarm is now read a second time independently, because an alarm
routed through the circuit it monitors is not an alarm.

The geometry promotion arm had no test that failed when it was deleted. The
per-node decision is extracted so both halves are pinned: an unsettleable
identity promotes when a positive-area rectangle agrees, and a zero-extent
one never promotes however exactly its hash matches.

* test: make the checks fail when what they guard is broken

Five review rounds each surfaced one more assertion that would survive its
own subject being deleted, so this sweeps the class instead of the instance.
Every test in the Windows crate was read and every claim below was proven by
breaking the exact line it guards and watching what happened.

Twenty assertions survived a mutation and now catch it: a sentinel whose
disposition could be flipped, six state producers that could each be deleted,
a property that could take another's name, two retry arms, a modal check whose
only test ran the non-Windows stub, an evidence projection whose two sources
were seeded identically and so were indistinguishable, two private-file gates,
a promotion failure tested as a formatter but never as a wired path, an action
vocabulary asserted against a superset containing names the producer cannot
emit, and three redaction rules.

Two lines guarding real defects had no test at all. Deleting the resolver's
verified-process stamp disarmed the corpse check for every resolved handle,
and turning the bounds-hash tie-break into first-match-wins silently resolved
the wrong element among hash-matched candidates - the shape measured on a real
file list. Both are pinned against fixtures where the right answer is not the
first one.

One production defect: the tree dump redacted only the first user path in a
value, and a provider chain carries one per provider, so a second path was
written out verbatim. Every occurrence is consumed now.

Eleven further lines are proven unguarded and named in the report rather than
quietly left; two pins cannot be inverted because the property they assert
comes from the live system, and they say so.

* fix: stop the gates and probes reporting what they never measured

The pre-commit hook had stopped running any gate. It selected staged files
with a pipeline whose reader quits at its first match, so the writer died of
a closed pipe and, with pipefail set, finding a match became a non-zero
status - a match read as no match. Once the staged path list outgrew the pipe
buffer the hook announced there was nothing to check and exited clean. It
reads the list once now, and both directions are driven against a list far
larger than any buffer, because this failure announced itself as success.

The npm release gate searched the whole workflow for a publishing permission
that another job also holds, so deleting the grant from the job it guards left
it passing. The bash-3 gate rejected two spellings of an associative array and
silently accepted three other constructs from later shells. The comment
scanner's own tests were named so the lane that discovers tests could never
match them, so they had never run. Each now carries a fixture that shares the
shipped rule and runs on every invocation, in both directions.

Among the probes: a comparison of two joined lists always evaluated to a
truthy string, so every path-survival and bounds-equality field reported
agreement for values that differed. An anti-vacuity guard counted a null as
one element and so never fired on the one input it exists to reject. A
registry lookup used a drive that does not exist, making a published
registration answer unconditionally false. A list-swap classification was
published from runs where the mutation never landed, and a leak check planted
its secret without reading it back, so deleting the plant changed nothing it
reported. A private-file probe published two dozen rows about renaming over an
open handle without ever opening one.

The redaction gate reached three quarters of the committed captures; it now
reaches all of them. A document title that lands on a container escapes the
content rule, because a container takes its name from its contents and the
window title only tracks the active pane - one such title was published and is
withdrawn. Chrome labels stay verbatim: the rule reduces only a name echoing
something already reduced beneath it, inside a target declared to carry user
content, which the unscoped form was measured to get wrong.

Three ledger rows are corrected to what re-measurement shows.

* fix: let the pre-commit hook run on the platform this phase targets

The hook drove clippy and the library tests across the whole workspace, which
resolves through default members and so pulls in the macOS crate. That crate
cannot build anywhere else, so the hook failed outright on every Windows and
Linux machine and the documented way to commit was to skip it. Skipping it is
how a formatting break and a batch of comment-rule violations reached CI this
week: the gates that would have caught both run in the hook.

Both invocations now take the package set the host can actually build, taken
from the CI lanes rather than invented - Windows and Linux mirror their own
lanes, and macOS keeps everything it already covered so the released platform
loses nothing. A guard refuses the commit if the macOS crate ever enters a
non-macOS set, since that is the failure being fixed. Format stays workspace
wide because it builds nothing.

Everything else is untouched, including the staged-file matching whose own
fixture still fails in both directions when sabotaged.

* docs: correct what actually gates a chromium content tree

Two scope entries recorded that a web target's tree only reaches a client
that settles long enough, and that the dev box could therefore only ever see
a first-contact shell - so the hardening sub-phase was told a longer-lived
client on its own runner was the lever. Measurement disagrees.

On this dev box a restored Chromium target answers the product's own path
immediately with a complete tree and allocated refs, while the same process
minimised answers with a timeout and shows a raw walk eighteen nodes. Holding
one client against the minimised window for ninety seconds adds nothing, and
neither does the accessibility handshake the window answers: the tree is
wholly there on the first read once the window is restored, and wholly absent
while it is not. Window state decides it, not elapsed time.

The rate inside web content is still unmeasured and still owned by the
hardening sub-phase. What changes is that it starts from a true premise, and
its first obligation is to control the target's window state rather than to
attribute a thin tree to the machine.

* refactor: split the two facts one resolution flag was carrying

A single flag told the resolver both that an attempt had left some region of
the window unread and that a particular landing was uncertain. Those are not
the same claim, and for a landing at a stored index they are provably
unrelated: the child list is only ever appended to, so a truncation drops a
suffix, and a landing that resolved an index at every level had every read it
needed. Whatever went unread lies past it.

Carrying both facts in one flag meant each reader saw the one they cared
about. One reading found the flag discarded and concluded a missing element
could be declared gone off a walk that never finished. Another found the same
flag reaching a successful landing and concluded the landing was unsound.
Both were reading a value that could not answer either question on its own.

The path walk now returns its own landing, carrying whether the walk left a
region unread as a property of that walk rather than as an argument threaded
into the tier below. Accepting a landing is a separate function that has no
completeness parameter at all, so the tier cannot be made to second-guess a
positional answer with a fact about elsewhere. The verdict takes the landing
itself, so the walk's gap cannot be dropped on the way without writing a
claim that no walk ran, next to the walk that ran.

The property this protects was never actually pinned: reintroducing the
discarded-gap defect left the suite passing. Both directions are pinned now,
along with the prefix lemma the positional answer rests on, and the tier is
load-bearing rather than redundant - a control with no settleable text
identity resolves through it and cannot be resolved without it.
2026-08-06 16:14:04 -07:00
..
brainstorms chore: track planning artifacts with secret and privacy scanning (#109) 2026-07-26 01:41:55 -07:00
dogfood-reports feat: windows resolution and live locator (sub-phase 2.5) (#120) 2026-08-06 16:14:04 -07:00
plans feat: windows resolution and live locator (sub-phase 2.5) (#120) 2026-08-06 16:14:04 -07:00
solutions feat: windows resolution and live locator (sub-phase 2.5) (#120) 2026-08-06 16:14:04 -07:00
architecture.excalidraw docs: add architecture diagram to README 2026-02-24 06:22:52 -08:00
architecture.html refactor!: unify command execution contracts 2026-05-19 18:27:08 -07:00
architecture.png refactor!: unify command execution contracts 2026-05-19 18:27:08 -07:00
example.html refactor!: unify command execution contracts 2026-05-19 18:27:08 -07:00
example.png refactor!: unify command execution contracts 2026-05-19 18:27:08 -07:00
faq.md feat!: implement Playwright-grade foundation contract 2026-07-20 00:21:38 -07:00
json-output.md feat: windows resolution and live locator (sub-phase 2.5) (#120) 2026-08-06 16:14:04 -07:00
phases.md feat: windows resolution and live locator (sub-phase 2.5) (#120) 2026-08-06 16:14:04 -07:00