Skip to content

map: match full input paths and bind flows to the exact sink call - #123

Merged
patchstackdave merged 2 commits into
mainfrom
fix/map-flow-path-and-ownership
Aug 13, 2026
Merged

map: match full input paths and bind flows to the exact sink call#123
patchstackdave merged 2 commits into
mainfrom
fix/map-flow-path-and-ownership

Conversation

@patchstackdave

Copy link
Copy Markdown
Contributor

The two blockers an external review named before map can be used as an automatic rule-authoring source. Both let precise — the signal a generator would pin a parameter on — identify the wrong input.

1. Path collisions

Matching reduced both inputs and reads to their last segment, so billing.email and shipping.email both became email: reading one marked both precise.

Flows now compare canonical full paths (data.shipping.emailshipping.email), with index tokens erased so tags[0].label, tags[].label and tags.label compare equal. precise requires the whole path to match, or the input to be an ancestor of what was read (billing legitimately covers billing.email).

2. Sink ownership

Evidence was gathered from the enclosing statement, so a sibling expression could lend it:

await Promise.all([ audit(data.title), db.from("items").insert({ title: "system" }) ]);

…reported title → insert [precise] although the insert only receives a literal.

Sinks now carry their source span (start/end) as their identity, and evidence comes only from that call's own arguments plus other calls in the same fluent chain (.update({…}).eq('id', data.id) is one operation). Walking up stops at an array literal or argument position — which is exactly what excludes the sibling.

Two subtleties worth noting: a line is not an identity, and neither is a start offset alone — in db.from(t).insert(x) both calls start at db, so the key is start:end.

Two recall bugs found while testing

Both silently produced zero precise flows for the most common shapes:

  • A validated payload wasn't tainted. Schema.parse(await req.json()) broke the chain, so every validated handler lost its flows. Validation is not sanitization — a validated value is still attacker-controlled — so taint now survives parse/safeParse.
  • A destructured handler param was treated as a path segment. { data } (TanStack's validated-payload convention) shifted every path by one segment, killing all matching.

Result on the real reference app

More accurate than before — the spurious helper-reached select flows are gone, and the chain-fed filters are correctly precise:

createTask:   precise=[title->insert]
toggleTaskFn: precise=[id->update, completed->update]
deleteTask:   precise=[id->delete]
getTasks:     precise=[]

Sinks also expose start/end in the emitted map, as the review asked, so a consumer can point at the exact call. 784 tests green; typecheck clean.

Not in this PR

The review's larger rule-generation roadmap — argument roles (command vs args, URL vs body, path vs contents, raw SQL vs values), the finer confidence taxonomy (exact/transformed/interprocedural/heuristic/unknown), a TypeChecker-based dataflow engine, explicit why-not-generatable reporting, the candidate lifecycle, and corpus metrics. Those are the next tranche, tracked separately; this PR only removes the two correctness blockers.

patchstackdave and others added 2 commits August 13, 2026 14:35
…eal Workers bundler

Closes the last gap from the external review: our edge coverage bundled
dist/protect.edge.js DIRECTLY, which proves the artifact is edge-clean but not that a
consumer ever reaches it — a mis-ordered or mistyped `exports` condition would silently
hand an edge bundler the Node build.

- tests/protect/edge-export-resolution.test.ts imports the real specifier
  (`@patchstack/connect/protect`) from a fixture with the package linked into
  node_modules, and resolves it under workerd / worker / edge-light / deno / browser,
  asserting each lands on the edge artifact. The CONTROL is what makes it meaningful:
  with no edge condition the same import resolves to the Node build and FAILS to bundle
  for a Node-free target, so a pass is caused by the condition rather than a lenient
  target. Mutation-checked: pointing `workerd` at dist/protect.js makes it fail.
  (platform 'neutral' on purpose — 'browser' would inject the `browser` condition and
  mask whether the edge conditions themselves work.)

- scripts/verify-edge-platform.mjs (`npm run verify:edge`) compiles a real Worker with
  the actual Cloudflare toolchain (`wrangler deploy --dry-run`) and asserts wrangler
  selected dist/protect.edge.js and emitted a bundle with no Node builtins. Verified
  locally: wrangler 4 compiles it, 134.90 KiB, zero Node imports. It downloads wrangler
  and shells out to a platform bundler, so it is deliberately NOT in `npm test` (which CI
  runs on four Node versions) — the two suite-level tests cover the same property cheaply
  and this is the end-to-end confirmation for a release job.

A native `next build` fixture remains the one uncovered variant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two blockers an external review named before `map` can drive automatic rule
generation. Both let `precise` — the signal a generator would PIN A PARAMETER on —
identify the wrong input.

1. PATH COLLISIONS. Matching reduced inputs and reads to their last segment, so
   `billing.email` and `shipping.email` both became `email`: reading one marked BOTH
   precise. Flows now compare canonical full paths (`data.shipping.email` →
   `shipping.email`), with index tokens erased so `tags[0].label`, `tags[].label` and
   `tags.label` compare equal. `precise` requires the whole path to match, or the input to
   be an ANCESTOR of what was read (`billing` legitimately covers `billing.email`).

2. SINK OWNERSHIP. Evidence was gathered from the enclosing statement, so a sibling
   expression could lend it:
       await Promise.all([ audit(data.title), db.from("items").insert({ title: "system" }) ]);
   reported `title -> insert [precise]` although the insert only receives a literal.
   Sinks now carry their source span (`start`/`end`) as their IDENTITY, and evidence comes
   only from that call's own arguments plus other calls in the SAME fluent chain
   (`.update({…}).eq('id', data.id)` is one operation). Walking up stops at an array
   literal or argument position, which is what excludes the sibling. Note a line is not an
   identity, and neither is a start offset alone: in `db.from(t).insert(x)` both calls start
   at `db`, so the span key is start+end.

Also fixed two recall bugs found while testing, both of which silently produced ZERO
precise flows for the most common shapes:
 - a validated payload (`Schema.parse(await req.json())`) was not treated as tainted, so
   every validated handler lost its flows. Validation is NOT sanitization — a validated
   value is still attacker-controlled — so taint now survives `parse`/`safeParse`.
 - a destructured handler param (`{ data }`, TanStack's validated-payload convention) was
   treated as a path SEGMENT rather than a container, shifting every path by one and
   killing all matching.

Sinks now expose `start`/`end` in the emitted map, as the review asked, so a consumer can
point at the exact call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderbuds

coderbuds Bot commented Aug 13, 2026

Copy link
Copy Markdown

Precise span-based flow binding greatly improves path matching accuracy.

🎯 Quality: 80% Excellent · 📦 Size: Large — consider splitting if possible

🛡️ Standards: no pre-flight fit check ran for this change — wire assess-change-fit into your coding agents to catch size before opening.

📈 This month: Your 56th PR — above team average · Averaging Good

See how your team is trending →

@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

@patchstackdave
patchstackdave merged commit 4526ad6 into main Aug 13, 2026
5 checks passed
@patchstackdave
patchstackdave deleted the fix/map-flow-path-and-ownership branch August 13, 2026 13:12
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.

2 participants