# Run 20260913T071131Z — capture failure and deadline overrun

hertz, 2026-09-13. Read-only, from saved artifacts and frozen source. Nothing was rerun, no
rig file edited.

Subjects, each hash re-measured by me now, not carried forward:

- driver `fp-driver-d2.sh` = `e093e80dcf41247e0a855408661092602709131bdf34b44c072b41d81ac6575b` (the frozen, admitted driver)
- instrument `d2_capture.ps1` = `a0e7afbee4dce9f310ade9704c199b3b759715c06da7e033d286beb7cfda8c06` (matches the grant pin)
- validator `d2_validate.py` = `fe95072279ac89a4928fcfc147565df0f628855a89b298fbbd044dc36bf8178d`

Evidence archive: `archive-20260913T071131Z/`, manifest sha
`25d3398506e1c949a85061fc2b7cdd50f0eb33fe269a3065f95b9e30c412527f`, 152 of 152 verified.

Each finding says which it is: ESTABLISHED (the artifacts decide it) or HYPOTHESIS.

---

## F1 — ESTABLISHED. All three captures read INCOMPLETE because `matches` is serialized as an object, not an array. That measured the instrument, not the host.

`Get-ObjectProps` (d2_capture.ps1:222-239) ends `return @($out.ToArray())`. PowerShell unrolls
a returned collection into the pipeline, so a ONE-element result assigned at the call sites —
`$matches = Get-ObjectProps ...` at :274 (rules) and :335 (filters) — binds the ITEM, not a
one-element array. `ConvertTo-Json -Depth 12` (:545) then faithfully writes
`"matches": { ... }`. The `@()` inside the function does not survive the return.

d2_validate.py:126-128 requires `isinstance(matches, list) and len(matches) > 0`; otherwise it
records `MISSING_OBSERVATION <label>/matches` **and returns**, so no property state is ever
reached. That is why every document reports `records_present=43 missing=46` and
`D2VALUES present=0 empty_array=0 empty_string=0 null=0 absent=0 denied=0 error=0 no_rule=0` —
all zero. The `no_rule` counter at :129-130 is unreachable for the same reason: the
empty-matches return fires first.

MEASURED, both engines, with controls. This is a probe of an unmodified subject (the shell); it
references nothing in the rig, so it needs no baseline arm.

| arm | return type | JSON emitted |
|---|---|---|
| one element (the defect) | `System.Collections.Hashtable` | `{"matches":{"i":0}}` |
| two elements (positive control: arrays DO survive) | `System.Object[]` | `{"matches":[{"i":0},{"i":1}]}` |
| `@(f())` at the call site (fix control) | `System.Object[]` | `{"matches":[{"i":0}]}` |

pwsh 7.6.6 Core and Windows PowerShell 5.1.26100.8875 gave identical results in all three arms.

POPULATION, enumerated with my own predicate rather than sampled: 43 records per document
(8 rules + 35 filters) across 3 documents = 129. Objects: 129. Arrays: 0.

This was never an edge case. d2_capture.ps1:223 — `if ($list.Count -eq 0) { $list = @($null) }` —
makes an ABSENT rule yield exactly one synthetic NO_RULE record, and a rule matched by name
yields exactly one too. Single-element is the normal case.

Why my own controls did not catch it: :461 already re-wraps defensively
(`foreach ($m in @($r.matches))`) when the script re-reads its own records in-process, so
`Test-NumbersAreNumbers` and the D2CONTROLS block both read through that re-wrap and saw an
array — which is why `D2CONTROLS ... sound=true` sits in the same file as 46 missing
observations. The defect exists only on the JSON side of the boundary, and every control I
wrote stayed on the PowerShell side of it. The serializer receipts exercised the serializer,
not the function-return seam.

CONSEQUENCE FOR THE RUN'S VERDICTS. The three validator outputs are byte-identical apart from
the `point=` label (diffed: 2 lines differ, both the label). A verdict that cannot vary with
host state is not a measurement of host state. **The run would have read INCOMPLETE even if the
owned pair had been created.** The INCOMPLETE verdicts are uninformative about the pair, in
both directions — they neither support nor refute its presence.

CORRECTION (narrow): `$matches = @(Get-ObjectProps ...)` at d2_capture.ps1:274 and :335. Two
sites. It must be re-measured against a written document, never against an in-process re-read,
or the same control blindness recurs.

---

## F2 — ESTABLISHED. The `host` block is a second, independent schema mismatch.

d2_validate.py:223-229 reads `host['outcome'][k]` for k in interfaces / profiles / addresses.
d2_capture.ps1 emits `host` with a FLAT `query_outcome` (plus `query_interval`, `note`) and no
`outcome` map at all. So all three host observations report `MISSING_OBSERVATION host/outcome.<k>`
regardless of what the host read returned — 3 of the 46 misses, and they survive the F1 fix.

CORRECTION: emit `outcome = @{ interfaces=...; profiles=...; addresses=... }`. Per-key, not one
collapsed value: the capture already performs three separately-outcomed reads, and the
document's own ceiling text claims that distinction is preserved. Teaching the validator to
accept the flat `query_outcome` instead would discard exactly that claim.

---

## F3 — ESTABLISHED. t1's exit 2 is mislabelled. The document WAS written, completely.

d2_capture.ps1:545-558 — one try-block does, in order: `WriteAllBytes(json)`,
`WriteAllBytes(b64)`, `Get-FileHash` on the output, print, `exit 0`. The catch prints
`D2_WRITE_FAILED` and exits 2.

The archived console (`archive-20260913T071131Z/executor/leg-003-console.txt`, line 359):

    D2_WRITE_FAILED .../d2-t1.json : The term 'Get-FileHash' is not recognized as the name
    of a cmdlet, function, script file, or operable program.

The failure is the RECEIPT HASH, taken after both writes had already succeeded.

Corroboration: `d2-t1.json` is 369903 bytes, parses, carries schema `spt-core/d2-capture/1` and
43 records, and validates identically to t2 and t3; `d2-t1.json.b64` is 493204 bytes; the driver
hashed the file afterwards as `6ef7fcfe...` in the run's own `MANIFEST.sha256`, and that
manifest verifies against the archived copy.

So the ledger's `d2_t1_exit=2 (UNAVAILABLE = the elevated capture never ran: a labelled evidence
gap)` is false. The capture ran, in the elevated leg, and produced a complete document. I wrote
that line. **I withdraw it here.** The preserved ledger stays unchanged as ruled; this is the
correction of record.

The defect is structural, not incidental: the IDENTICAL cmdlet at :477 (the instrument's
self-hash) IS guarded — try/catch, degrade to `instrument_sha256='UNREADABLE'`, continue — which
is precisely why t1 still carries a usable document. The one at :550 is not. An
instrument-availability problem is laundered into a written claim that no evidence exists.

CORRECTION: give :550 its own try/catch, degrade to `sha256=UNREADABLE` in the summary line,
keep `exit 0`. Reserve exit 2 for an actual `WriteAllBytes` failure — the one thing that exit
code is supposed to mean.

---

## F4 — NOT ESTABLISHED. Why `Get-FileHash` was missing in that elevated leg.

REFUTED from the artifact itself, all in the same process, same leg:

- PowerShell 2.0 — refuted: `ConvertTo-Json -Depth 12` ran (3.0+).
- ConstrainedLanguage mode — refuted: `New-Object System.Text.UTF8Encoding($false)` and
  `[System.IO.File]::WriteAllBytes` both executed.
- Microsoft.PowerShell.Utility unavailable, or PSModulePath lost through msys — refuted:
  ConvertTo-Json lives in that same module and resolved.
- The launch form — refuted by control: the same shape
  (`powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command ...`
  from msys bash, this box, today, unelevated) reports `GetFileHash=True`, PSVersion
  5.1.26100.8875.

REMAINING SHAPE consistent with all four: an engine at PowerShell 3.0, where ConvertTo-Json
exists and Get-FileHash (added in 4.0) does not. **I have not measured this** and hold no
authorization for a new elevated observation. It is a hypothesis and stays one.

THE ACTIONABLE DEFECT is not the cause — it is that the cause is unreachable. **The capture
document records no engine facts at all**: no `$PSVersionTable`, no `LanguageMode`, no
`PSModulePath`. A cmdlet-availability failure in a leg that cannot be re-observed is therefore
undiagnosable from saved evidence, which is exactly the position I am in.

CORRECTION: record those three in the document header. Read-only, costs nothing, and turns F4
into a measurement the next time it happens instead of an elimination exercise.

---

## F5 — ESTABLISHED. 310s of a 300s window: post-capture bookkeeping charged to the activity clock.

    07:15:57Z  GO — the 300s experimental window opens
    ~07:20:28  deadline_check t2-capture: "29s of 300s remain"   (last budget read of the run)
    07:20:33Z  d2_t2_START bound=24s                              (276s in)
    07:20:56Z  d2_t2_END rc=0                                     (299s in — inside budget)
    07:21:07Z  experimental_END "sequence complete"               (310s — 10s over)

The capture's own bound is correct: `exp_left` is re-read at the call site (29s at the check,
24s at the bound, 23s used). Nothing about the bound arithmetic drifted.

The overrun is the 11s AFTER the last activity step. fp-driver-d2.sh:2416 `d2_capture_at t2` →
**:2419 `d2_validate_at t2`** → :2433 `end_experimental "sequence complete"`. No `deadline_check`
between, and the window's end is stamped only once validation has finished.

This is a category error, not a slow step. `d2_validate_at` (:924-959) opens the local JSON and
runs python over it. It touches NO host state. The driver's own header (lines 60-64, 606-608)
defines the experimental budget as bounding ACTIVITY and the 180s clock as bounding REPORTING.
Validation is reporting.

THE WIDER VERSION OF THE SAME FINDING, which matters more than the 10s: `deadline_check` is
called at exactly TWO sites in this run — :2178 (elevated-setup) and :2413 (t2-capture) — and
the findings record contains exactly two BUDGET lines. Everything between GO+0s and GO+271s ran
unbudgeted: the setup handoff wait, t1 validate, portfields, the 1001-rule census (`setup-post`
at 07:19:39Z), identity capture, d1 render and exec. 271 of the 300 seconds were spent without a
single budget read. The 10s overrun is the visible tip of that, not the whole of it.

CORRECTION (smallest that is correct): move `end_experimental "sequence complete"` to
immediately after `d2_capture_at t2` returns, before :2419. That stamps the window closed at the
end of activity — which is what the driver says the window means — and hands validation to the
reporting deadline, where it already belongs.

NOT the correction: adding a `deadline_check` before validate. That would make a local file read
compete with host activity for the activity budget, and would truncate a report rather than a
measurement — the one outcome the budget design exists to prevent.

---

## Ownership

The setup leg's `LAN_BOOTSTRAP_REFUSED:unsigned-exe`, and the `DAEMON_AUTOSTART` line above it,
are product-path and are Todlando's per doyle's split. Nothing above depends on how that
resolves: F1, F2, F3 and F5 fire identically on a run where the owned pair IS created.
