Skip to content

feat: connector verification loop — evidence claims, run-and-watch, scenario record/replay - #140

Open
tnunamak wants to merge 797 commits into
mainfrom
feat/connector-verification
Open

feat: connector verification loop — evidence claims, run-and-watch, scenario record/replay#140
tnunamak wants to merge 797 commits into
mainfrom
feat/connector-verification

Conversation

@tnunamak

@tnunamak tnunamak commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Settled state — exact head f6a1eea43 (33 commits), substantive suites green head-exact (polyfill-connectors and reference-implementation via dispatched runs after a push-trigger drop; prior content-identical head 92f6ea8f4 was 11/11 green including the full suite). Based on codex/uat-health-candidate-0812 (#125 train); retargets to main when #125 merges.

What this delivers

The connector verification loop: evidence-claims vocabulary (docs/reference/connector-evidence-claims.md), run-and-watch (bin/connector-dev.ts, pdpp.run-summary/1), scenario record/strict-replay (pdpp.connector-scenario/1, bin/scenario-record.ts / bin/scenario-verify.ts), fixture provenance labels + enforcement, connector-init scaffolding, observe-schema observation reports, generate-validators spike, and workflow docs. Proven end-to-end on the unmodified oura and spotify connectors against synthetic in-test providers (synthetic-spike evidence class by the tools' own grounding rules — the first non_loopback_contact_observed capture requires a machine with real accounts).

Verification at head

  • Focused harness batch: 233 tests, 231 pass, 2 environment-gated skips (unprivileged user namespaces unavailable on the host — the isolation capability detector reports this honestly).
  • Both real-connector spikes green under full wire-strictness.
  • Package tsc, biome, conformance gates green; exact-head GitHub suite fully green.

Claim eligibility (all conditions independent, each with its own limitation string and negative test)

Canonical recorded_replay requires: registered connector identity · capture-time declaration digest · capture-time source digest · current declaration digest computed · current source digest computed · supported environment driver on every run · protocol trace on every run · OS-namespace descendant isolation · no unsupported evidence surface (ASSISTANCE) · driver evidence (recorded-http: ≥1 recorded interaction; consumption enforced by the matcher). Anything less: diagnostic_replay: PASS / recorded_replay: WITHHELD / named limitations, plus machine-readable claim: and scenario status: candidate oracle lines.

Wire and trace contract

Untrusted subprocess output is validated before normalization: unknown message types reject on record and verify sides; RECORD requires stream/key/object-data/emitted_at with the op directive validated, captured, and compared (delete↔upsert mutations fail); INTERACTION prompts compare field-exactly (kind/message/canonical schema/timeout; request_id volatile); RECORD ops are mandatory in the v1 format and STATE has symmetric wire validation. TRACE_POLICY dispositions all 13 runtime message kinds under a satisfies clause (compile-time exhaustive); tracked kinds get field-level dispositions (compared / digested / excluded-volatile) with continuation evidence via the runtime's own validator (parity-tested on 16 curated malformed cases). Evidence labels state observed facts only: non_loopback_contact_observed until provider-authority policy exists; author_live withheld by all tooling; secrets redact by default (--persist-otp opt-in).

Review history

Seven independent external review rounds plus two internal adversarial passes; every P0–P2 across all rounds repaired in-branch with per-finding disposition comments. Every strictness increase caught at least one real pre-existing bug (test-order dependence, silent trace field drops, JSON-path rooting mismatch, untracked fixture labels).

Honestly deferred (per the reviews' own disposition tables)

Provider-authority policy (labels already narrowed so nothing overclaims meanwhile) · browser HAR replay driver · gmail IMAP seedable fake · subprocess-output driver (slackdump/gmcli class) · overlap/dedup/idempotence scenario fixtures · built-package digest binding (publication pipeline) · response-body field-level secret handling (scenarios remain local-only).

Assisted-by: AI

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Limit the expanded subprocess budget to the two behavioral cases that actually launch more than 50 fake gmcli processes.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Fence the legacy SQLite writer during migration reconciliation, preserve explicit queue paths, make state filenames injective, and route maintenance commands through saved-profile queue resolution.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Add scrubbed metadata-only rows for every remaining manifest-declared Codex inventory stream, using production source-inventory paths, hashes, classifications, and reasons.\n\nAssisted-by: AI

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Compare fixture stream stems rather than suffix-bearing filenames so manifest inventory checks use the same ordering authority as declared stream names without changing connector emission order.\n\nAssisted-by: AI

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Treat the browser disconnected event as an observation for both diagnostic timestamps, and make the regression event-first without timer sleeps.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Preserve canonical writes while rebuilding summary evidence under the connector-instance fence.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Split expensive evidence construction from the short canonical receipt compare-and-publish fence. Remove terminal trigger lock cycles, disposable bootstrap projection writes, and fleet-global terminal folds.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Allow a current-revision repair failure to publish its durable failure receipt, and stabilize the PostgreSQL cooperative-deadline restart oracle without changing production deadline semantics.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Base automatically changed from codex/uat-health-candidate-0812 to local/uat-final-0811 August 14, 2026 17:49
…idation

Eighth-review closure plus pre-submission audit findings. ops is now
required on every stream expectation - validateScenario rejects
missing, misaligned, or invalid entries with named reasons, and
verifyStreamOps always compares; the legacy tier is deleted rather
than gated (the format is unmerged and scenarios are local-only, so no
real legacy corpus exists - one fewer state to maintain). STATE gets
the wire validator symmetric with RECORD/INTERACTION: nonempty stream
(grounded in every real emission site; the wire type alone does not
annotate it), cursor property presence via Object.hasOwn preserving
deliberate value opacity, malformed STATE fails recording and replay
instead of disappearing. Internal closure-matrix audit before
submission found one stale backward-compat comment (fixed) and one
trust-by-convention soft spot: verify's emit now enforces STATE stream
validity itself, so a future RunCollector bypassing the shared
projection cannot silently reopen the gap.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
@tnunamak

Copy link
Copy Markdown
Contributor Author

Closure packet — final delta (head f6a1eea43 = a124a79a1 + an empty CI-retrigger commit; 33 commits).

Round-8 disposition: P1 (legacy ops-less scenarios could earn the canonical claim) → closed via the review's blessed simpler option: ops is mandatory in the v1 formatvalidateScenario rejects missing (missing_ops), misaligned (ops_length_mismatch), and invalid (invalid_op_literal) entries with named reasons; verifyStreamOps always compares; the legacy tier is deleted, not gated (unmerged format, local-only scenarios, no real legacy corpus). P2 (STATE lacked wire validation) → closed symmetric with RECORD/INTERACTION: assertValidStateMessage requires a nonempty stream (grounded in every real emission site, stated as grounded rather than invented — the wire type alone doesn't annotate it) and cursor property presence via Object.hasOwn (value opacity preserved); malformed STATE fails recording and replay on both paths through the single shared projection.

Pre-submission internal audit (the reviewer's own closure matrix applied to the delta before this push): one P2 found and fixed (a stale comment still describing ops as backward-compatible-optional), one P3 hardened (verify's emit now enforces STATE stream validity itself, so a future RunCollector bypassing the shared projection cannot silently reopen the gap). All other surfaces — vacuous-truth/empty-set on the new rules, writer-path completeness, STATE symmetry incl. state threading, claim/doc consistency, validator bypass routes, negative-control coverage — clean bill, with evidence per surface.

Verification: 233-test focused batch (231 pass, 2 environment-gated skips), both real-connector spikes green under mandatory ops. Exact-head workflow runs (manually dispatched after a push-trigger drop; results are head-exact): polyfill-connectors https://github.com/PDP-Connect/pdpp/actions/runs/31829065426 ✓, reference-implementation https://github.com/PDP-Connect/pdpp/actions/runs/31829081713 ✓, docker-images in progress at packet time (passed on the content-identical prior head 92f6ea8f4, as did REUSE).

Deferred list unchanged. Ready for final closure review.

Assisted-by: AI

quiet: true suppresses only the library's injected tip/ad line; env
loading behavior is unchanged. The three pre-existing bin scripts that
share the banner (orchestrate, register-all, amazon-request-export)
are base-branch files left untouched here.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
@tnunamak

Copy link
Copy Markdown
Contributor Author

Post-approval cosmetic delta: ec1397fb6 adds quiet: true to the three scenario CLIs' dotenvConfig calls, suppressing only the dotenv library's promotional tip line from tool output. No behavioral change; env loading identical; tests green. Approved head f6a1eea43 is otherwise unchanged.

Assisted-by: AI

Credentials-kind fields and OTP codes now render as asterisks instead
of echoing to the terminal (and thence scrollback/pane captures).
Non-secret prompts are unchanged. Found by the owner during the first
live browser-connector dev run.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
…nel on TTY

The first live browser-connector run surfaced three UX defects: the
readline masking hack redrew the line and erased the question text, so
the operator typed into an unlabeled prompt; every credentials field
masked, including usernames; and the file-drop instructions printed
directly above a live terminal prompt with no indication which channel
to use. Masking is now a raw-mode reader that keeps the question
visible and applies only to secret-named fields; the file-drop channel
prints in full only on non-TTY runs, where it is the only way in.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
A terminal failure line names what broke, not what to do about it. The
first live browser run of this tool ended on
'venmo_probe_transport_error: Failed to fetch' after 78 seconds, with
nothing to distinguish bad credentials from a stale profile, provider
blocking, or a connector defect. connector-dev now prints derived
next steps per failure class (transport/auth/rate-limit, with a
generic fallback), including the exact PDPP_CAPTURE_ON_FAILURE
invocation that retains DOM/ARIA/screenshots/traces for the failing
run. Guidance is derived from the failure text, never invented.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
A total duration cannot tell an author where a long run went. A real
ynab run took 75 minutes across 11 streams; the dominant cost was one
stream (month_categories: 140 monthly windows against the connector's
audited ~20s/request pacing ceiling), and the summary reported only
record counts and a total. Stream summaries now carry first/last
record timestamps and derived elapsed_ms - from timestamps the
protocol already emits - and connector-dev prints streams
slowest-first with human-scaled durations.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
ensureVenmoOrigin swallowed page.goto errors, leaving the page on
about:blank so the auth probe fetched from an opaque origin and
reported the symptom (venmo_probe_transport_error: Failed to fetch)
with the cause discarded. Observed live: capture checkpoints showed
the probe at about:blank with a 39-byte DOM, the page reaching
venmo.com 75ms later. Navigation failure now throws
venmo_origin_navigation_failed with the real error, and the landed
origin is verified before returning. Note: in the observed runs the
underlying cause was an unusable display (0x0 viewport) - addressed
separately in browser-launch - but a swallowed goto can mask any
navigation failure and is wrong independently.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Two features, entangled in connector-dev.ts by concurrent development.

Iteration flags (ergonomics over existing protocol semantics - no new
concepts, per the essential-complexity review of the bounded-capture
idea this replaces): --streams a,b,c filters the START scope on
connector-dev and scenario-record (a real ynab run took 75 minutes
dominated by one paced stream; scoping makes iteration minutes), and
--seed-last-state reuses the prior run's committed cursors from
runs/<connector>/last-state.json for naturally short incremental live
runs. scenario-verify already replayed the recorded scope verbatim -
confirmed, not changed.

Failure evidence (per the failure-diagnosis prior-art research):
capture-on-failure is now the default in connector-dev (--no-capture
or explicit PDPP_CAPTURE_ON_FAILURE=0 to opt out) - every leading tool
retains by default, and opt-in retention meant the first failure was
always evidence-free. The speculative free-text hints are gone,
replaced by evidence pointers (failing checkpoint's URL and DOM size
from the capture just written) and a closed two-rule classifier
(viewport_zero, navigation_incomplete) that stays silent when no rule
matches. browser-launch fails fast with named remedies when the headed
browser comes up with a definitively 0x0 viewport (DISPLAY set but
unusable - the four-run venmo hunt this session) instead of running
toward a misleading transport error.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Passing a run-summary file to scenario-verify produced a FATAL stack
trace for a mistake the validator had already precisely diagnosed -
and the mistake was structurally guaranteed: summaries and scenarios
share runs/<connector>/, and an operator who has only used
connector-dev has a directory containing exactly one file, the wrong
kind. Validation rejections now print as plain verdicts; the
summary-vs-scenario confusion specifically gets told what the file is,
what scenarios exist in that directory, or the exact scenario-record
command when none do.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
…outs

A real scoped ynab capture died at 9m20s: run 2 was executing
correctly - ynab's audited ~20s/request pacing lawfully needs ~4.5
minutes - and was SIGKILLed by a fixed 300s total-duration ceiling
that assumed fast connectors. Both scenario CLIs now use an
inactivity watchdog (reset on any subprocess output; --timeout
<seconds> overrides the 300s window; suspended while a recording
interaction waits on the operator - thinking at a prompt is not a
hang). Firing produces a plain verdict with partial evidence
(per-stream counts, last message seen and when) instead of a stack
trace with empty stderr, and the capture is marked incomplete by
rule. The run-2 progress line summarizes seeded state per stream
instead of interpolating the full cursor blob into the terminal.
Unit tests caught a disposal race in the watchdog itself (post-close
data events could re-arm a kill against an exited subprocess).

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Replay wall-time was bounded by connector self-pacing running in real
time - a scoped ynab replay cost the same ~4-5 minutes as its live
capture, against the iterate-in-seconds goal. The first design (an env
check in the shared governor) was rejected in review as concern-mixing:
production machinery learning a consumer's execution mode, and covering
only governor pacing while inline page-delay sleeps escaped. Time is an
effect the replay environment already virtualizes (Date.now is pinned
there); the replay preload now also scales setTimeout/setInterval
delays by 100x - scaling, not skipping, so backoff-before-retry
semantics and relative timer orderings survive (proven by an
ordering fixture). Recording is untouched and paces for real. Known
gap, flagged not hidden: the connector spikes carry pre-extraction
inline copies of the preload and still pay real pacing - dedup debt.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
@tnunamak
tnunamak force-pushed the local/uat-final-0811 branch from d89d8cb to f537163 Compare August 15, 2026 03:47
Base automatically changed from local/uat-final-0811 to main August 15, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant