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
297 changes: 74 additions & 223 deletions .dev-loop/INGEST_REPORT.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions log.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,4 @@ Append-only. Format: `## [YYYY-MM-DD] <ingest|revise|lint|gap|contradiction|drif
## [2026-08-30] revise | frontend/design/design-canvas-workflow — agent-gate Check 3 remediation (PR #164). The gate's fact finding was half right: its "no skill named `design` exists" conclusion is refuted by ground truth (the skill is listed in the authoring session's available-skills roster as `design: Create a design canvas…` and its bundled payload exists on the authoring machine's disk; the reviewer's CI environment lacks the preview, and it conflated the skill with the unrelated `/design-sync` component-upload tool) — but the verifiability objection stands: the skill is an early research preview absent from public docs, so a single un-fetchable in-session source cannot carry `confidence: verified`. Fixed per AGENTS.md: confidence downgraded to field-tested with the experience context stated in the body; two live-fetched public sources added (anthropic.com/news/claude-design-anthropic-labs — official Claude Design product announcement, research preview, Claude Code handoff; explainx.ai 2026 post — /design ships in Claude Code as a research-preview command producing editable artboards via the Artifacts runtime); the mandatory-routing directive and the frontend/index.md load-when line conditioned on the skill appearing in the session's available-skills list, with the existing no-skill edge case as the explicit fallback; the unverifiable "design-review skill" name-drop in the audit edge case generalized to session-provided audit tooling.
## [2026-09-03] ingest | databases/selection — new category: datastore selection by workload (4 pages: choosing-a-datastore-by-workload, relational-jsonb-vs-document-store, vector-search-engine-selection, graph-workloads-relational-vs-graph-db); brave-search research, sources cited per page
## [2026-09-04] ingest | infrastructure/agent-orchestration/code-graph-as-orientation-layer (field-tested — 1 new page). A locally built code knowledge graph (graphify) is a freshness-gated, lead-not-evidence orientation layer for planning and parallel task decomposition: gate on graph mtime vs git log, symbol-anchored explain/path only, CLI delivery with bounded output, graph-derived assumptions named in reports. Sources: Developers Digest, AQ Score, Autonoma, Tiare Balbi, graphify README + measurements on graphifyy 0.4.23.
## [2026-09-08] ingest | Knowledge flush — 21 queued insights: 12 new pages, 8 merges, 1 fold onto open PR #183. New: infrastructure-ci-cd-secret-needing-gate-on-fork-prs (pull_request_target + trust if + base-ref checkout + fail-closed verdict), infrastructure-ci-cd-review-bot-adopted-as-a-blocking-gate (OCR exit code ≠ findings; parse JSON/SARIF yourself), infrastructure-agent-orchestration-tool-retirement-knowledge-transplant (inventory + trigger-level gap analysis before disabling), backend-python-language-dict-subclass-attribute-loss-on-copy (dict displays drop subclass attrs; copy.copy / mutate+revert), backend-python-language-iterative-dfs-for-unbounded-graph-depth (explicit frame stack vs recursion limit), qa-document-verification-sweeping-pre-gate-citations-for-fabrication (retroactive citation sweep, quote marks verbatim), qa-process-session-identity-leak-in-plugin-prose (grep distributed plugin prose for the session's injected name), platforms-toolchains-regeneration-silently-drops-hand-edited-state (xcodegen regen deletes GUI targets/schemes; diff --stat first), testing-quality-narration-based-ordering-assertions (read real state, mutate to prove red), testing-mocking-autouse-fixture-shadows-function-under-test (capture original before monkeypatch), testing-quality-cross-task-stub-assertions (assert root class/contract not stub wording), testing-strategy-real-cli-spot-check-for-new-execution-paths (green scripted suite ≠ real path). Merges: security-data-commit-identity-in-public-repos (+cross-repo %ae/%ce/trailer audit, filter-repo --mailmap --replace-message), security-data-masking-verification (+whitelist vs blacklist mask, direct-store-read call sites), infrastructure-agent-orchestration-session-completion-gates (+ledger gate, no-progress release valve), debugging-methodology-probe-path-vs-operation-path (+layer-by-layer pipeline probing via real store entrypoint), frontend-state-effects-usage (+rAF loop keyed on hook-returned object; ref mirror), platforms-toolchains-flag-availability-at-the-execution-site (+verify flag by real invocation, not --help), qa-document-verification-editing-a-gated-document (+pinned corpus count vs lint count), infrastructure-agent-orchestration-worktree-isolated-workers (+info/exclude over tracked .gitignore). Fold: multi-line CHECK truncation in gate-check.sh → edge row proposed on PR #183's gate-parsing-vs-command-execution. Deferred related links (ids only on open PRs): frontend-design-pointer-attracted-particle-fields (#187), infrastructure-agent-orchestration-checkable-claims-in-an-adopted-plan (#181), infrastructure-agent-orchestration-verify-command-in-a-worker-brief (#179), qa-document-verification-model-coupled-guidance-aging-detector (#186), testing-quality-gate-parsing-vs-command-execution (#183)
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]
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, security-data-masking-verification]
---

# Enumerating Call Sites Before Changing a Callee's Contract
Expand Down
2 changes: 2 additions & 0 deletions wiki/backend/python/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ Match your situation to a "load when" line; load only matching pages.
| [bytecode-cache-staleness](language/bytecode-cache-staleness.md) | A script or harness rewrites `.py` files and re-runs them in a loop (mutation testing, edit/test/revert, codegen check, bisect) and the result stops tracking what is on disk — a revert that `git diff` reports clean still fails, or an injected change has no effect; choosing between clearing `__pycache__`, refreshing mtime, and hash-based `.pyc` (PEP 552); designing byte-length-preserving mutations |
| [default-encoding-in-text-io](language/default-encoding-in-text-io.md) | Python opens a text file without `encoding=` (`open`, `Path.read_text`, `subprocess` text mode) and you are adding the argument or writing the regression test that keeps it there; a file-writing bug reproduces on Windows, a `LANG=C` container, or a cp949/cp932 desktop but not on your machine; choosing a test discriminator that does not depend on the runner's locale |
| [source-introspection-of-a-dynamically-loaded-module](language/source-introspection-of-a-dynamically-loaded-module.md) | A check, doc snippet, or test loads a module by path (`spec_from_file_location` + `module_from_spec` + `exec_module`) and then introspects it (`inspect.getsource`/`getfile`/`getsourcelines`) or pickles its objects; a snippet raises `TypeError: <class 'x.C'> is a built-in class` for a plainly pure-Python class; choosing between registering the module in `sys.modules` and retargeting the assertion from a class to one of its methods; deciding whether an always-red check is the target's defect or the loader's |
| [dict-subclass-attribute-loss-on-copy](language/dict-subclass-attribute-loss-on-copy.md) | Building a new dict from a `dict` subclass instance that carries extra instance attributes (an ORM/driver row wrapper holding optimistic-lock version info, an ETag, or similar out-of-band metadata) via `dict(obj, **{...})` or `{**obj, ...}` before adding or overriding a key; downstream `getattr(row, "attr", None)` returns `None` instead of the real value with no error; choosing between `copy.copy` and in-place mutate-and-revert |
| [iterative-dfs-for-unbounded-graph-depth](language/iterative-dfs-for-unbounded-graph-depth.md) | Implementing a DFS-based graph or cycle-detection algorithm in Python (compiler diagnostics walking declared-entity relationships, a dependency graph) where the traversal depth scales with user/module content rather than a small fixed set; a recursive implementation risks `RecursionError` near Python's default 1000-frame limit; tempted to raise `sys.setrecursionlimit` instead |

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
id: backend-python-language-dict-subclass-attribute-loss-on-copy
domain: backend
category: language
applies_to: [python]
confidence: verified
sources:
- https://docs.python.org/3/reference/expressions.html#dictionary-displays
- https://docs.python.org/3/library/copy.html
- "Local reproduction (python3 3.14.6, 2026-09-08): dict(obj, **{...}) and {**obj, ...} on a dict subclass both yield type(...) == dict with the subclass's instance attribute gone; copy.copy(obj) yields the original subclass with the attribute intact"
last_verified: 2026-09-08
related: [backend-python-language-mutable-state-traps, databases-transactions-optimistic-vs-pessimistic-locking]
---

# Dict Subclass Attributes Lost When Building a New Dict From It

## When this applies

You hold an instance of a `dict` subclass that carries extra instance
attributes alongside its key/value contents — an ORM/driver row wrapper
storing optimistic-lock version info, an ETag, or similar out-of-band
metadata — and you need to add or override a key on it before passing it
onward (to a serializer, a cache, an UPDATE builder).

## Do this

| Case | Do |
|------|----|
| The mutation must not be visible to other holders of the same reference | `copy.copy(obj)`, then mutate the copy — `copy.copy()` "normally returns an instance of the same type", preserving the subclass and its instance attributes; the top-level container is independent of the original |
| The original may be safely changed temporarily and must be restored before anything else observes it | Mutate in place — `obj[key] = value` — then revert in a `finally`: `obj.pop(key, None)` |
| The receiver is verified to never read the subclass's instance attributes | `dict(obj, **{key: value})` / `{**obj, key: value}` is fine — but the verification has to happen first, not be assumed |

`dict(obj, **{...})` and `{**obj, ...}` are dictionary displays, and "a
dictionary display yields a new dictionary object" — always plain `dict`,
regardless of the runtime type of `obj`; `**` unpacking only copies key/value
pairs into that new object, never `__class__` or `__dict__`/`__slots__`. Reach
for them only in the third row's case.

## Edge cases

| Case | Then |
|------|------|
| Downstream code reads the attribute via `getattr(row, "x", None)` | The loss is silent — `getattr` returns the default with no exception, so the bug shows up as wrong behavior far from the copy site, not a stack trace at it |
| Downstream code reads the attribute directly (`row.x`) | The loss is loud — a plain `dict` raises `AttributeError: 'dict' object has no attribute 'x'` immediately, which is why this trap survives in codebases that always use `getattr` defensively |
| The subclass nests further mutable state (a cache, a list) that must also stay independent of the original | `copy.copy()` is shallow — nested mutable values still alias the original object; only the top-level container and its own attributes are independent |
| The subclass defines `__copy__` | `copy.copy()` calls it — confirm the override also copies instance attributes before relying on it as the fix |

## Instead of

| If you are about to | Do this instead | Why |
|---------------------|-----------------|-----|
| Build `dict(row, **{key: value})` or `{**row, key: value}` from a `dict` subclass carrying instance attributes | `copy.copy(row)` then mutate the copy, or mutate `row` in place and revert in a `finally` | Dictionary displays always construct a plain `dict`; the subclass type and every attribute that lived only on the instance are silently dropped |

## Sources

- https://docs.python.org/3/reference/expressions.html#dictionary-displays — "A dictionary display yields a new dictionary object"
- https://docs.python.org/3/library/copy.html — slicing and `.copy()` methods "can create an instance of the base type when copying an instance of a subclass, whereas copy.copy() normally returns an instance of the same type"
- Local reproduction (python3 3.14.6, 2026-09-08): a `VersionedRow(dict)` with instance attribute `observed_version=42`; `dict(row, **{"_schema_gen": "abc"})` and `{**row, "_schema_gen": "abc"}` both produced `type(...) == dict`, `hasattr(..., "observed_version") == False`; `copy.copy(row)` produced `type(...) == VersionedRow`, `observed_version == 42`, and mutating the copy left `row` untouched; `getattr(dict_instance, "observed_version", "DEFAULT")` returned `"DEFAULT"` while `dict_instance.observed_version` raised `AttributeError`
- Field incident (a Python SQLite repository driver): `_read` returns a `_VersionedRow(dict)` carrying `.observed_version` for a conditional UPDATE; `dict(row, **{"_schema_gen": digest})` silently dropped `.observed_version`, turning every conditional UPDATE unconditional — caught by failing concurrency/retry-conflict tests (`1 != 10` on a concurrent-increment test), not by an exception
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
id: backend-python-language-iterative-dfs-for-unbounded-graph-depth
domain: backend
category: language
applies_to: [python]
confidence: verified
sources:
- https://docs.python.org/3/library/sys.html#sys.getrecursionlimit
- https://docs.python.org/3/library/sys.html#sys.setrecursionlimit
- https://docs.python.org/3/whatsnew/3.12.html
last_verified: 2026-09-08
related: [backend-python-language-mutable-state-traps, backend-python-language-dict-subclass-attribute-loss-on-copy]
---

# Iterative DFS for Graph Traversal Depth Driven by Input Size

## When this applies

You are implementing a DFS-based algorithm in Python (cycle detection, a
dependency/reference graph, compiler diagnostics walking declared-entity
relationships) where the graph's size is driven by user- or module-supplied
content rather than a small fixed set — the traversal depth is not capped by
anything in the language or the domain.

## Do this

1. **Use an explicit frame stack, not Python's call stack, whenever depth
scales with input.** `sys.getrecursionlimit()` documents that the limit
"prevents infinite recursion from causing an overflow of the C stack and
crashing Python" — a legitimate, non-infinite traversal that happens to be
deep hits the same wall as a bug would.
2. **Replace the recursive call with a stack of `(node, child_iterator)`
pairs.** Push a new frame when descending into an unvisited child; when the
current frame's iterator is exhausted, mark the node done (black) and pop.
This reproduces the recursive version's visitation order exactly, including
the point at which a node is finalized.
3. **Keep white/gray/black coloring (or an equivalent visited/in-progress/done
set) on the explicit stack version** — a gray node reached again is the
cycle; a black node reached again is safe cross-root memoization. The
iterative rewrite does not change this logic, only where the "call stack"
lives.
4. **When you are tempted to raise `sys.setrecursionlimit(N)` instead, rewrite
iteratively.** The documentation warns raising it "should be done with care,
because a too-high limit can lead to a crash" — it trades a catchable
`RecursionError` for an uncatchable interpreter crash once the C stack (a
separate, platform-dependent limit) is exhausted.

## Edge cases

| Case | Then |
|------|------|
| Target is CPython 3.12+ | The recursion limit "now applies only to Python code" (builtins are protected by a separate mechanism) — a pure-Python recursive DFS is still Python code, so the trap and the fix are unchanged |
| The graph is guaranteed small by a hard schema limit (not by convention) | Recursive DFS is fine — state the limit and why it holds, so a later change to the schema is the trigger to revisit |
| Traversal needs to return a value assembled bottom-up (not just visit/color) | Push return values on a parallel results stack keyed by node, popped and combined when the node's frame pops — the iterative shape still supports post-order aggregation |

## Instead of

| If you are about to | Do this instead | Why |
|---------------------|-----------------|-----|
| Write a recursive DFS over a graph whose size is not bounded by the language/domain | An iterative DFS with an explicit `(node, child_iterator)` frame stack | Python's default recursion limit is 1000 frames; a chain of ~500+ nodes reaches it, and the failure mode is a crash on legitimate input, not a bug in the algorithm |
| Raise `sys.setrecursionlimit()` to work around a deep recursive DFS | Rewrite the traversal iteratively | A higher limit only postpones the crash and can turn a catchable `RecursionError` into an interpreter crash when the underlying C stack overflows |

## Sources

- https://docs.python.org/3/library/sys.html#sys.getrecursionlimit — "the maximum depth of the Python interpreter stack. This limit prevents infinite recursion from causing an overflow of the C stack and crashing Python"
- https://docs.python.org/3/library/sys.html#sys.setrecursionlimit — raising the limit "should be done with care, because a too-high limit can lead to a crash"; "If the new limit is too low at the current recursion depth, a RecursionError exception is raised"
- https://docs.python.org/3/whatsnew/3.12.html — "The recursion limit now applies only to Python code. Builtin functions do not use the recursion limit, but are protected by a different mechanism that prevents recursion from causing a virtual machine crash"
- Local reproduction (python3 3.14.6, 2026-09-08): default `sys.getrecursionlimit()` is 1000; a recursive DFS over a 2000-node chain graph raised `RecursionError: maximum recursion depth exceeded`; an iterative version with an explicit frame stack completed the same graph and returned matching cycle-detection output (`True`) against the recursive version on a 500-node cyclic graph within the default limit
- Field application (compiler diagnostics over declared-entity relationships): recursive DFS crashed at ~500 chained nodes; iterative white/gray/black rewrite with an explicit `frames`/`path_stack` list verified correct on synthetic 2000- and 5000-node graphs with `sys.setrecursionlimit(200)` forced low, cross-root memoization preserved, no correctness regression
2 changes: 1 addition & 1 deletion wiki/backend/python/language/mutable-state-traps.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ sources:
- https://docs.python.org/3/library/contextvars.html
- https://docs.python.org/3/library/asyncio-task.html
last_verified: 2026-07-10
related: [backend-common-concurrency-shared-state-and-pools, testing-strategy-import-time-side-effects]
related: [backend-common-concurrency-shared-state-and-pools, testing-strategy-import-time-side-effects, backend-python-language-dict-subclass-attribute-loss-on-copy, backend-python-language-iterative-dfs-for-unbounded-graph-depth]
---

# Mutable State Shared Across Calls and Requests in Python
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ sources:
- https://vladmihalcea.com/optimistic-vs-pessimistic-locking/
- https://www.postgresql.org/docs/current/explicit-locking.html
last_verified: 2026-07-10
related: [databases-transactions-isolation-level-selection]
related: [databases-transactions-isolation-level-selection, backend-python-language-dict-subclass-attribute-loss-on-copy]
---

# Optimistic vs Pessimistic Locking
Expand Down
Loading
Loading