Skip to content

22: Q4=C temp-table-materialised sample (plan) - #30

Merged
wjduenow merged 8 commits into
devfrom
feature/22-temp-table-sample
May 9, 2026
Merged

22: Q4=C temp-table-materialised sample (plan)#30
wjduenow merged 8 commits into
devfrom
feature/22-temp-table-sample

Conversation

@wjduenow

@wjduenow wjduenow commented May 6, 2026

Copy link
Copy Markdown
Owner

Summary

Super-plan for issue #22 — adopt the temp-table-materialised sample strategy for v0.2 sample-mode prune.

Phase: detailing (awaiting review).
Stories: 8 implementation stories + Quality Gate + Patterns & Memory.
Decisions: 11 (DEC-001 through DEC-011) covering compiled-SQL determinism, BigQuery session lifecycle, session_id redaction, conservative-bias failure routing, and total-budget semantics.

What this plan ships (when devolved)

  • New WarehouseAdapter.materialise_sample(...) ABC method; default impl raises MaterialisationNotSupportedError.
  • BigQuery override using sessions: one CREATE TEMP TABLE _sf_sample_<seeded_run_id> per prune_tests call; per-test queries reuse the session via connection_properties so they can read _SESSION._sf_sample_<run_id>.
  • New PruneConfig.sample_strategy: Literal["oneshot", "materialised"] field, default materialised.
  • Two new WarehouseError subclasses (MaterialisationFailedError, MaterialisationNotSupportedError), both → CLI exit-code tier 3.
  • Conservative-bias failure routing: any WarehouseError during materialisation → every candidate test routes to kept-without-evidence with a class+message why field; one PruneEvent per candidate written to JSONL (fail-closed audit preserved).
  • Probe re-run: split into test_sample_rows_cost_baseline_oneshot (regression guard for the 9.92 GB AR-B1 measurement) and test_sample_rows_cost_materialised (asserts < 100 MB per test).
  • Doc updates across 5 surfaces (prune-ops.md, warehouse-adapter-ops.md, two rule files, CLAUDE.md v0.2 surface).

Plan document

See plans/super/22-temp-table-sample.md for:

  • Discovery (codebase findings, 27 convention constraints, 7 scoping questions).
  • Architecture review (six baseline reviews; 4 distinct blockers consolidated).
  • Refinement (11 DECs locked).
  • Detailed story breakdown (10 stories, dependencies, TDD per story).

Architectural commitments preserved

Next steps

  • Review the plan in this PR.
  • On approval, devolve into beads (epic + 10 tasks with dependency graph).

Closes #22 (after implementation lands; this PR is the plan only).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation
    • Added planning documentation for future architectural improvements to the sample handling system.

Super-plan for adopting the temp-table-materialised sample strategy
for v0.2 sample-mode prune. 11 decisions captured, 10 stories ready
for devolve, awaiting review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • feature/*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9d30d899-edd0-47be-aecb-c9375028de99

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment

Thanks for integrating Codecov - We've got you covered ☂️

wjduenow and others added 6 commits May 5, 2026 22:17
Reverses DEC-011's prior "config-only" position based on user feedback
(2026-05-05): operators want to flip between thorough (full-scan) and
cheap (materialised sample) modes per-run without editing
signalforge.yml.

Adds DEC-012 documenting the override-via-model_validate pattern
(mirrors safety-layer.md DEC-018 / cli-layer.md graduated DiffConfig
.render_kind in #9).

Inserts new US-006 story for the CLI flag work; renumbers US-006
through US-010 forward by one to US-007 through US-011. Plan now
ships 9 implementation stories + Quality Gate + Patterns & Memory
(11 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback (2026-05-05): cleanup-failure path should still message the
user with the raw session_id, the manual kill command, and the TTL
fallback note. Don't silently swallow.

Changes:
- DEC-002 revised: __exit__ now belt-and-braces closes the BQ session
  (graduates v0.3 deferral forward; cheap to add now).
- DEC-003 revised: narrow exception to session_id redaction — the raw id
  appears in the cleanup-failure WARNING (single user-facing surface).
- DEC-013 (new): explicit cleanup mechanism via CALL BQ.ABORT_SESSION();
  best-effort with three-layer defence (explicit close + TTL fallback +
  swallow-on-failure).
- DEC-014 (new): cleanup-failure WARNING contract — multi-line shape
  with raw session_id, manual `bq query` command, and "auto-expire in
  Ns" reassurance.
- US-003: 12 new tests covering __exit__ cleanup, success-path INFO,
  failure-path WARNING shape, session_id redaction on happy path,
  state reset in finally. Total 22 tests now (was 12).
- US-004: adds expect_abort_session helper to FakeBigQueryClient (4
  new tests). Total 7 tests now (was 3).
- US-008 probe: third test verifies temp table is gone post-__exit__
  (positive proof of DEC-013).
- US-009 docs: warehouse-adapter-ops.md gets a "Session cleanup &
  manual recovery" section with the manual command template + an
  INFORMATION_SCHEMA query for ops.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User audit (2026-05-05) caught three places where the cleanup amendment
didn't fully land:

1. US-005 had no test pinning the orchestrator's use of `with adapter:`.
   Without that, BigQueryAdapter.__exit__ never fires and DEC-013 cleanup
   is unreachable from the orchestrator. Added three new tests:
   test_prune_tests_uses_adapter_as_context_manager,
   test_prune_tests_adapter_exit_fires_after_normal_completion,
   test_prune_tests_adapter_exit_fires_after_materialisation_failure.

2. US-009 surface 4 (.claude/rules/warehouse-adapters.md) only mentioned
   the session-state pattern. Extended to call out a "Best-effort cleanup
   in __exit__ with user-actionable failure WARNING" sub-section
   covering DEC-013/DEC-014 verbatim, so v0.3 stateful-adapter work
   inherits the canonical reference.

3. US-011 Patterns & Memory had no entry for the cleanup pattern.
   Added entry #5: best-effort cleanup with user-actionable WARNING as
   a reusable project pattern, contrasted with safety-layer.md DEC-011's
   fail-closed-on-primary-work pattern (cleanup boundary vs primary
   boundary). Renumbered the bd remember entry to #6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User audit asked us to double-check how cleanup works and what we tell
users. Six fixes:

G1 — Factual error: BQ assigns the session_id server-side; we capture
it from job.session_info.session_id after .result(). Plan previously
said we mint via uuid4().hex. DEC-002 + US-003 description corrected;
note added on run_id vs session_id (distinct identifiers).

G2 — Missing degraded-run WARNING from orchestrator. When materialise
fails and all tests route to kept-without-evidence, the only signal
was N identical `why` fields buried in the diff. DEC-009 now requires
a single stderr WARNING (lazy-format JSON) at the head of the
conservative-bias routing path. US-005 adds two pinning tests.

G3 — TTL semantics overstated. BigQuery sessions have a server-managed
max lifetime (~24h); ttl_seconds is OUR-side hint to the WARNING text,
not a BQ knob. DEC-013 clarified.

G4 — Pinned that --quiet does NOT suppress the cleanup-failure WARNING
(operator-actionable; deliberate non-suppressible). DEC-014 extended.

G5 — prune_tests docstring requirement: callers MUST use the adapter
inside `with adapter:`. Without it, no explicit BQ.ABORT_SESSION() call
fires; cleanup falls back to BigQuery's server-side timeout. CLI's
cmd_generate already complies; notebook/script callers responsible.
US-005 "Done when" extended.

G6 — docs/cli-ops.md 5-surface parity: added three WARNING surfaces
(cleanup-failure, materialisation-failure, budget-exceeded) to a
"Stderr shapes" section so CI parsers and downstream tooling have one
stable reference. US-009 surface count: 5 -> 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Created bd epic SignalForge-6tv (external-ref gh-22) with 11 child
tasks SignalForge-6tv.1 through .11. Wired 26 dependency edges per
the plan's "Depends on" graph. US-001 is the sole ready task on
devolve; all others blocked until their predecessors close.

Plan phase advanced from approved -> devolved. Beads manifest table
appended to plan document.

Note: bd dolt auto-push warnings throughout creation are non-fatal
(dolt remote sync issue, not a local-write failure). bd dolt push
will be run separately to reconcile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wjduenow
wjduenow marked this pull request as ready for review May 8, 2026 16:58
@wjduenow
wjduenow requested a review from Copilot May 8, 2026 16:58

Copilot AI 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.

Pull request overview

Adds a super-plan for Issue #22 describing the v0.2 shift to a temp-table-materialised sampling strategy (Q4=C) to amortise BigQuery sampling costs across candidate tests, including proposed API seams, error handling, testing, and documentation updates.

Changes:

  • Introduces a detailed planning document covering architecture decisions (DECs), implementation stories, and acceptance criteria for materialised sampling.
  • Specifies BigQuery session-based temp table materialisation mechanics, conservative-bias failure routing, and CLI/config surface changes.
  • Defines a test and docs rollout plan (including probe split and ops documentation updates) to validate the cost reduction and operational behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +9 to +13
- **Phase:** devolved
- **PR:** <https://github.com/wjduenow/SignalForge/pull/30>
- **Approved:** 2026-05-07 by wjduenow
- **Devolved:** 2026-05-07
- **Beads epic:** `SignalForge-6tv`
- **Approved:** 2026-05-07 by wjduenow
- **Devolved:** 2026-05-07
- **Beads epic:** `SignalForge-6tv`
- **Worktree:** `/Users/wesduenow/Projects/worktrees/SignalForge/22-temp-table-sample`
Comment on lines +237 to +239
### US-002 — `WarehouseAdapter.materialise_sample` ABC method + default impl

**Description.** Add abstract method `materialise_sample(table, n, *, partition_filter=None, ttl_seconds=3600) -> TableRef` to `WarehouseAdapter`. The default impl raises `MaterialisationNotSupportedError`; subclasses override (BigQuery in US-003). Method is NOT `@abstractmethod` because the default impl IS the v0.2 behaviour for non-BQ adapters.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
plans/super/22-temp-table-sample.md (3)

158-159: 💤 Low value

Update DEC-007 to account for three probe tests.

DEC-007 states "Probe ships as two @pytest.mark.bigquery tests", but US-008 (line 446) correctly describes three tests: the two probe tests (oneshot baseline + materialised target) plus a third cleanup-verification test per DEC-013.

Consider amending DEC-007 to say "Probe ships as two cost-measurement tests plus one cleanup-verification test" for alignment with the final US-008 scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plans/super/22-temp-table-sample.md` around lines 158 - 159, Update DEC-007
to reflect three probe tests instead of two: change the wording to state that
Probe ships as two cost-measurement tests plus one cleanup-verification test so
it aligns with US-008 and DEC-013; ensure the description mentions the existing
test names (test_sample_rows_cost_baseline_oneshot,
test_sample_rows_cost_materialised) and the additional cleanup-verification test
to avoid ambiguity.

176-182: 💤 Low value

Add language specifier to fenced code block.

The DEC-014 WARNING template block (lines 176-182) lacks a language specifier, triggering markdownlint warning MD040. Add ```text or ```plaintext to the opening fence for proper syntax highlighting and markdown compliance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plans/super/22-temp-table-sample.md` around lines 176 - 182, The fenced code
block in the DEC-014 WARNING template block is missing a language specifier and
triggers markdownlint MD040; update the opening fence of that block (the
triple-backtick that precedes "BigQuery session cleanup failed...") to include a
plain text language such as "text" or "plaintext" (i.e., change ``` to ```text)
so the block is properly tagged and the MD040 warning is resolved.

263-263: 💤 Low value

Optional: Consider tightening inline code spans to reduce markdown lint warnings.

Line 263 triggers nine markdownlint MD038 warnings for spaces inside code spans (e.g., <captured> instead of <captured>). While the spaces improve readability for placeholder values, removing them would align with strict markdown conventions.

This is cosmetic and not blocking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plans/super/22-temp-table-sample.md` at line 263, Summary: The markdown has
MD038 lint warnings due to spaces inside inline code spans; remove the extra
spaces. Fix: edit the doc text around the BigQuery sample materialisation
description and remove leading/trailing spaces inside inline code spans (e.g.,
change backticks wrapping ` <captured> ` to `<captured>`, ` _sf_sample_<run_id>
` to `_sf_sample_<run_id>`, ` job.session_info.session_id ` to
`job.session_info.session_id`, ` self._active_session_id ` to
`self._active_session_id`, ` _SESSION._sf_sample_<run_id> ` to
`_SESSION._sf_sample_<run_id>`, and any JSON/code examples like ` {"model": ...,
"sample_rows": ..., "session_id_hash": blake2b-4(...), "run_id": ...,
"duration_ms": ...} ` to remove surrounding space) so all inline code spans have
no interior leading/trailing spaces and the MD038 warnings are resolved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plans/super/22-temp-table-sample.md`:
- Line 144: DEC-001's blake2b digest size is misstated: update the spec so the
hash length and description match the project's convention used elsewhere;
either change `blake2b-12(...)` to `blake2b-8(...)` with "(16-hex output)"
(recommended) or keep `blake2b-12(...)` and correct the note to "(24-hex
output)"; ensure the `run_id` formula and the temp-table name template
`_sf_sample_<run_id>` reflect the chosen digest size so downstream references
(US-003, snapshot fixtures, and `compiled_sql_hash` expectations) stay
consistent.

---

Nitpick comments:
In `@plans/super/22-temp-table-sample.md`:
- Around line 158-159: Update DEC-007 to reflect three probe tests instead of
two: change the wording to state that Probe ships as two cost-measurement tests
plus one cleanup-verification test so it aligns with US-008 and DEC-013; ensure
the description mentions the existing test names
(test_sample_rows_cost_baseline_oneshot, test_sample_rows_cost_materialised) and
the additional cleanup-verification test to avoid ambiguity.
- Around line 176-182: The fenced code block in the DEC-014 WARNING template
block is missing a language specifier and triggers markdownlint MD040; update
the opening fence of that block (the triple-backtick that precedes "BigQuery
session cleanup failed...") to include a plain text language such as "text" or
"plaintext" (i.e., change ``` to ```text) so the block is properly tagged and
the MD040 warning is resolved.
- Line 263: Summary: The markdown has MD038 lint warnings due to spaces inside
inline code spans; remove the extra spaces. Fix: edit the doc text around the
BigQuery sample materialisation description and remove leading/trailing spaces
inside inline code spans (e.g., change backticks wrapping ` <captured> ` to
`<captured>`, ` _sf_sample_<run_id> ` to `_sf_sample_<run_id>`, `
job.session_info.session_id ` to `job.session_info.session_id`, `
self._active_session_id ` to `self._active_session_id`, `
_SESSION._sf_sample_<run_id> ` to `_SESSION._sf_sample_<run_id>`, and any
JSON/code examples like ` {"model": ..., "sample_rows": ..., "session_id_hash":
blake2b-4(...), "run_id": ..., "duration_ms": ...} ` to remove surrounding
space) so all inline code spans have no interior leading/trailing spaces and the
MD038 warnings are resolved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 156d7575-4661-4f57-908f-fc75285aab19

📥 Commits

Reviewing files that changed from the base of the PR and between 2b4db07 and 3cc8ab1.

📒 Files selected for processing (1)
  • plans/super/22-temp-table-sample.md


### Decisions

- **DEC-001 — Compiled-SQL determinism via seeded run_id (R1=B).** `run_id = blake2b-12(model.unique_id + signalforge_version + sample_size + canonical_json(partition_filter))` (16-hex output). Temp-table name = `_sf_sample_<run_id>`. Same input → byte-equal compiled SQL across runs; `compiled_sql_hash` invariant preserved unchanged. Rationale: simpler than snapshot normalisation; sessions provide namespace isolation so two concurrent runs on the same model don't collide on the temp-table identifier.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for existing blake2b usage patterns to determine the project's digest-size convention
rg -nP 'blake2b' --type=py --type=md -C2

Repository: wjduenow/SignalForge

Length of output: 50378


🏁 Script executed:

#!/bin/bash
# Read the target file to verify line 144 and context
sed -n '140,160p' plans/super/22-temp-table-sample.md

Repository: wjduenow/SignalForge

Length of output: 4270


Fix blake2b digest size specification in DEC-001.

Line 144 specifies blake2b-12(...) with "(16-hex output)", but the project's established convention is blake2b-N → 2N hex characters (confirmed by DEC-003's blake2b-4 → 8 hex chars). Thus blake2b-12 produces 24 hex chars, not 16.

Correct to either:

  • blake2b-8(...) with "(16-hex output)" — recommended, matches the codebase pattern for deterministic hashes, or
  • blake2b-12(...) with "(24-hex output)"

This specification drives the temp-table naming in US-003 and snapshot fixtures; the mismatch will cause implementation misalignment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plans/super/22-temp-table-sample.md` at line 144, DEC-001's blake2b digest
size is misstated: update the spec so the hash length and description match the
project's convention used elsewhere; either change `blake2b-12(...)` to
`blake2b-8(...)` with "(16-hex output)" (recommended) or keep `blake2b-12(...)`
and correct the note to "(24-hex output)"; ensure the `run_id` formula and the
temp-table name template `_sf_sample_<run_id>` reflect the chosen digest size so
downstream references (US-003, snapshot fixtures, and `compiled_sql_hash`
expectations) stay consistent.

@wjduenow
wjduenow merged commit 8840a9b into dev May 9, 2026
6 checks passed
wjduenow added a commit that referenced this pull request May 9, 2026
Resolves conflict in plans/super/22-temp-table-sample.md by keeping
the feature branch's post-review corrections — dev brought in PR #30
which is the original (pre-review) plan content; the feature branch
already corrected the same five locations in response to Copilot +
CodeRabbit feedback (run_id recipe, CTAS shape, two-part _SESSION
form, MD040 fenced-block lang tag).

No code conflicts — only the plan file diverged. Validation green
post-merge: ruff / format / pyright / pytest (1459 passed).
wjduenow added a commit that referenced this pull request May 9, 2026
* 22: import plan from feature/22-temp-table-sample for impl branch

Path B per super-plan workflow: implementation branches off `dev` while
plan PR #30 stays open on feature/22-temp-table-sample. Cherry-pick the
plan file (squashed; full history remains on the plan PR) so worker
subagents in `bd worktree` worktrees can read DEC-001 through DEC-014
when implementing US-001 through US-011.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* SignalForge-6tv.1: PruneConfig.sample_strategy + MaterialisationFailedError + MaterialisationNotSupportedError

* SignalForge-6tv.2: WarehouseAdapter.materialise_sample ABC method (US-002)

Adds the materialise_sample(table, n, *, partition_filter=None,
ttl_seconds=3600) -> TableRef method to WarehouseAdapter per DEC-004 of
plans/super/22-temp-table-sample.md. Default impl raises
MaterialisationNotSupportedError (DEC-008) — deliberately NOT decorated
@AbstractMethod because the typed raise IS the v0.2 contract for non-BQ
adapters; concrete adapters override (BigQuery in US-003).

Two TDD tests pin the contract:
- default impl raises MaterialisationNotSupportedError carrying the
  DEC-006 remediation text verbatim (locked operator-facing surface)
- inspect.signature pins kw-only separator, defaults, and TableRef
  return annotation; drift on any one fails the test loud

Traces: DEC-004, DEC-006, DEC-008.

Validation passes (ruff + ruff format + pyright + pytest with the 5
documented deselects: 7th AST scan blocked by US-007 + 4 pre-existing
symlink-loop env failures).

* SignalForge-6tv.7: register Materialisation* errors in CLI exit-code table

Lands the two materialisation-seam typed errors (US-001) in the
``signalforge.cli._helpers._EXCEPTION_TO_EXIT_CODE`` mapping at tier 3
(external-dep / fail-closed) per DEC-008 of US-007 in
``plans/super/22-temp-table-sample.md``. ``MaterialisationFailedError``
wraps any SDK / network / quota failure during the per-run materialise
query (BigQuery CTAS / equivalent); ``MaterialisationNotSupportedError``
is the ``WarehouseAdapter`` ABC default-impl raise that fires when a
non-BigQuery v0.2 adapter has not overridden ``materialise_sample``.
Both are tier-3 inheritances from ``WarehouseError`` but get explicit
per-class entries so the 7th AST scan
(``tests/test_audit_completeness.py::test_every_typed_error_is_in_exit_code_mapping_table``)
passes — the scan asserts every concrete ``*Error`` declaration in any
``src/signalforge/*/errors.py`` appears as its own key in the mapping
(per DEC-024 of #9 / ``.claude/rules/cli-layer.md``).

Tests:

* Adds explicit per-class branches for both errors in
  ``_construct_exception`` so the construction shape is documented at
  the test seam (``MaterialisationFailedError`` follows the ``cause=``
  kwarg pattern; ``MaterialisationNotSupportedError`` takes a
  positional adapter name). The parametrized contract picks the new
  entries up automatically because ``_PARAMS`` derives from
  ``_EXCEPTION_TO_EXIT_CODE.items()``.

* ``test_materialisation_failed_error_maps_to_tier_3`` and
  ``test_materialisation_not_supported_error_maps_to_tier_3`` —
  non-parametrized callouts mirroring the precedent set by
  ``test_table_not_found_error_exits_tier_two`` so a future
  tier-change diff is easy to read in code review.

* ``test_audit_completeness_scan_passes_for_new_errors`` — pins the
  mapping membership + tier assignment at the per-class level so a
  regression names the offending class up front instead of only in the
  broader 7th-scan failure list.

Validation: ``ruff check`` clean, ``ruff format --check`` clean,
``pyright`` 0/0/0, ``pytest`` 1387 passed (2 skipped platform-specific,
15 deselected: the 4 known-environmental symlink-loop tests plus the
always-deselected ``bigquery`` / ``anthropic`` / ``cli_subprocess``
markers). Coverage 94.91% (above the 80% floor).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* SignalForge-6tv.3: BigQueryAdapter.materialise_sample (US-003 of #22)

Implements DEC-001/002/003/013/014 of plans/super/22-temp-table-sample.md.

Production:
- BigQueryAdapter.materialise_sample(table, n, *, partition_filter=None,
  ttl_seconds=3600) -> TableRef. Computes deterministic run_id =
  blake2b-8(table.qualified_name + signalforge_version + n +
  canonical_json(partition_filter)) for snapshot determinism (DEC-001;
  16 hex chars so the temp-table name passes validate_identifier).
- Issues CREATE TEMP TABLE _sf_sample_<run_id> AS SELECT ... with
  QueryJobConfig(create_session=True, use_query_cache=False, ...
  stage="warehouse_sample_materialise"). Captures the BQ-assigned
  session_id from job.session_info.session_id (NOT minted by us).
- Stores _active_session_id + _session_started_at + _session_ttl_seconds;
  run_test_sql now threads connection_properties=[ConnectionProperty(
  key="session_id", value=...)] when active.
- One INFO log on success: {"table","sample_rows","session_id_hash":
  blake2b-4(session_id),"run_id","duration_ms"} — DEC-003 redaction
  (raw session_id never leaks on the happy path).
- __exit__ extends DEC-013/014 cleanup: CALL BQ.ABORT_SESSION() in the
  active session; success → INFO {"session_id_hash","ttl_remaining_
  seconds"}; failure → multi-line WARNING with raw session_id + manual
  bq command + "auto-expire in <N>s" line (DEC-014, the deliberate
  exception to DEC-003 — the bq command is unconstructable without
  the raw id). Always resets state in finally.
- SDK noise contained in _client.py: _make_query_job_config now accepts
  create_session= and session_id= kwargs.

Tests (22 new):
- tests/warehouse/test_materialise_sample.py — covers TableRef shape,
  identifier validation, run_id determinism, version-bump invalidation,
  byte-equal CTAS SQL fixture, stage label, partition filter placement,
  create_session config, MaterialisationFailedError wrapping,
  run_test_sql session routing, INFO redaction, use_query_cache invariant,
  __exit__ cleanup happy/failure paths, raw-session-id-in-WARNING,
  manual-bq-command line, TTL line, exception-class-name, state reset,
  no-WARNING-on-success.
- tests/fixtures/warehouse/sample_materialise_v1.sql — pinned CTAS
  template with {run_id} placeholder (substituted at test-time so
  signalforge.__version__ bumps don't require fixture refresh).

Validation: ruff check + ruff format --check + pyright + pytest pass
(1412 passed, 2 skipped, 15 deselected per the 4 environmental symlink
deselects). 7th AST scan still green; logger grep gate still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* SignalForge-6tv.4: FakeBigQueryClient.expect_materialise_sample + expect_abort_session (US-004 of #22)

Two purpose-built helpers on FakeBigQueryClient mirroring the production
surface for BigQueryAdapter.materialise_sample (CTAS into _SESSION) and
__exit__'s CALL BQ.ABORT_SESSION cleanup path. Each helper consumes one
matching call; non-matching calls raise the standard "unexpected ..."
AssertionError. Both queues short-circuit only when populated so US-003's
existing raw expect_query matchers continue to work unchanged.

expect_materialise_sample(source_ref, sample_size, partition_filter=None,
*, returns: TableRef | Exception) — matches a CREATE TEMP TABLE _sf_sample_
SQL referencing source_ref's qualified name + LIMIT <n> + (when registered)
the rendered partition_filter fragment. Success returns a job with a
populated session_info.session_id (deterministic, derived from the returned
TableRef name) so production captures it via job.session_info after
.result(). Exception returns propagate, driving the
MaterialisationFailedError wrapping path.

expect_abort_session(session_id, *, returns=None | Exception) — matches a
CALL BQ.ABORT_SESSION query whose job_config carries the registered
session_id via connection_properties. returns=None simulates success;
returns=Exception drives the DEC-014 swallow-and-warn WARNING path on
__exit__.

Eight meta-tests pin the contract: consume-one-call + assertion-error on
second call (both helpers); returns=Exception propagation (both helpers);
assert_all_expectations_met fails on unconsumed materialise registration;
session_id mismatch on abort raises loudly; happy-path returns=None on
abort iterates an empty rowset; partition_filter contract — registering
a filter requires the matched SQL to carry the rendered fragment.

Traces: R-TEST-3, DEC-013 of plans/super/22-temp-table-sample.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* SignalForge-6tv.5: prune orchestrator dispatch on sample_strategy + conservative routing (US-005 of #22)

* SignalForge-6tv.6: --scope and --sample-strategy flags on signalforge generate (US-006 of #22)

Both flags are optional and independent (set one, the other, both, or
neither). Override application uses PruneConfig.model_validate(...) so
every Pydantic validator re-runs (DEC-012); never model_copy(update=...)
because that path silently skips @model_validator(mode='after'). Mirrors
SafetyPolicy.with_mode (DEC-018 of safety-layer.md) and DiffConfig.render_kind
graduation in #9 (DEC-020 of cli-entrypoint).

Multi-surface parity (cli-layer.md): help text, add_parser docstring,
cmd_generate docstring, module docstring, test names, and the DEC trace
all aligned in one commit. Argparse choices rejection produces tier-2
exit (no traceback per DEC-016).

Eight tests pin DEC-011 + DEC-012:
- test_generate_scope_flag_overrides_config_value
- test_generate_sample_strategy_flag_overrides_config_value
- test_generate_both_flags_independent
- test_generate_no_flag_uses_config_value
- test_generate_invalid_scope_returns_exit_2
- test_generate_invalid_sample_strategy_returns_exit_2
- test_generate_help_text_lists_new_flags
- test_generate_override_re_runs_pydantic_validators

Traces: DEC-011, DEC-012 of plans/super/22-temp-table-sample.md.

Files: src/signalforge/cli/generate.py, tests/cli/test_generate.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* SignalForge-6tv.8: probe re-run scaffolding + cleanup verification (US-008 of #22)

Restructure tests/warehouse/test_sample_cost_probe.py into three
@pytest.mark.bigquery tests (DEC-007 + DEC-013 of plans/super/22-temp-table-sample.md):

* test_sample_rows_cost_baseline_oneshot — preserves AR-B1's 9.92 GB
  measurement as a regression guard. Asserts bytes_billed >= _BYTES_WARN_AT
  (cost cliff genuinely exists on the legacy path) AND bytes_billed <
  _BYTES_CEILING (sanity ceiling).
* test_sample_rows_cost_materialised — issue's primary acceptance
  criterion. Drives materialise_sample + a per-test query through the
  adapter; asserts per-test bytes_billed < 100 MB.
* test_materialised_session_cleaned_up_after_exit — positive proof of
  DEC-013. After __exit__ fires, querying the _SESSION._sf_sample_<run_id>
  temp table by name fails with a GoogleAPIError (NotFound / "session not
  found" / "table not found"). A buggy implementation that no-op'd
  CALL BQ.ABORT_SESSION() would make the query SUCCEED and pytest.raises
  would fail with "DID NOT RAISE", so the test cannot pass on a broken
  cleanup path.

All three are SF_RUN_BQ-gated (truthy values: 1/true/yes/on); default
pytest skips them via the addopts marker exclusion. Maintainer runs
`SF_RUN_BQ=1 pytest -m bigquery tests/warehouse/test_sample_cost_probe.py
--no-cov` before declaring the PR ready (--no-cov per testing-signal.md
Coverage section, since --cov-fail-under in addopts would otherwise fail
the marker-only run).

Adds two default-collected scaffolding tests:

* test_probe_module_imports_and_exposes_three_test_functions — pins the
  module shape and asserts each live-BQ test carries the bigquery marker
  via pytestmark introspection.
* test_probe_constants_unchanged — pins _BYTES_CEILING (5 GB),
  _BYTES_WARN_AT (500 MB), and _BYTES_PER_TEST_TARGET (100 MB) so a
  silent threshold edit can't soften the regression contract.

Validation: ruff check . && ruff format --check . && pyright && pytest
all pass; 1437 tests pass, 17 deselected (markers), 2 skipped (platform).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* SignalForge-6tv.9: documentation surfaces (6-surface parity)

Update six documentation surfaces in lockstep for the v0.2 temp-table
materialised-sample work tracked in plans/super/22-temp-table-sample.md
(US-009). All updates are docs-only; no production code changes.

- docs/prune-ops.md: document `sample_strategy` config field; add
  post-Q4=C cost subsection with TBD placeholders for US-010 quality
  gate; add audit reading guide distinguishing materialised vs.
  oneshot `compiled_sql` shapes.
- docs/warehouse-adapter-ops.md: add `warehouse_sample_materialise`
  stage label; document `materialise_sample` ABC method, BigQuery
  session-state pattern, v0.2 → v0.3 migration story; add Session
  cleanup & manual recovery section covering the three-layer cleanup
  model (DEC-013) and the verbatim manual `bq query` command (DEC-014);
  add INFORMATION_SCHEMA.JOBS_BY_PROJECT query template for spotting
  orphan sessions; extend error-reference table.
- .claude/rules/prune-engine.md: add v0.2 reservations / additions
  section covering `sample_strategy`, the two new typed errors,
  `_SESSION._sf_sample_<run_id>` audit signal, conservative-bias
  routing + degraded-run WARNING (DEC-009), total-budget-includes-
  materialisation invariant (DEC-010), and the context-manager
  requirement (DEC-013).
- .claude/rules/warehouse-adapters.md: document `materialise_sample`
  ABC + BigQuery session-state pattern; add Best-effort cleanup in
  `__exit__` sub-section with WARNING shape verbatim; cross-reference
  `safety-layer.md` DEC-011 for the primary-work fail-closed contrast.
- CLAUDE.md: amend "Public API surface (v0.1)" to "(v0.1 + v0.2
  additions)"; list four new exports under v0.2.
- docs/cli-ops.md: add Stderr shapes (WARNING) section with three
  WARNING entries (cleanup-failure, materialisation-failure /
  degraded-run, budget-exceeded) for operator parity.

Validation passes: ruff check + ruff format --check + pyright + the
1445-test default suite (with the four pre-existing symlink-loop
deselects from the worktree pattern). No production code touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* SignalForge-6tv.10: Quality gate — fix bugs from code review

Four concerns surfaced by the multi-lens reviewer pass; all four
addressed inline (no blockers).

1. docs/cli-ops.md "Runtime knob flags" missing --scope and
   --sample-strategy entries (5-surface parity violation per
   cli-layer.md). Added two bullets mirroring the help text in
   cli/generate.py.

2. .claude/rules/prune-engine.md run_id digest claim was wrong:
   said "blake2b-12(model.unique_id + ...)" but production uses
   "blake2b(table.qualified_name + ..., digest_size=8)" (16 hex
   chars). Updated rule to match the implementation; preserves
   the "16-hex" claim consistently.

3. tests/llm/test_logger_grep_gate.py extended to cover
   src/signalforge/warehouse/ (was 6 dirs; now 7). US-003
   introduced new logger calls in the warehouse layer; existing
   calls comply with lazy-format pattern, but adding the gate
   prevents future regressions.

4. docs/warehouse-adapter-ops.md "Session cleanup & manual
   recovery" section gets a new "Edge case: SDK returns
   session_info=None" sub-section documenting the v0.3-tracked
   path where BQ creates a session server-side but the SDK
   doesn't surface the id (would orphan until BQ's own timeout).

Out-of-session deferred to maintainer:
- 3 more code-reviewer passes (skill says 4; did 1 multi-lens).
- CodeRabbit review (PR-time, automatic when PR opens).
- Maintainer probe-run: SF_RUN_BQ=1 pytest -m bigquery
  tests/warehouse/test_sample_cost_probe.py --no-cov against a
  real BQ project.
- Cost-figure substitution in docs/prune-ops.md (replace four
  <TBD: ...> placeholders with the maintainer's measured figures).

Validation: 1445 passed, 2 skipped, 17 deselected (5 known:
4 environmental symlink-loop + the AST scan was the v0.2 incomplete
state which closed when US-007 landed). ruff + format + pyright
all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* SignalForge-6tv.11: distil patterns into rule files + bd remember (US-011 of #22)

Rule-file additions (generic, durable lessons — not duplicating US-009's
issue-specific notes):

- warehouse-adapters.md: "Session/connection state on the adapter" generic
  pattern (DEC-002 of #22 generalised) for v0.3 adapters needing per-call
  state primitives; "Cleanup-boundary fail-soft pattern" generic pattern
  (DEC-013/DEC-014 of #22 generalised) with the three-thing WARNING contract
  (identifier + copy-pasteable command + durable fallback) and the
  --quiet-doesn't-suppress invariant.
- safety-layer.md: paragraph near DEC-011 distinguishing primary-work
  fail-closed (this rule) from cleanup-boundary fail-soft (warehouse-adapters
  DEC-013/DEC-014) so a v0.3 maintainer doesn't conflate them.
- prune-engine.md: refines C8 (5-value DropReason taxonomy) with the
  conservative-bias-routing-across-WarehouseError-subclasses paragraph
  (generalises DEC-009 of #22 to any v0.3 warehouse exception); adds the
  5-surface parity rule (rule file / ops doc / CLAUDE.md / test / DEC) at
  the top of the v0.2 reservations section, mirroring cli-layer.md's flag
  parity rule.
- testing-signal.md: one-paragraph note on seeded determinism over
  snapshot normalisation (mirrors DEC-001 of #22 + LLM drafter prompt_version).

bd remember invocations (2): BQ session creation latency observation
(~700ms-2.5s on US-003 worktree builds; informs DEC-010 budget calibration)
and BQ session abort failure rate expectation (rare; revisit DEC-013 if
>1% of prune runs hit the swallow-and-warn path).

Validation: ruff/pyright/pytest all green. 1445 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 22: fix _SESSION qualified-name + record post-Q4=C cost figures

Maintainer probe-run (2026-05-08, billed to duenow-nest) surfaced one
real production bug + three probe-design fixes. All four landed
together so the next probe-run from a clean clone Just Works.

## Production fix

`BigQueryAdapter.materialise_sample` returned
`TableRef(project=client.project, dataset="_SESSION", name=...)`. The
three-part `<project>._SESSION._sf_sample_<run_id>` qualified_name is
rejected by BigQuery even inside the owning session:

    400 Use of _SESSION is not allowed here; reason: invalid

Within a session, the temp table must be referenced as the two-part
`_SESSION._sf_sample_<run_id>`. Fix: return `project=None` so
`TableRef.qualified_name` renders the two-part form.

This bug shipped through US-003's unit tests because `FakeBigQueryClient`
matches on regex shape, not SQL semantics — only a real-BQ run could
catch it. Updated the production code, the matching unit test pin, the
prune fixture's `compiled_sql` references, and the test that uses a
literal `fake_project._SESSION._sf_sample_x` SQL string.

## Probe fixes

1. `_BYTES_CEILING` raised from 5 GB → 15 GB. AR-B1 measured 9.92 GB;
   the original 5 GB ceiling was internally inconsistent with the
   recorded figure (the regression-guard would always fail).

2. `BigQueryAdapter()` instantiations bumped from the 100 MB default
   cap to 20 GB (`_BOOTSTRAP_BYTES_BILLED_CAP`). The 100 MB DEC-005 cap
   blocks even the materialised CTAS bootstrap (it scans the source
   table once, ~10 GB). The probe needs a higher cap to MEASURE
   figures; the production safety net stays at 100 MB for end users.
   v0.3 may want a per-stage cap override; tracked in PR #31's
   maintainer-follow-ups section.

3. Per-test query in `test_sample_rows_cost_materialised` was
   `SELECT COUNT(*) FROM (SELECT * FROM <temp> WHERE FALSE) AS t`. BQ's
   planner short-circuited on `WHERE FALSE` and billed 0 bytes — the
   probe's `int(getattr(...) or -1)` bug then converted 0 to -1
   ("unavailable") and xfailed. Replaced with a representative
   `not_null` test against the `invoice_and_item_number` column +
   fixed the `or -1` bug to treat None as the unavailable sentinel
   (0 is a valid measurement).

## Cost figures recorded in docs/prune-ops.md

- Oneshot baseline: 9,984,540,672 bytes (~9.98 GB) per query — matches
  AR-B1 within 1%.
- Materialisation CTAS: ~9.98 GB once per prune run.
- Per-test on materialised: 10,485,760 bytes (10 MB) — well under the
  100 MB acceptance gate.
- 30-test run: ~10.3 GB end-to-end (vs. ~299 GB on the legacy oneshot
  path) → ~29× cheaper.

Issue #22 acceptance #1 ("per-test bytes_billed < 100 MB without
raising cost_limit_bytes") is satisfied — the per-test queries against
the temp table run cleanly under the 100 MB default cap; only the
maintainer probe needs the bumped cap for the bootstrap measurement.

Validation: ruff + format + pyright + 1445-test pytest all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 22: close Codecov patch-coverage gap on materialise_sample + prune routing

Adds 8 targeted tests covering the 11 patch-attributable lines Codecov
flagged on PR #31:

* materialise_sample fail-loud sizing parity with sample_rows:
  - n <= 0 ValueError pre-call (DEC-008 input validation)
  - UnknownTableSizeError when num_rows is None and no partition filter
  - default-bucket fallback when num_rows is None but partition filter pins cost
  - SamplingRequiresPartitionFilterError when num_rows >= 100M and no partition
* materialise_sample fail-loud post-CTAS guard:
  - MaterialisationFailedError when SDK returns no session_id
* prune engine defence-in-depth + setup-error propagation:
  - invalid-identifier compile result routes to kept-without-evidence
  - WarehouseError during sample-mode size fetch propagates as-is
* TTL helper defensive guard:
  - _compute_ttl_remaining_seconds returns 1 when session state unset

Total signalforge coverage: 95.00% -> 95.30%. Remaining uncovered lines
in bigquery.py / prune/engine.py are all pre-existing code from issue #3,
outside this PR's diff hunks; no patch lines are uncovered after this
commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 22: address PR review — fix drift, route materialise_sample errors through map_bq_exception

Code fixes:
* materialise_sample wraps SDK exceptions through map_bq_exception
  before raising MaterialisationFailedError so cause carries the stable
  warehouse-error surface (e.g. WarehouseAuthError) instead of raw SDK
  text. The original SDK exception is preserved on __cause__ via the
  raise-from chain (CodeRabbit).
* prune_tests docstring corrected: the engine OWNS the with-adapter
  block; library callers MUST pass a non-entered adapter and MUST NOT
  wrap the call themselves (Copilot).
* materialise_sample Returns: docstring corrected — the returned
  TableRef is project=None / dataset=_SESSION (two-part qualified_name);
  the three-part <project>._SESSION.<name> form is the rejected one
  (Copilot).
* tests/warehouse/_fake.py _qualified_name_substring docstring
  clarified — the existing period-prefixed substring IS correct (it
  matches the suffix of the production three-part backtick-quoted
  form); Copilot's two-part suggestion would actually break the match.

Test fixes:
* test_materialised_session_cleaned_up_after_exit: the cleanup
  verification query now reuses the captured session_id via
  connection_properties so the test would fail if cleanup leaked
  (CodeRabbit). Without this, the post-exit query failed for an
  unrelated reason (no session on the wire) and the test passed even
  on a broken cleanup.
* test_materialise_sample_wraps_warehouse_sdk_errors: assertion
  follows the new map_bq_exception contract (cause is the typed
  warehouse error; __cause__ preserves the raw SDK exception).
* test_sample_cost_probe.py:495 comment corrected to two-part
  _SESSION._sf_sample_<run_id> form (Copilot).
* tests/warehouse/test_materialise_sample.py module docstring
  refreshed — US-004 has shipped expect_materialise_sample /
  expect_abort_session helpers; the module continues to use raw
  expect_query intentionally for diagnostic SQL-byte assertions
  (Copilot).

Doc/rule drift cleanup (CTAS shape, _SESSION reference, run_id recipe):
* run_id is blake2b(table.qualified_name + version + str(n) +
  canonical_partition_filter, digest_size=8) — 16 hex chars; corrects
  the stale "blake2b-12 / model.unique_id" recipe.
* CTAS uses bare _sf_sample_<run_id> name; subsequent reads use
  two-part _SESSION._sf_sample_<run_id>. Corrects the stale
  _SESSION._sf_sample_<run_id> CTAS form.
* warehouse_session_abort added to docs/warehouse-adapter-ops.md
  stage-label list.
* AST-scan + logger-grep-gate counts updated (7 scans, 6 dirs as
  of #9) in .claude/rules/prune-engine.md (CodeRabbit).
* DEC-014 cleanup-warning fenced block in plan now carries text
  language tag (CodeRabbit MD040 lint).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wjduenow
wjduenow deleted the feature/22-temp-table-sample branch May 9, 2026 05: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.

3 participants