Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 134 additions & 136 deletions .dev-loop/INGEST_REPORT.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ follow the cross-pointers in their index or take the next matching seeded domain
| [testing](wiki/testing/index.md) | **seeded** | Writing or structuring automated tests: level choice, test-before-code ordering, a UI action that is also a registered agent tool, cases/assertions, cross-layer effect scoping, test data, mock decisions, flaky tests, test-infrastructure containers (Testcontainers) failing on the dev host, testing a SwiftPM executable target (release-process quality → qa) |
| [qa](wiki/qa/index.md) | **seeded** | Release-quality process: release gates, regression scoping, bug reports, severity/priority triage, evidence for completion claims, the agent-tool parity gate for a web UI release, acting on code-review feedback, adversarial review of high-risk diffs, exploratory testing (guarded-path coverage, override matrices), scope-purity gates, sourcing deliverable documents from generated artifacts, verifying the quantitative claims in a document before publishing it, a documented claim about a third-party tool's side effects, an obligation row in a tier/policy table that another contract also pins, rationale prose left behind by a config-value change, automated verification of document deliverables (spec/RFC gates), an aging detector for model-coupled agent guidance, capturing an app's own screen content without Screen Recording permission (writing automated test code → testing) |
| [debugging](wiki/debugging/index.md) | **seeded** | Diagnosing a failure — finding what is wrong and why: reproducing, bisection, hypothesis testing, traces/logs, intermittent failures (fixing the diagnosed fault → its owning domain) |
| [security](wiki/security/index.md) | **seeded** | Trust-boundary decisions: input validation, session-vs-token auth choice, per-resource authorization (IDOR), secrets hygiene (including ciphertext orphaned by a regenerated encryption key), dependency trust, PII handling, in-session agent tool exposure (prompt-injection blast radius), the author identity a commit publishes to a public repository, host-compromise triage / incident response (verifying assumed security agents, identifying masquerading processes) (XSS rendering → frontend; CI secrets → infrastructure; JWT implementation → backend/frontend auth) |
| [security](wiki/security/index.md) | **seeded** | Trust-boundary decisions: input validation, session-vs-token auth choice, per-resource authorization (IDOR), secrets hygiene (including ciphertext orphaned by a regenerated encryption key), dependency trust, PII handling, in-session agent tool exposure (prompt-injection blast radius), a restriction flag in a config shared by several adapters that only some enforce, the author identity a commit publishes to a public repository, host-compromise triage / incident response (verifying assumed security agents, identifying masquerading processes) (XSS rendering → frontend; CI secrets → infrastructure; JWT implementation → backend/frontend auth) |
| [platforms](wiki/platforms/index.md) | **seeded** | OS-level differences breaking code across macOS/Linux/Windows: shell portability, BSD-vs-GNU CLI, filesystem case/line endings, Unicode normalization in text/file-name matching, commands inspected before execution, permission deny rules for bypass-mode agent workers, background services/cron, invoking prompt-capable CLIs non-interactively, toolchain version pinning |
| [mobile](wiki/mobile/index.md) | **seeded** | App-side iOS/Android/cross-platform: process death/state survival, offline-first sync, mobile-network calls, store rollout/hotfix strategy, startup time, modal presentation (several sheets/covers on one host, screen-level error sheets) |

Expand Down
4 changes: 4 additions & 0 deletions log.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,7 @@ Append-only. Format: `## [YYYY-MM-DD] <ingest|revise|lint|gap|contradiction|drif
## [2026-09-28] ingest | testing-strategy-agent-tool-shared-handler-tests — a UI action that is also a registered tool is tested once at the shared function plus two entry-point tests per tool (Registration incl. AbortSignal teardown, Wiring via spy) against a `document.modelContext` stub; a bug fix that changes the handler's contract changes the `inputSchema` assertion in the same commit; one DevTools Run-tool pass per release.

## [2026-09-28] revise | WebMCP adopted as the development standard (owner decision 2026-09-28): frontend/agent-interfaces/agent-facing-tool-surfaces trigger widened to any new or changed user action in a web UI + bug-fix and exclusion-list edge cases; AGENTS.md routing step 7 gains a web-UI-action row → frontend agent-interfaces then qa parity gate; INDEX.md frontend/qa/testing route lines and the frontend/qa/testing domain indexes updated; related links added both ways (release-gates, cross-layer-effect-tests, in-session-tool-exposure). The standard keeps the human UI primary and the tool layer additive (CG draft; Chrome origin trial + ChatGPT desktop runtimes).

## [2026-09-28] ingest | backend-common-change-impact-threading-a-parameter-through-executor-hops — threading a new parameter through a call chain with an executor hop: scan each function on the path (symtable: referenced but not parameter/local/module-bound) before tests; an exception raised inside a worker is stored on the future and a caller that records only `exception() is None` futures reports empty results with exit 0. Back-links from call-site-enumeration and async-failure-handling.

## [2026-09-28] ingest | security-agent-exposure-capability-flag-across-adapters — a restriction flag in a config shared by several adapters that only some enforce: census the readers, refuse the flag in non-enforcing adapters before spawning (CWE-636 fail-closed), gate it at the caller on adapter id, pass each adapter's native lock-down switch (pi `--no-tools`) unconditionally, test the true arm per adapter. Back-links from authorization-scope-persistence and unenforced-declarations.
3 changes: 2 additions & 1 deletion wiki/backend/common/api-design/unenforced-declarations.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ sources:
- https://kubernetes.io/blog/2023/04/24/openapi-v3-field-validation-ga/
- https://json-schema.org/draft/2020-12/json-schema-validation
last_verified: 2026-08-05
related: [security-input-validation-at-trust-boundaries, infrastructure-config-environment-config, backend-common-api-design-error-responses, qa-process-acceptance-criteria, backend-common-change-impact-widening-a-closed-value-table, platforms-processes-tool-diagnostics-without-a-failing-exit-code, backend-common-errors-diagnostics-from-a-shared-code-path]
related: [security-input-validation-at-trust-boundaries, infrastructure-config-environment-config, backend-common-api-design-error-responses, qa-process-acceptance-criteria, backend-common-change-impact-widening-a-closed-value-table, platforms-processes-tool-diagnostics-without-a-failing-exit-code, backend-common-errors-diagnostics-from-a-shared-code-path, security-agent-exposure-capability-flag-across-adapters]
---

# Accepting a Declaration the System Does Not Enforce
Expand Down Expand Up @@ -58,6 +58,7 @@ happened", or a feature was "configured" in an environment where it never ran.
| Case | Then |
|------|------|
| A newer client sends a field this older server has not learned yet | Warn rather than reject on the server, and let the *client's* strict mode catch it at author time; rejecting forward-compatible traffic breaks rolling upgrades |
| The declaration is a security restriction in a config shared by several adapters, and only some adapters implement it | Refuse it in every adapter that does not enforce it, before the adapter spawns anything — accept-and-warn is too weak when downstream automation trusts the flag ([security-agent-exposure-capability-flag-across-adapters]) |
| The declaration is enforced on one execution path but not another | Report it as unenforced on the path that ignores it, keyed by path — a single global status makes one of the two paths lie |
| The vocabulary is generated (parsed from a schema or enum) | Assert the parsed table is non-empty before using it to validate; an empty table accepts everything and turns strict mode into a no-op |
| Enforcement is measured but not applied (a budget reported, never imposed) | Say so in the diagnostic's wording — "measured, not enforced" — so a reader does not infer a guarantee from the value appearing in output |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ sources:
- https://docs.python.org/3/library/ast.html
- https://peps.python.org/pep-0570/
last_verified: 2026-08-05
related: [qa-process-regression-scope, backend-python-language-mutable-state-traps, testing-data-test-data-and-isolation, testing-quality-policy-at-several-return-sites, backend-common-change-impact-widening-a-closed-value-table, backend-common-change-impact-corpus-sweep-before-a-rejection-rule, backend-common-errors-diagnostics-from-a-shared-code-path, backend-common-change-impact-inserting-a-guard-before-an-existing-side-effect, backend-common-change-impact-sibling-validators-on-a-shared-node, security-data-masking-verification]
related: [qa-process-regression-scope, backend-python-language-mutable-state-traps, testing-data-test-data-and-isolation, testing-quality-policy-at-several-return-sites, backend-common-change-impact-widening-a-closed-value-table, backend-common-change-impact-corpus-sweep-before-a-rejection-rule, backend-common-errors-diagnostics-from-a-shared-code-path, backend-common-change-impact-inserting-a-guard-before-an-existing-side-effect, backend-common-change-impact-sibling-validators-on-a-shared-node, security-data-masking-verification, backend-common-change-impact-threading-a-parameter-through-executor-hops]
---

# Enumerating Call Sites Before Changing a Callee's Contract
Expand Down Expand Up @@ -67,6 +67,7 @@ the search never listed.
| A test helper wraps the callee or rebuilds its data shape (a fixture builder feeding it) | Read every helper definition the enumeration surfaces and enumerate the helper's own call sites too — the helper appears once in the callee enumeration while supplying the old contract to every one of its callers ([testing-data-test-data-and-isolation]) |
| A parameter is renamed but keeps its position and type | Keyword callers break loudly; positional callers keep working silently with the new meaning — the callee enumeration is the only search that lists them |
| Parameters of the same type are reordered | The most dangerous shape change: every positional caller still type-checks and silently swaps values. Rename the callee or change a parameter type so the mismatch surfaces at every stale site |
| The new parameter's value must travel through intermediate helpers (a retry wrapper, a dispatcher) before reaching the callee, and one hop is an executor `submit` | The intermediates are not callers of the changed callee — scan each function on the path for a reference to the new name it does not bind, and read an empty parallel result as a possible stored exception ([backend-common-change-impact-threading-a-parameter-through-executor-hops]) |
| Callers forward through `*args` / `**kwargs` / a dict spread | The call site names nothing the search can match — enumerate the forwarding wrapper's definition and treat its callers as a second enumeration pass |
| The change adds a parameter with a default | Every caller keeps compiling while silently receiving the default — enumerate and decide per site anyway, or the default becomes permanent behavior nobody chose |
| The change reshapes a data structure rather than the parameter list | Also enumerate the structure's producers (fixtures, factories, seed files) by its field names — they are call sites of the shape, not of the function |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
id: backend-common-change-impact-threading-a-parameter-through-executor-hops
domain: backend
category: change-impact
applies_to: [general, python]
confidence: verified
sources:
- https://docs.python.org/3/library/concurrent.futures.html
- https://docs.python.org/3/library/symtable.html
last_verified: 2026-09-28
related: [backend-common-change-impact-call-site-enumeration, backend-common-errors-async-failure-handling, testing-quality-sequential-dispatch-assumption-under-concurrency, debugging-signals-stack-traces]
---

# Threading a New Parameter Through a Call Chain That Crosses an Executor

## When this applies

You are adding a parameter to a callee and threading its value from the function
that has it, through one or more intermediate helpers, to the function that
consumes it — and one hop on that path is an `Executor.submit()` / `Executor.map()`
boundary (`ThreadPoolExecutor` or equivalent). Also when, after such a change, a
parallel section "produced no results" — an empty list, zero recorded outcomes —
with no traceback in the output and a green exit code.

Enumerating the *callers* of the callee whose signature changed →
[backend-common-change-impact-call-site-enumeration].

## Do this

1. **Enumerate the path by the new value's name, not by the callee's name.** The
callee enumeration lists who calls the changed function; an intermediate
helper that forgot the parameter is not a caller of anything new — it is a
function that *references* the new name without *binding* it. Run a scope
scan over the file before running tests:

```python
import symtable, builtins
top = symtable.symtable(src, path, "exec")
module_names = {s.get_name() for s in top.get_symbols() if s.is_assigned() or s.is_imported()}
def walk(t):
if t.get_type() == "function":
bound = set(t.get_parameters()) | set(t.get_locals())
for s in t.get_symbols():
n = s.get_name()
if s.is_referenced() and n not in bound and (s.is_global() or s.is_free()) \
and n not in module_names and not hasattr(builtins, n):
print(f"{t.get_name()}: uses {n!r} but does not bind it")
for c in t.get_children(): walk(c)
walk(top)
```

`symtable` exposes the two facts the scan needs — `Function.get_parameters()`
"Return a tuple containing names of parameters to this function" and
`Symbol.is_referenced()` "Return `True` if the symbol is used in its block". A
referenced name that is neither parameter, local, module binding nor builtin
is a `NameError` waiting for the first call.

2. **Read the executor boundary as an exception sink.** `Future.result()`: "If
the call raised an exception, this method will raise the same exception";
`Executor.map()`: the exception "will be raised when its value is retrieved
from the iterator". Nothing raises at `submit()` time. A caller that records
only futures whose `exception()` is `None` — or appends `result()` inside a
`try` that continues — converts every worker failure into missing work.

3. **Decide the surfacing policy per collection loop:**

| Collection loop | Do |
|-----------------|----|
| Records `result()` only for futures that finished OK | Log or re-raise `future.exception()` for every other future — each per-future failure must leave a trace with its item id |
| Iterates `Executor.map()` | The first failed item raises at retrieval and skips the remaining retrievals — decide whether that abort is wanted, and catch per item if not |
| `as_completed` with `except Exception: continue` | Record `repr(exc)` against the item before continuing; an empty `except` here is the silent mode |

4. **Treat "block produced no results" in executor code as a possible swallowed
exception before treating it as a logic error.** Print `future.exception()`
for each future, or call `result()` on one, before reading the business logic.

5. **Re-run the scan after the edit and require zero unbound names on the path,
then run the tests** — the re-run turns the scan into a completion check.

## Edge cases

| Case | Then |
|------|------|
| The path crosses a `functools.partial`, a lambda, or a bound method handed to `submit` | The argument list lives at the `submit`/`partial` site — include those sites in the path enumeration |
| A retry wrapper sits between caller and callee | The wrapper is a function on the path and needs the parameter as much as the callee — the field incident's missing hop was the retry helper |
| The new name shadows a module-level or builtin name (`keys`, `id`, `input`) | The scan's module/builtin exclusions hide it; grep the name as well — a same-named module binding turns the `NameError` into wrong-value behaviour with no exception at all |
| The language is not Python | The mechanism holds where a result is materialized on read: Java `Future.get()` wraps the failure in `ExecutionException`, JS `Promise.allSettled` records rejections, Rust `JoinHandle::join` returns `Err` — the collection loop decides what surfaces |
| The parallel branch's test asserts `== []` or a count that can legitimately be zero | That assertion cannot separate "nothing to do" from "every worker raised" — add a per-item outcome assertion ([testing-quality-sequential-dispatch-assumption-under-concurrency]) |

## Instead of

| If you are about to | Do this instead | Why |
|---------------------|-----------------|-----|
| Grep the callee's name, update its callers, run the tests | Also scan every function between the value's source and the callee for a reference to the new name that is not bound there | The intermediate helper is not a caller of the changed callee, and it is exactly where the `NameError` lives |
| Read "N parallel tests fail with `[]`" as a scheduling or ordering bug | Print `future.exception()` for each future first | Reproduced 2026-09-28: a `NameError` in the worker gave `results: []`, exit code 0, no traceback |
| Keep `if f.exception() is None: results.append(f.result())` as the whole collection loop | Record every non-`None` exception against its item | The loop is correct for the futures it records and silent for the ones that carried the failure |

## Sources

- https://docs.python.org/3/library/concurrent.futures.html — `Future.result()`: "If the call raised an exception, this method will raise the same exception"; `Future.exception()`: "Return the exception raised by the call … If the call completed without raising, `None` is returned"; `Executor.map()`: "If a *fn* call raises an exception, then that exception will be raised when its value is retrieved from the iterator" — the exception is stored on the future and surfaces only where a result is read
- https://docs.python.org/3/library/symtable.html — `Function.get_parameters()` "Return a tuple containing names of parameters to this function"; `Function.get_locals()` "Return a tuple containing names of locals in this function"; `Symbol.is_referenced()` "Return `True` if the symbol is used in its block"; `Symbol.is_free()` "Return `True` if the symbol is referenced in its block, but not assigned to"; `SymbolTable.get_children()` "Return a list of the nested symbol tables" — the basis for the step-1 scan
- Reproduction 2026-09-28 (CPython 3.14.4, macOS): `run_parallel` submits `run_with_retry(step)` to a `ThreadPoolExecutor(max_workers=4)` and appends `result()` only when `exception()` is `None`; `run_with_retry` calls `run_step(step, keys)` without binding `keys`. Output: `results: []`, exit code 0, no traceback. Calling `result()` on one future raised `NameError: name 'keys' is not defined` from inside the worker. The step-1 scan printed `run_with_retry: uses 'keys' but does not bind it` on the broken file and printed nothing on the fixed file (parameter added and passed at the `submit` site), which then returned three results
- Field incident 2026-09-28 (a Python workflow runtime, task t175): `binding_keys` was threaded to the step executor but not to `_execute_step_with_retry`, which ran inside `ThreadPoolExecutor` workers; 8 `test_parallel_execution` cases failed showing `[]` steps and no traceback. An `ast.walk` scan listing each function's references to the name against its parameters/locals located the single missing hop and confirmed no other function on the path lacked it
Loading
Loading