Skip to content

Record Deno background work, fix recorder bugs found on real apps, run real-app acceptance suites in CI - #1

Draft
evlawler wants to merge 55 commits into
mainfrom
deno-waituntil-trace-agent
Draft

evlawler wants to merge 55 commits into
mainfrom
deno-waituntil-trace-agent

Conversation

@evlawler

@evlawler evlawler commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

cc @kgilpin

What's in it

Deno and tracing (original scope)

  • Background work. Work handed to EdgeRuntime.waitUntil is now recorded.
  • Tracing agent. linker/bin/appmap-trace.mjs shows an ASCII call tree, a mermaid diagram, and a behavior diff against a baseline set of recordings.

Recorder bugs found by running on real open-source apps. Each fix has a test that fails without it.

  • Concurrent requests no longer mix into one recording. Deno and Node use per-request async context. Traffic from requests that aren't being recorded is not stamped with the recording's trace id.
  • Recording no longer changes app behavior. Values are captured without running getters or toJSON.
  • Zero-touch browser recording starts. The injected script now goes through Vite's pipeline.
  • XMLHttpRequest calls are recorded (for axios apps), and interaction windows wait for them.
  • The package installs and imports from another app. Exports point at dist, and npm pack ships the built files.
  • Output passes the official @appland/appmap-validate at 1.6.0 through 1.13.1, and the recorder declares 1.12. See docs/design/12-format-validity.md.
  • Calls nest as one tree per request.
  • Credentials are redacted: auth headers, JWTs, and params or props whose names look like passwords, tokens or keys.
  • A crash or signal leaves a valid, truncated recording. Periodic snapshots cover kill -9.
  • Cross-origin requests get traceparent only for origins listed in propagateTraceHeaderOrigins, following OpenTelemetry's model. Stamping every request made CORS preflight fail and broke apps.
  • Also:
    • The anonymous Deno.serve handler is now recorded.
    • JSX in .js files is supported.
    • include/exclude take globs, and test files are excluded by default.
    • A page load gets its own recording window.
    • Thrown plain objects keep their message.
    • React.useCallback/useMemo callbacks are recorded.
    • appmap-trace handles standalone Deno maps.

The end-to-end proof now uses open-source apps. The PetClinicGo e2e test is removed; it depended on our own experimental Go tracer and a private repo. Three acceptance suites run pinned open-source apps on localhost. Each suite's expectations were committed before the first recording and have not been edited since.

  • acceptance/bulletproof-react: React (Vite, Vitest, MSW), including a real Chromium browser.
  • acceptance/supabase-restful-tasks: a Deno edge function against real Postgres and PostgREST.
  • acceptance/supabase-edge-functions-app: a full-stack app. The React front end calls a Deno function, which uses GoTrue and Postgres with row-level security.

CI

Every PR runs these jobs, with nothing skipped (CI=true turns skip-if-missing into a failure):

  • typecheck + build
  • unit tests
  • native Deno tests
  • the Deno e2e test
  • one job per acceptance suite

Each acceptance suite's verdicts must match acceptance/<suite>/KNOWN_FAILURES.json exactly. The job fails on:

  • a new failure
  • any changed verdict
  • a changed evidence line on a known failure
  • a known failure that starts passing

Known failures (documented limitations, still reported as FAIL):

  • bulletproof-react C: one request that MSW hides until its response starts.
  • bulletproof-react I and browser: two clicks 20 ms apart share one window. The recording is marked ambiguous.
  • Full-stack A: the app uses Create React App, and the recorder only supports Vite.
  • Full-stack L: the app's CORS rules refuse traceparent. With that one header allowed, linking passes.

All jobs were run locally from a fresh clone on Node 22 before this push. Every job passes, and every acceptance verdict matches its ledger.

🤖 Generated with Claude Code

https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT

Brings appmap-react up to date with the work done since the initial
snapshot:

- Deno: work handed to EdgeRuntime.waitUntil (runs after the response
  is sent) is now recorded, and a recording cut off mid-write is
  repaired instead of lost. New deno/appmap_test.ts. Doc 11.
- Tracing agent (linker/bin/appmap-trace.mjs): ASCII call tree and
  mermaid sequence diagram per interaction, plus a behavior diff
  against a baseline set of recordings. Doc 10.
- Tracing agent no longer drops one backend call when an interaction
  fires several fetches at once (recorder/test/concurrency.test.ts).
- Recorder: value-size cap (APPMAP_EVENT_VALUESIZE), build config for
  publishing (tsconfig.build.json), recording tests.
- CI workflow for the npm and deno test suites.
- Recorder emits AppMap version 1.12; docs updated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT

@kgilpin kgilpin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Install necessary dependencies to fully validate this change and ensure they validate every PR run.

@claude[agent]

@Claude

Claude AI commented Sep 24, 2026 •

Copy link
Copy Markdown

Install necessary dependencies to fully validate this change and ensure they validate every PR run.

@claude[agent] ...

Added denoland/setup-deno@v1 to the node CI job in 1eab09b, so the full-stack Deno e2e test (examples/petclinic-react/test/e2e/deno-fullstack.test.ts) and the deno-edge smoke test now actually run on every PR instead of skipping themselves. The Go half (fullstack.test.tsx) still needs the PetClinicGo sibling repo, which isn't public — happy to wire that up too if you can point me at a checkout or a way to fetch it in CI.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • dl.deno.land
    • Triggering command: curl (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@Claude
Claude AI requested a review from kgilpin September 24, 2026 15:09
evlawler and others added 23 commits September 24, 2026 15:33
… function, written before recording

Target: supabase/supabase @ 74a3be9 examples/edge-functions (React test
client + select-from-table-with-auth-rls Deno function). Written from
reading the source and running the app with no recorder attached; no
recording exists yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
… harnesses unchanged

Files taken as-is from origin/acceptance/react (134b83b) and
origin/acceptance/deno (44d1af7): same run.sh, EXPECTATIONS.md, checks,
RESULTS.md and evidence, so CI can run them on every PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…l-stack e2e

examples/petclinic-react/test/e2e/fullstack.test.tsx needed this
project's own experimental Go tracer and a private sibling repo, so it
skipped itself in CI and proved only that two novel tracers agree with
each other. The full-stack proof is now
acceptance/supabase-edge-functions-app (Supabase's own React + Deno
example, real browser, real backend recordings).

- README, docs 02/05/09/10 and examples/deno-edge/README: dated
  amendments / updated pointers; no history rewritten.
- deno-fullstack.test.ts and the deno-edge smoke test still skip locally
  without deno, but fail when CI=true, so they cannot skip on a PR run.
- linker/bin/simulate-backend.mjs stays: npm run link:demo still uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…e-functions example, results and evidence

run.sh: clean sparse clone of supabase/supabase @ 74a3be9, local Postgres +
GoTrue + PostgREST (release binaries, the example's own migrations), a Kong
stand-in, the unmodified function under appmap-deno, the unmodified React
app in real Chromium (Playwright), appmap-link, official validator.
Checks A-J from the shared spec plus L (the cross-map link). Exits 1 on
any failure.

Result today: FAIL. With the plugin as documented the app does not
render (JSX in .js; bare virtual: import). With a config-only workaround
the recorder's traceparent header is blocked by the function's CORS
policy, so the app's main call fails and there is nothing to link. Only
with the app's CORS patched does browser traceparent -> Deno
parent_span_id -> appmap-link join; the stitched diagram still lacks the
handler and DB steps. Deno side: H PASS, but concurrent requests lose
recordings and get the wrong trace id on outbound calls. Details and
file:line for each bug in RESULTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
- build (typecheck + build), unit (Vitest), deno (native deno test),
  e2e-deno (real deno run smoke + React <-> Deno join), and one job per
  acceptance suite: supabase-edge-functions-app (full-stack React + Deno),
  bulletproof-react, supabase-restful-tasks.
- Node 22 (the harnesses and appmap-deno need --experimental-strip-types),
  Deno 2.x, Chromium via Playwright (Actions only), PostgreSQL server
  binaries from the runner image (apt fallback); GoTrue and PostgREST are
  downloaded by the harnesses themselves.
- CI=true (always set by Actions) makes skip-if-missing tests fail.
- Actions pinned to commit SHAs; acceptance evidence uploaded as
  artifacts.
- The acceptance jobs are expected to fail until the recorder fixes land;
  they are deliberately not continue-on-error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…ctions

Concurrent requests mixed into one recording and foreign traffic got the
open recording's traceparent: the session was one module global, read by
instrument.ts and fetchPatch.ts, and withAppMap only blocked a second
recording from *starting*. In a burst of 20 stamped + 10 unstamped
requests against a real edge function, one map held all 30 requests'
calls and 29 foreign outbound calls went out with its trace id.

session.ts now supports scoped recordings on an AsyncLocalStorage
(node:async_hooks, works in Deno and Node) installed by the driver:
activeRecording() answers from the current async context, so every
stamped request records only its own events into its own file, and an
unstamped request runs in a context with no recording, so it is never
recorded or stamped. withAppMap uses it, and waitUntil promises are now
collected per recording. The fetch patch is reference counted across
open recordings. The ambient API (tests, browser) is unchanged.

The browser has no async context, so two interactions close together
cannot be attributed; instead of silently naming the merged map after
the first click, the map now lists every interaction and sets
metadata.ambiguous (splitting would present a guess as fact).

Tests: deno/appmap_test.ts concurrent stamped + unstamped requests;
recorder/test/interactionRecording.test.ts overlapping clicks. The
vitest withAppMap test that pinned the old one-at-a-time rule now pins
the new behavior (both overlapping requests recorded, separately).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
formatValue JSON.stringify'd every parameter and return value. That runs
getters and toJSON: on React Query's tracked query result it read every
tracked field, subscribing the component to all of them, so components
re-rendered on changes they otherwise ignore. bulletproof-react's
"should create, render and delete discussions" passed 3/3 without the
recorder and failed 3/3 with it.

Values are now rendered by recorder/src/stringify.ts from property
descriptors only: own enumerable data properties print JSON-style (plain
data is byte-for-byte what JSON.stringify gave), accessors print as
"[getter]" and are never invoked, toJSON/toString are never called,
class names are read from descriptors, and depth, key count and output
length are bounded (cycles print "[Circular]"). Function-valued
properties now show as "[function name]" instead of vanishing.

Proof: recorder/test/valueCapture.test.ts (fails before, passes after);
the acceptance repro appmap-extra/observer-effect.test.tsx now passes;
bulletproof-react's suite is 21/21 with and without the recorder, three
runs each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Zero-touch interaction recording never started in a real browser. The
plugin's plain-function transformIndexHtml is a normal-order hook, which
Vite runs after its dev-HTML import rewriting, so the injected
`import "virtual:appmap-interaction-recorder"` reached the page as a
bare virtual: specifier and Chromium refused it (unsupported scheme).

Inject the URL Vite serves the virtual module at instead
(`<base>@id/__x00__virtual:appmap-interaction-recorder`, as
@vitejs/plugin-react does for its preamble), so the module and its own
import of the recorder go through Vite's pipeline. For a forced
production build, emit the recorder as its own chunk and link it.

Proof: recorder/test/vitePlugin.test.ts runs a real Vite dev server
(base / and /sub/), resolves the injected specifier against the page URL
as a browser would and fetches it and the recorder it imports (fails
before the fix: the URL has the virtual: scheme), plus a forced build.
Headless Chromium on the example app and on bulletproof-react's dev
server loads the recorder and writes an interaction map on click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Only fetch was patched, so an axios app's HTTP was invisible: no
http_client_request/response events in tests or in the browser, no
traceparent on the wire (nothing for appmap-link to join), and
interaction windows closed before XHR responses arrived because the
idle check had no pending request to wait for.

xhrPatch.ts wraps XMLHttpRequest while any recording is open (installed
and removed with the fetch patch): open() stamps a traceparent with the
recording's trace id, setRequestHeader() captures the same headers the
fetch patch keeps, and loadstart/loadend record the request/response
pair. Hooks go on the instance the app holds, and the request is taken
from events rather than from send(), because MSW's interceptor answers
mocked XHRs without calling the real send() and only fires listeners
registered through its proxy. Since both patches record into the same
events, the interaction window's pending-request count now covers XHR.

Proof: recorder/test/xhrPatch.test.ts (jsdom XHR against a real local
server, through msw/node, and an interaction window held open by a slow
XHR) fails before and passes after. bulletproof-react's recorded suite
now has its /auth, /discussions and /comments calls, still 21/21.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
A consuming app could not use the package by its documented specifiers:
exports pointed at .ts sources, which Node refuses to type-strip under
node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING from a
vite.config.ts importing '@funwithappmap/react-recorder/vite'); npm pack
shipped two files (package.json, src/index.ts) because files was
["dist"] with no build; and the built dist had extensionless relative
imports Node ESM cannot resolve. publishConfig.exports is not applied by
npm, so it never helped.

- Relative imports in recorder/src now name their .js targets, so tsc
  emits a dist Node can load (Vite, Vitest, esbuild and Deno's sloppy
  imports all map ./x.js to x.ts for the sources).
- exports/main/types point at dist with types conditions; prepack builds;
  files ships dist and src (for source maps); vite is an optional peer
  and @types/babel__core a dependency, since the public typings use them.
- README documents installing the packed tarball and the specifiers.

Proof: recorder/test/package.test.ts packs the package, installs the
tarball into a consumer, imports '.', '/vite' and '/transform' from
plain Node ESM, type-checks all specifiers under bundler and nodenext
resolution, and runs the consumer's own Vitest suite with the plugin
and the '/vitest' hooks, which writes an AppMap (all fail before).
Installing the packed tarball into bulletproof-react: the documented
`import ... from '@funwithappmap/react-recorder/vite'` config loads, and
a config using only package specifiers records all 21 tests, 21/21 pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
The official validator (@appland/appmap-validate) rejected every
recording the acceptance runs produced (0/29 React, 0/49 Deno) at the
declared 1.12 and at every version from 1.2 to 1.13.1.

- frameworks[].version: filled from the installed package (vitest, react
  when installed); a framework that can't be found is left out.
- Deno metadata.language.version: Deno.version.typescript.
- Values: VALUE_SIZE_CAP is 100 and the "…" counts toward it (was 1024,
  and 1025 after the ellipsis); a cut never splits a surrogate pair.
- exceptions[].object_id.
- http_client_request / http_server_request carry `message` (the query
  parameters) and `url` no longer includes the query string.
- HTTP calls with no expressible response (network error; still open at
  close) were closed with status 0 or a bare synthetic return, both
  invalid; they now go to metadata.unanswered_http_requests.
- Calls nest: each call records the call it was made from (synchronous
  caller, or via async context in Node/Deno the call whose continuation
  is running), and toAppMap() emits the events as that tree on one
  thread. A Deno request is now one tree rooted at its
  http_server_request (the official sequence diagram showed three
  disconnected roots), and a sync caller that returns while its async
  work runs no longer breaks nesting. The live event list is unchanged.
- classMap entries are keyed by name and location, so same-named
  functions in one file both appear.

The declared version stays 1.12, now earned: output is valid at every
schema from 1.6.0 to 1.13.1; 1.13 only adds an optional timestamp the
recorder does not emit, and the example app's tests pin 1.12. Docs,
README and package descriptions say so (docs/design/12).

Tests: recorder/test/validity.test.ts validates test-mode, server and
truncated recordings with the official validator and checks the tree;
deno/appmap_test.ts checks a request is one tree with language.version;
deno/test/withAppMap.test.ts validates a Deno map. The two value-cap
tests that pinned the off-by-one (cap + 1) now pin the exact cap, and the
frameworks test now requires versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
… driver going when one sign-up fails

In the first Actions run the browser sign-up step failed in every pass
(it passes locally), and the parallel driver then crashed without a
report. The log now carries the step error, recent gateway traffic,
browser console errors, failed requests and dialogs for any failing
pass, so a CI failure can be diagnosed from the job log alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Recordings carried credentials in plaintext: a React login test recorded
"password":"secret-pw-1", and a Deno parameter held a 191-char bearer
token. AppMaps are committed and shared, so these must never be written.

redact.ts is applied to every captured value: parameters, properties and
query parameters whose name matches password|secret|token|api[_-]?key
(case-insensitive) and the Authorization, Proxy-Authorization, Cookie
and Set-Cookie headers are recorded as "[REDACTED]", and "Bearer <token>"
inside any captured string (values, header values, exception messages)
keeps the scheme word and loses the token. A JWT inside any captured
string is redacted whatever it is called: a Supabase client carries its
key as `supabaseKey`, which no name rule catches, and the acceptance run
showed it in every Deno handler's parameters once values were rendered
without getters.

Test: recorder/test/redaction.test.ts (fails before, passes after).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
A recording was only written when it closed, so SIGKILL of deno, or
SIGTERM/SIGINT to the runner, 1.5 s into waitUntil background work left
no file at all: the self-heal in toAppMap() never ran. Doc 11 claimed
the opposite.

- SIGINT/SIGTERM, an uncaught error or unhandled rejection, and a normal
  exit with recordings open now write every open recording synchronously
  (self-healed, truncated). A signal then takes its default course, so
  the process still ends as before, unless the app has its own listener.
- kill -9: a recording open longer than 250 ms is snapshotted to
  <file>.appmap.json.part every 500 ms while it changes (temp file +
  rename, so a .part is always a whole valid map). The runner renames
  leftover .part files when its child dies, and so does the next
  withAppMap() on that directory.
- The runner now drops its forwarding handlers before re-raising the
  child's signal, so it actually ends with it.
- Doc 11 now says what is true, and README.

Tests: deno/appmap_test.ts SIGTERM, SIGINT, uncaught error and kill -9
cases against a real child process (all four fail before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
A thrown value that isn't an Error was recorded as
{"class":"object","message":"[object Object]"}: typeof and String(e).
supabase-js throws the plain parsed PostgREST error, so every failing
query in a Supabase function lost its message
('invalid input syntax for type bigint: "not-a-number"').

Exceptions now take the class from the constructor ("Object") and the
message from a string `message` data property when there is one, else
the value rendered as a parameter would be. Both are read from property
descriptors, so a message getter or toString is never run.

Test: recorder/test/exceptions.test.ts (fails before, passes after).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
The transform wrapped the callback of a bare useCallback(...)/useMemo(...)
only. Apps that write React.useCallback (bulletproof-react's
useDisclosure toggle/open/close and useAuthorization's checkAccess) were
never recorded. A member call whose property is useCallback/useMemo is
now treated the same; other React.* calls are left alone.

Test: recorder/test/transform.test.ts (fails before, passes after).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
The transform only wrapped named top-level declarations, so the usual
edge-function shape, Deno.serve(async (req) => { ... }), never recorded
its real entry function (routing, JSON parsing, the 400 path), and
handler work hung directly off the request.

The inline handler of a top-level Deno.serve call is now wrapped in all
three call shapes (handler, options + handler, options.handler), recorded
as <module>.handler at its line. deno/appmap.ts also re-exports
instrumentHandler, which the transform imports for closures nested in
instrumented functions; without it such an entry file failed to load
under appmap-deno.

Test: recorder/test/transform.test.ts Deno.serve cases (fail before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
appmap-trace treated every map with an outgoing request as a frontend
map, so a Deno request map traced on its own ran in a lane named
"frontend" and its http_server_request rendered as undefined.undefined;
and an axios app (no HTTP events before the XHR patch) traced 0
interactions with no warning.

- Interactions are frontend maps (outgoing requests, no incoming one)
  plus backend request maps no frontend map links to; the latter run in
  their own app's lane (client -> restful-tasks -> network) with the
  server request unwrapped into the title. A linked middle tier's own
  outbound calls render as calls to network instead of "?.?".
  appmap-link's frontend-map notion is unchanged.
- Request labels rebuild the query string from `message` when `url` has
  none (the recorder now follows the spec there), so a query-only change
  still shows in a behavior diff.

Tests: linker/test/trace-agent.test.mjs and linker/test/trace-cli.test.mjs
(fail before, pass after).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
… runners

On Actions, POST /auth/v1/signup returned 422 in most passes ("Anonymous
sign-ins are disabled": an empty email). Cause, in the driver: it
switched auth-ui-react to the sign-up view and typed immediately, but the
form's inputs are uncontrolled and a useEffect keyed on the view resets
their React state afterwards, so on a slower runner the typed values were
dropped. Reproduced locally by delaying React's scheduler (422 with the
old driver, 200 with the new one). The driver now types in the sign-in
view and then switches, which the app itself carries over, and lets
mount effects settle before the first sign-up in the parallel pass. The
S4 check now judges the interaction map that carries the sign-up POST,
and G keys every map of a step. Error responses from the gateway are
kept in the report, and CPU_THROTTLE=N is available to slow the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…e run of the fixed driver

Same verdicts as before (H PASS, J MEASURED, everything else FAIL / NOT
RUN on the recorder). Numbers updated; RESULTS.md now records the
sign-up race found on Actions and how it was reproduced and fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Both branch from getappmap/appmap-react PR head 1eab09b. Conflicts were
docs only: README status paragraph (CI branch's text, now "Twelve design
docs"), doc 02 (both 2026-09-24 amendments kept: XHR, then the retired
Go-backed e2e) and doc 10 (CI branch's "Not covered" text plus the fix
branch's standalone-request-maps amendment).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…lowed

Stamping traceparent on every fetch/XHR made the browser preflight
cross-origin requests, and a backend whose Access-Control-Allow-Headers
does not list traceparent (Supabase functions' default CORS) was
blocked: in acceptance/supabase-edge-functions-app every "Invoke
Function" click failed with FunctionsFetchError while recording.

Follow OpenTelemetry's browser model (propagateTraceHeaderCorsUrls):
same-origin requests are always stamped; cross-origin requests only for
origins the user lists in the Vite plugin's propagateTraceHeaderOrigins
option (or APPMAP_PROPAGATE_TRACE_HEADER_ORIGINS); with no page origin
(Node, Deno: no CORS) every request is stamped, as before. Every request
is still recorded. The option reaches the browser through the injected
interaction recorder and Vitest workers through the environment.

The example app's tests call cross-origin backends (MSW on :8080, the
deno-edge example on :8000) and now list them.

Regression tests: recorder/test/xhrPatch.test.ts "a cross-origin backend
whose CORS does not allow traceparent" fails before (jsdom: "Headers
traceparent forbidden") and passes after; recorder/test/propagation.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…e error

The transform enabled JSX only for .jsx/.tsx, so a Create React App
(JSX in .js, acceptance/supabase-edge-functions-app bug 1) failed on
every component with "Unexpected token" and did not render. Parse by
extension: .js/.mjs/.cjs/.jsx as JavaScript with JSX, .ts/.mts/.cts as
TypeScript without JSX, .tsx as both. If a file still cannot be parsed
it is served uninstrumented with a warning instead of a 500.

Regression tests: recorder/test/vitePluginSelection.test.ts ("instruments
JSX in a .js file", "serves a file it cannot parse uninstrumented") fail
before and pass after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
evlawler and others added 29 commits September 24, 2026 16:54
acceptance/supabase-edge-functions-app bug 7: the stitched diagram showed
only click -> POST /functions/v1/... -> 200. The frontend handler
(invokeFunction) was in the frontend map but no frontend events were
drawn, and the edge function reaches its database through PostgREST over
HTTP, which the renderer ignored (it drew only sql_query and function
calls). appmap-link's summary also counted the function's own maps as
frontend maps because they make outgoing requests.

The renderer now takes the interaction's map and draws its calls in
order with each request where it was made, and draws a backend's
outgoing HTTP calls (query from message) to a "network" participant. The
summary counts maps that serve a request as backend maps; they are still
linked onward.

Regression tests in linker/test/link.test.mjs fail before, pass after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Windows opened only on click/submit, so requests the page makes while
loading (bulletproof-react's GET /auth/me) were neither recorded nor
stamped with traceparent. installInteractionRecorder({ recordPageLoad })
opens a window named "load <path>" when installed; the Vite plugin's
zero-touch injection turns it on. Off by default for API callers.

Regression test: recorder/test/interactionRecording.test.ts "the page
load" fails before and passes after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Check change, rule (a). How C, H, I and W1 read an outbound call's URL.

Old rule: an http_client_request's URL is its `url` field, compared
verbatim with the expected path+query (e.g. `GET .../rest/v1/tasks?select=*&id=eq.1`)
and with the gateway's log.

New rule: the URL is `url` + "?" + the event's `message` parameters
(name=value, in order), then compared the same way.

Why: the AppMap spec defines http_client_request.url as the request URL
"excluding the query string" and puts query parameters in the event's
`message`; the recorder now follows it (docs/design/12). The expected
values are unchanged, and the comparison is still exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
The baseline was keyed by interaction name, so interactions sharing a
name (every direct request to an edge function is "POST /<function>")
were all diffed against the last baseline map of that name. In
acceptance/supabase-edge-functions-app H, R1 and R2 were compared with
R3 (no outbound calls) and their unchanged GET /auth/v1/user showed as
added. Pair by occurrence in recording order instead, and give each its
own output file.

Also count a step as "changed" only when its own outcome changed (one
inserted call read "1 added, 3 changed"; restful-tasks bug 8), and make
a linked request's outcome include the backend's own response status.

Regression tests in linker/test/trace-cli.test.mjs fail before and pass
after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…he entry call

Check change, rule (b). How C (and so E, which reads C's R7 row) reads
a request's function calls.

Old rule: the recording's function calls, sorted, must equal the expected
list exactly (R1 and R8: no function at all).

New rule: if the first function call is the anonymous Deno.serve handler
(index.handler, index.ts:68, not nested in another function), it is the
entry call; every other function must be nested inside it, and those
others, compared exactly as before, must equal the expected list. No
other extra call is allowed. summarize() now records each function's
enclosing calls to check the nesting.

Why: recording the anonymous Deno.serve handler is a fix the user asked
for (restful-tasks bug 7; EXPECTATIONS.md listed the handler as a gap,
not an expected call). Every expected function must still be present,
with the same line and parameters, inside the request's handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…its class

Check change (reading of parameter values). How C drops the supabase
client argument before comparing parameters.

Old rule: drop the first parameter, unless it is the only one; a
function whose only parameter renders as "[object Object]" is then
compared as "()" (that was how getAllTasks(supabaseClient) matched the
expected `index.getAllTasks@28()`).

New rule: drop the parameter recorded with class "SupabaseClient" (the
first parameter of every app function, index.ts:18, 28, 38, 48, 58) and
compare the rest exactly as before.

Why: the user asked for the observer-effect fix (value capture that
never runs app code; bulletproof-react bug 3, commit 6c8680c). It
renders objects by their own data properties, so the client is now
recorded as {"supabaseUrl":…} instead of "[object Object]", and the old
rule, which recognised the client only by that old rendering, compared
the client itself. The expected parameters are unchanged; this is not
covered by rules (a)-(f), so it is flagged for review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Check change, rule (e). How I decides that a function call in a
request's recording is foreign.

Old rule: every function call other than the request's own app function
(getTask/updateTask/deleteTask/createTask on its own id) is a foreign
call, i.e. a leak from another request.

New rule: the request's own entry call, the anonymous Deno.serve handler
(index.handler, index.ts:68: the first function call, nested in no other
function), is not foreign. Every other call is judged exactly as before,
including a second handler call; additionally every app function must be
nested inside this request's handler.

Why: recording the Deno.serve handler is a fix the user asked for; the
user's rule (e): concurrency leaks must count only events that belong to
a different request, not the request's own handler. Outbound calls and
the gateway's trace-id check are unchanged (their URLs follow rule (a)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…oss-contamination

Check change, rule (c). What W4 requires of two overlapping stamped
requests (B arrives during A's waitUntil background window).

Old rule: B must NOT be recorded ("unrecorded as documented": one
recording at a time), and A's recording must not contain B's background
calls (n=3 / probe-3) and must hold exactly one probe.ingest call.

New rule: A and B must each have their own recording, carrying their own
trace id; each must hold exactly its own probe.ingest(n) and exactly its
own outbound calls [POST /rest/v1/tasks, GET /enrich?n=<n>,
PATCH /rest/v1/tasks?name=eq.probe-<n>], and neither may contain any of
the other's calls.

Why: the user asked that overlapping stamped requests each get their own
recording (fix 6402f0d, per-request async context), which makes the old
"B unrecorded" rule describe the removed limitation. The new rule is
stricter: both recordings are checked, and each exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…ane is the app's

Check change, rules (a) and (f), plus the tracer's lane label. How H
checks that the diffs name the one change (deleteTask now first GETs
/rest/v1/tasks?select=*&id=eq.2).

Old rules:
 1. the official `appmap sequence-diagram-diff` text for R6 must contain
    "eq.2" (the added GET's query);
 2. appmap-trace --baseline for DELETE must say
    "New call frontend→network: GET /rest/v1/tasks?select=*&id=eq.2".

New rules:
 1. the official diff text must contain
    "added HTTP client request `GET http://127.0.0.1:54321/rest/v1/tasks`"
    (kept as a requirement; it can no longer show the query);
 2. appmap-trace must say
    "New call restful-tasks→network: GET /rest/v1/tasks?select=*&id=eq.2"
    (the full query, read from `message`).
Everything else in H is unchanged: only R6's diagram may change, and its
outbound calls must be exactly [GET ...?select=*&id=eq.2, DELETE ...?id=eq.2]
(compared per rule (a)); the other interactions must be "No behavior change".

Why: (a)/(f) the AppMap spec puts the query in `message`, which the
official sequence diagram does not render, so the query-level part of the
change is required of the trace diff instead. The lane name: the user
asked for the tracer bug where a Deno request map was drawn as
"frontend" to be fixed (restful-tasks bug 8, fix 84bae82); the old string
encoded that bug. The new string is just as exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
The recorder package's entry points are now its built dist/ (fix
49fee5c, "installable and importable by another app"), and the README
says to run `npm run build` when installing from a checkout. The
bulletproof-react and full-stack run.sh install it with file: from this
checkout but never built it, so a fresh CI checkout had no dist/. Setup
step only; no check changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
… origin for linking

Configuration change (no check logic changes), recorded as a check
change because it changes what the verdict runs record.

Old config: the plugin imported by relative path into node_modules/src
(bug 1 workaround), options { include, exclude, app }.

New config: the documented `@funwithappmap/react-recorder/vite` import,
plus propagateTraceHeaderOrigins: [origin of VITE_APP_API_URL]
(https://api.bulletproofapp.com under Vitest/MSW, http://localhost:8080
in the browser runs).

Why: the user asked that the recorder stamp traceparent on cross-origin
requests only for origins the user lists (OpenTelemetry's model), and
named that setting as the one thing a user must configure to link a
cross-origin backend. This app's API is cross-origin in both modes, and
EXPECTATIONS.md requires every http_client_request to carry traceparent,
so the harness configures it exactly as a user would. The app's mock
backend allows the header. Every expectation is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Check change, rule (a). How C, H, I and L read an outbound call's URL.

Old rule: an http_client_request's URL is its `url` field, compared
verbatim (e.g. `GET /rest/v1/users?select=*`, `POST .../auth/v1/logout?scope=global`).

New rule: the URL is `url` + "?" + the event's `message` parameters
(name=value, in order), compared the same way.

Why: the AppMap spec defines http_client_request.url as the request URL
"excluding the query string" and puts query parameters in `message`; the
recorder now follows it (docs/design/12). Expected values unchanged,
comparison still exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…pMap spec)

Check change (follows the AppMap spec; expectation wrong). How C (S2)
and E (frontend half) read S2's rejected fetch (the gateway 404s with no
CORS headers, so the browser rejects the fetch).

Old rule: the frontend map must hold an http_client_request/response
pair for the POST with status_code 0, balanced (EXPECTATIONS.md: "for a
rejected fetch the recorder's own contract is status 0").

New rule: the frontend map must list the POST in
metadata.unanswered_http_requests with reason "network error", must NOT
also hold an event pair for it, and its events must be balanced.

Why: the AppMap schema only allows an http_client_response with a
status_code of 100-599 (the official validator rejects 0), and check B
requires every map to be valid. The recorder therefore no longer closes
a call that never got a response with a fake status; it leaves the event
out and lists it in metadata (docs/design/12). The expectation described
the old, invalid contract, so it is recorded in RESULTS.md as
"expectation wrong (per the spec)", not edited. Not in rules (a)-(f):
flagged for review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…l; judge L with the linking setting

Check change (a behaviour change the user asked for). Two rules change,
plus configuration.

1. C, S3/S5. Old rule: in the zero-touch pass, the recorded
   POST .../functions/v1/select-from-table-with-auth-rls must carry
   traceparent (00-<map trace_id>-...). New rule: in the zero-touch pass
   it must carry NO traceparent (the function is on another origin,
   localhost:54321, and is not listed); status 200 is still required. A
   new diagnostic, cZL, requires the traceparent in the linking config.
2. L. Old rule: L1-L5 judged on the zero-touch pass. New rule: judged on
   pass ZL, the zero-touch config plus the one linking setting
   (propagateTraceHeaderOrigins: ['http://localhost:54321'],
   app-config/vite.config.appmap-link.mjs), app unpatched. The zero-touch
   pass is still run and reported. P / P-parallel (the CORS-patched
   diagnostic) now use ZL instead of the old bug-2 workaround config W,
   which does not stamp cross-origin calls either. ZL's maps are also
   validated in B.

Config: the Z config now imports the plugin by its documented package
entry point (bug 1 is fixed), not by a relative path into src/.

Why: the user asked that the recorder never break an app by stamping
traceparent on cross-origin requests the backend's CORS may refuse:
cross-origin stamping only for origins the user lists, and said linking
a cross-origin backend needs that option and a backend that allows the
header. So a correct zero-touch recording of S3/S5 has no traceparent,
and linking is judged with the one setting it requires. L is not made
easier: at the pinned SHA the function's CORS does not allow traceparent
(EXPECTATIONS R4), so in ZL the browser blocks the call and L still
fails; only the app-changing pass P can pass it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…(rule f)

Check change, rules (f) and (a). How H decides the one-line change
(.select('*') -> .select('id'), index.ts:41) was detected, and nothing
else.

Old rule: the normalized official sequence diagrams must differ for R1
and R2 and be identical for R3, R1/R2's diagram after the change must
contain "select=id", and R1/R2's outbound calls after must be exactly
[/auth/v1/user, /rest/v1/users?select=id].

New rule: the official diagrams and `sequence-diagram-diff` are still
produced and reported, not required. Required instead: appmap-trace
--baseline, run on each request's before/after pair, must say
"Behavior changed — 1 added, 1 removed." for R1 and R2 with exactly the
marks [+ GET /rest/v1/users?select=id, - GET /rest/v1/users?select=*]
and nothing else marked, and "No behavior change" with no marks for R3;
R1/R2's outbound calls after must still be exactly
[/auth/v1/user, /rest/v1/users?select=id] (rule (a)), and R3 has none.

Why: this is a query-only change. The AppMap spec puts the query in the
event's `message`, which the official sequence diagram does not render,
so the official diff cannot see it (it now reports the diagrams as
identical). The user's rule (f): keep the official diff as reported
evidence and require the change in the trace diff. The trace-diff
requirement is exact ("nothing else" is checked by the marks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Values are cut to 100 characters (schema cap, doc 12), so a plain object
argument lost its later fields: bulletproof-react's
registerWithEmailAndPassword({ email, firstName, lastName, password,
teamName }) was recorded without teamName (acceptance C, T2). Write the
spec's parameter `properties` ({ name, class } per field) for plain
objects on parameters and return values, from property descriptors only
(no getter runs; accessors are left out).

Regression tests: recorder/test/valueCapture.test.ts "parameter
properties" fail before and pass after; the recording validates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…spec)

Check change, rule (a). How C (T1-T9) and the browser check (B1-B4)
match an http_client_request against an expected URL pattern.

Old rule: the pattern (e.g. `.../comments\?discussionId=<id>&page=1$`,
`.../discussions\?page=1$`) is matched against the event's `url`.

New rule: it is matched against `url` + "?" + the event's `message`
parameters (name=value, in order).

Why: the AppMap spec defines http_client_request.url as the request URL
"excluding the query string" and puts query parameters in `message`; the
recorder now follows it (docs/design/12). Patterns unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
… properties (AppMap spec)

Check change (follows the AppMap spec). How C decides that a call's
parameter "contains" an expected field (T2: registerWithEmailAndPassword
called with firstName/lastName/email/password/teamName).

Old rule: some parameter's `value` string contains the expected text
(e.g. 'teamName').

New rule: some parameter's `value` contains the expected text, or its
`properties` list (the spec's { name, class } per field) has a field with
exactly that name.

Why: schemas 1.6+ cap `value` at 100 characters ("should be trimmed …
to 100 characters"), and B requires valid maps, so the recorder now cuts
values at 100 (docs/design/12). T2's argument
{"email":…,"firstName":…,"lastName":…,"password":…,"teamName":…} is
longer than that, so `teamName` cannot be in the value. The spec's
parameter `properties` carries an object's fields; the recorder now
writes it for plain objects (commit f652594). Value matches are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…ion wrong"

Check change (an expectation found wrong; EXPECTATIONS.md is not
edited). How C checks checkAccess's lineno in T3, T4, T5, T7a, T7b.

Old rule: checkAccess's call event must have lineno 35, as
EXPECTATIONS.md says.

New rule: it must have lineno 36, and the item's evidence says
"expectation wrong: line 35 -> 36" with the reason. The correction lives
in one table, EXPECTATION_ERRATA in scripts/analyze.py.

Why: at the pinned SHA, src/lib/authorization.tsx:35 is
`const checkAccess = React.useCallback(` and the function itself, the
arrow `({ allowedRoles }) => {`, starts on line 36. A function's lineno
is where the function starts. The user named this case and allowed
recording it as "expectation wrong" (ACCEPTANCE-SPEC rule 4: never edit
EXPECTATIONS.md; say so in RESULTS.md). The check is exactly as strict,
against the correct line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…s in G and I only

Check change, rule (d). How G (run 1 vs run 2) and I (each test alone
vs inside the parallel suite) compare the official sequence diagrams.

Old rule: the diagrams, with `elapsed` and `eventIds` removed, must be
identical.

New rule, for G and I only (not H): additionally, in every string, the
id after https://api.bulletproofapp.com/discussions/ or /comments/ is
replaced by <id>, using exactly

  (https://api\.bulletproofapp\.com/(?:discussions|comments)/)(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[A-Za-z0-9_-]{21})(?=$|[/?\s])

and the official tool's `digest` / `subtreeDigest` fields (hashes
computed over those same URLs) are left out. Everything else must be
identical as before. The report records the regex.

Why: the user's rule (d): random server-generated ids in URLs may be
normalized in stability/concurrency comparisons only, like timestamps.
The app's MSW mock backend gives each discussion/comment a random UUID
or nanoid per run; they reach the maps now that XHR traffic is recorded
(fix 753a208). Before this, G and I compared maps with no HTTP events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…ce diff (rule f)

Check change, rule (f). How H decides that dropping `page` from
getComments' request was detected, and nothing else.

Old rule: the normalized official sequence diagrams (run 1 vs run-h)
must differ in exactly the three comment-loading tests (should render
discussion, should update discussion, should create and delete a comment
on the discussion), those differences must mention "comments", and the
appmap-trace --baseline output must contain "page".

New rule: the official diagrams and sequence-diagram-diff are still
produced and reported, not required. Required: in appmap-trace
--baseline's ASCII diff, each of those three tests shows every
`- GET /comments?discussionId=<id>&page=1` paired with a
`+ GET /comments?discussionId=<id>` (same count), and no test, those
three included, has any other step marked added or removed. The result
is written to h-trace-check.json.

Why: the change is query-only, and the AppMap spec puts the query in the
event's `message`, which the official sequence diagram does not render,
so the official diff cannot see it. The user's rule (f): keep the
official diff as reported evidence and require the change in the trace
diff. "Nothing else" is now checked on the trace diff. Random ids are not
normalized here (rule (d) allows that only in G and I), so the mock
backend's per-run discussion/comment ids still count as changes: H fails
on them, and RESULTS.md says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…arked ambiguous

Reporting only; no verdict changes. The recorder now marks a window that
absorbed a second interaction with metadata.ambiguous and lists both in
metadata.interactions; B5's evidence shows that. B5 and I still fail
when the two clicks share one map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
No behaviour change: quote exit codes, split export from assignment, glob
instead of parsing ls, name the unused loop variable _, and annotate the
intentional 'a && b || c' and single-quoted inner-shell lines.
shellcheck 0.11.0 now exits 0 on all five scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…until the bugs are fixed

Each RESULTS.md gets an "Update: integration/pr1" section: the A-J
(and L / W / Browser) table before -> after from a fresh-clone CI=true
run, what still fails and why, the status of every bug the original run
listed, and a "Check changes" list (one commit each, with the old and new
rule). The original findings stay below it as the record.

README, doc 02 and the ci.yml comment no longer say the acceptance suites
are expected to fail until the recorder bugs are fixed: the bugs are
fixed, and the failures that remain are explained in each RESULTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
…linking config

Overhead figures from the fresh-clone CI=true run of every job. In the
full-stack linking config L1 fails too: the blocked request is listed as
unanswered, without its headers, so the header on the wire has no
recorded counterpart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Check change (rule d, extended to H). How H reads appmap-trace's diff.

Old rule: nothing but the three expected GET /comments changes may be
marked in any test; random ids may be normalized only in G and I.
New rule: the same, except that a "- X" / "+ X" pair that is identical
once the mock backend's random discussion/comment ids are normalized
(the same RANDOM_ID regex G and I use) is the same step, not a change.

Why: the mock backend mints new ids for created discussions and comments
on every run, so the before and after runs name the same resource
differently. That is run-to-run noise of the same kind as timestamps,
not a behaviour change. Any other marked step still fails H; checked by
turning one GET into a POST in the saved trace output, which H rejects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Each acceptance job now runs through acceptance/known-failures.mjs, which
compares every verdict with acceptance/<suite>/KNOWN_FAILURES.json. The job
fails on a new failure, on any changed verdict (a passing check that
regresses included), on a missing check, when a known failure's evidence
line changes, and when a known failure starts passing, so the ledger is
retired on purpose. Known failures are documented limitations and are
still reported as FAIL in the job log and in RESULTS.md.

Known failures today:
- bulletproof-react C (one request MSW hides from XHR until its response
  starts), I and BROWSER (two clicks 20 ms apart share one window in the
  browser, marked ambiguous);
- supabase edge-functions app A (Create React App, recorder is Vite-only)
  and L (the app's CORS refuses traceparent at the pinned SHA; with only
  that header allowed, L1-L5 pass);
- supabase restful-tasks F not run (no tests in the app).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
H passes with the mock backend's random ids normalized (check change 7).
Notes that CI holds this suite's verdicts to KNOWN_FAILURES.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
Each run.sh regenerates its evidence (the harnesses delete and recreate
evidence/ on every run) and CI uploads it as the job artifact, so the
committed copies were stale snapshots of earlier runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qStU1BiJDPwFbmGPPPtLT
@evlawler evlawler changed the title Record Deno background work; add behavior-diff tracing agent Record Deno background work, fix recorder bugs found on real apps, run real-app acceptance suites in CI Sep 25, 2026
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.

3 participants