Skip to content

fix(reporting): validate buyer frozen histories - #1222

Open
bokelley wants to merge 8 commits into
mainfrom
conductor/reporting-buyer-frozen-history-safety
Open

bokelley wants to merge 8 commits into
mainfrom
conductor/reporting-buyer-frozen-history-safety

Conversation

@bokelley

@bokelley bokelley commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Reconciliation could certify incomplete or ambiguous histories and compare an accepted receipt with a newer delivery attempt. The buyer now requires an unchanged, complete authorized frozen periods read before definitive outcomes or missing-obligation claims.

  • Validate immutable cross-page records, counts, ownership, dependency closure, and bounded page/row/byte budgets. Reuse the existing ownership validator and whole-history revision selector.
  • Validate revision and adjustment receipts as exact predecessor chains and use their current leaves. Official revisions take precedence, including those without artifacts. Accepted receipts remain bound to their referenced materialization; current readability is checked separately.
  • Require an accepted current receipt for every applicable adjustment in both reconciliation modes. Delivery-only obligations with missing, rejected, or partially accepted adjustment evidence remain non-definitive even when optional receipt counts are absent.
  • Keep unresolved ownerless statuses and absent applicable optional counts diagnostic. Reject contradictory declared counts even under ambiguous legacy ownership. Use per-read indexes and mutation fingerprints to avoid repeatedly scanning unrelated histories.
  • Add real PostgreSQL seller-projection coverage for both proven and unproven missing-period claims, and load complete page-size-one histories in the publication-clock checks while preserving their clock and schema assertions.

Missing-period proof is intentionally conservative: ExpectedReportingPeriod has no trusted finality requirement, so a response denominator containing only one finality cannot prove an absent period belongs to it. Returned obligations still reconcile normally, and an unproven expected period prevents overall success. This can suppress valid absence claims from complete official-only or snapshot-only configurations.

Migration: load before evaluating; manually assembled or modified ledgers cannot certify completeness. Inconsistent retained evidence now fails earlier and requires a fresh read after correction. Public positional fields and protocols remain compatible; max_bytes is additive. No database migration is needed.

Scope conflicts that previously returned the per-obligation REVISION_SCOPE_MISMATCH reason now abort the read with ReportingReconciliationError (INVALID_REVISION_OWNERSHIP or INVALID_LEDGER_DEPENDENCY). This escalation is intentional: conflicting revision/materialization ownership invalidates the frozen history's partition proof. Callers should handle the typed error and read again after the seller corrects the history.

Validation:

  • make lint typecheck-all validate-generated: passed.
  • Delivery-only adjustment regressions fail before the correction (16 failed, 8 passed), including real MCP/A2A mounts over memory/PostgreSQL. Focused buyer, ownership, consumer-loop, lifecycle, publication-clock, and PostgreSQL tests: 408 passed after the correction.
  • Complete PostgreSQL 16 core conformance after rebase: 2,227 passed, 6 Python 3.10-only installed checks skipped. Process-crash conformance: 7 passed.
  • make test after rebase: 11,999 passed, 2,191 skipped, 9 deselected, 1 xfailed; 81.83% coverage.
  • Repository hooks: passed.
  • actionlint and repository hooks passed for the coverage deadline change. The growing full suite reached 99% before the former 45-minute limit; the coverage step now allows 55 minutes within a 70-minute Python 3.12 job cap, preserving 15 minutes for setup and cleanup.
  • Fresh CI and review are running after the workflow correction. The buyer findings and the workflow headroom thread are addressed and resolved on this PR.

Raw client evidence capture and composition of submission intents in the later facade remain follow-up work. Strict rc6 wire validation still rejects legacy received statuses without an owner; diagnostic tolerance of already-typed legacy records does not establish mounted wire compatibility.

Refs #1167
Refs #1172

Open workspace in Conductor

@bokelley
bokelley force-pushed the conductor/reporting-buyer-frozen-history-safety branch from 771e70f to a0a5aaa Compare September 26, 2026 14:59
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Sep 26, 2026

@aao-secretariat aao-secretariat 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.

Ladon verdict: Approve

Approve — fail-closed buyer frozen-read reconciliation rewrite in _reconcile.py plus adversarial test coverage.

Checked:

  • Definitive/missing-period claims correctly gated on a fingerprinted complete read.
  • Receipt-chain and legacy-fanout paths fail closed and are work-bounded.
  • Wire secrets redacted from all error surfaces.
  • Three new adversarial conformance/unit test files exercise the new branches.

No Critical/High/Medium findings. No CI gate was disabled, no public-surface break, no type-layering or generated-code violations.

Nit (non-blocking): the full make test and complete PG core conformance lane are still marked pending in the PR body — ensure both are green before merge.

Decision path: no blocking findings; gated_paths false; high_risk false; author not on a no-auto-approve team; zero medium findings — falls through to row 9 (approve). Note review_decision is REVIEW_REQUIRED, but that only forces escalate when gated_paths is true, which it is not here.

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Sep 26, 2026

@aao-secretariat aao-secretariat 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.

Ladon verdict: Approve

Approve — test-only refactor with no blocking or medium findings.

This PR reroutes publication-clock checks in tests/conformance/reporting/test_reporting_publication_time.py through the public load_reporting_ledger path instead of a hand-assembled ReportingLedger. Per-page reporting-revision schema validation and clock assertions are preserved; outcome.ledger.revisions is valid and correctly typed.

Checked against the decision table:

  • No critical/high findings (row 1 n/a).
  • gated_paths is false (row 2 n/a).
  • high_risk is false (rows 3/5 n/a).
  • No medium findings, so no schema/data-loss/infra escalation (row 4 n/a).
  • Prior decision was approve, not escalate (row 6 n/a).
  • No no-auto-approve team match (row 7 n/a).
  • Zero medium findings, well under the three-medium threshold (row 8 n/a).

Falls through to row 9 → approve. The sole reviewer note is a Low observation that the request's max_results:1 pagination hint is stripped by the loader and thus inert — non-blocking. Clean pass.

@bokelley bokelley left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

REQUEST_CHANGES

(Filed as a review comment: GitHub refuses a formal REQUEST_CHANGES because the reviewer session and the PR author share the bokelley account. Treat this as blocking — the P1 below needs a fix on this branch.)

Reviewed at 737c3698 (rebased on 5fd54334). Ran locally: make lint OK, make typecheck-all OK, and pytest tests/test_reporting_frozen_read_safety.py tests/test_reporting_frozen_read_corrections.py tests/test_reporting_reconciliation.py tests/conformance/reporting/ -> 2206 passed, 2044 skipped, 0 failed (the skips are the PG lane; CI is green on both PG conformance jobs). The ownerless-status bound (unresolved_by_scope + AMBIGUOUS_CONSUMER_STATUS_OWNERSHIP), the absent-vs-contradicting optional-count split in _obligation_history, the read fingerprint / UNVERIFIED_LEDGER_SNAPSHOT gate, and the _receipt_leaves chain validation all look right and are well covered. Two findings.


P1 — delivery_only obligations with an unreconciled adjustment are reported definitive=True

evaluate_reporting_ledger only consults adjustment receipts inside the consumer_receipt branch (_reconcile.py:1618-1642), and this PR removes the unconditional ADJUSTMENT_RECONCILIATION_REQUIRED reason that main appended for any obligation with adjustment_count or an adjustment against the selected revision.

Net effect: a delivery_only obligation carrying a post-official adjustment that nothing has receipted now yields no reason at all, so both ObligationReconciliation.definitive (not reasons) and the top-level definitive are True — while the seller is simultaneously declaring pending_adjustment_count=1.

_validate_read_ledger does read pending_adjustment_count, but only to bound it against the leaf receipts; outside consumer_receipt the value never becomes a reason.

Repro on this branch:

raw = _history(adjustments=True)      # one post-official adjustment
raw["receipts"] = []
raw["adjustment_receipts"] = []       # nothing has receipted the correction
_counts(raw)
raw["periods"][0].update(
    reconciliation_mode="delivery_only",
    reconciliation_status="not_required",
    receipt_count=None, accepted_receipt_count=None,
    adjustment_receipt_count=None, accepted_adjustment_receipt_count=None,
    pending_adjustment_count=1,       # seller: 1 unreconciled correction
)
result = evaluate_reporting_ledger(await _load(raw), expected_periods=_expected(), now=NOW)
# definitive: True   reasons: ()

On main the same ledger gives ADJUSTMENT_RECONCILIATION_REQUIRED / definitive=False. Telling a buyer the revision is definitive while the seller has published an unreconciled correction to it is the failure mode this PR is otherwise closing.

Either fix works:

  • append a reason for all modes (reuse ADJUSTMENT_RECONCILIATION_REQUIRED, or gate on unaccepted leaves / pending_adjustment_count), or
  • reject adjustments against delivery_only obligations in _index_read_ledger as INVALID_LEDGER_DEPENDENCY.

Please add coverage either way — _partly_owned_legacy_history is the only delivery_only fixture and it pins adjustment_count=0, so this path is currently untested.

P2 — REVISION_SCOPE_MISMATCH is retired and its triggers become whole-ledger exceptions; not mentioned in the description

REVISION_SCOPE_MISMATCH no longer exists anywhere in src/ or tests/. Both of its main triggers are now hard failures in _index_read_ledger: a materialization whose revision does not match its obligation, and two obligations materializing the same revision.

# two obligations materializing the same revision, on this branch:
# ReportingReconciliationError INVALID_REVISION_OWNERSHIP
#   (INVALID_LEDGER_DEPENDENCY without the ownership extension)

On main that returned a usable result with a per-obligation REVISION_SCOPE_MISMATCH reason. Escalating to an exception is defensible for a frozen read, and nothing in-repo or in docs/ consumes the string — but under a fix(reporting): title it is an adopter-visible change from "degraded result" to "no result", so please call it out in the PR description (rule 5) and confirm the escalation is intended.


Non-blocking, no action needed: evaluate_reporting_ledger and reconcile_reporting each re-run the full _validate_read_ledger walk that load_reporting_ledger already did; the association_limit bounds it, so it is cost rather than correctness.

Copy link
Copy Markdown
Contributor Author

P1 is fixed on this branch: the selected revision's adjustment current-leaf check now runs in both reconciliation modes. Missing, rejected, or partially accepted adjustment evidence prevents a definitive result; optional count applicability is unchanged.

Added 20 public loader/evaluator cases and four real MCP/A2A cases over memory/PostgreSQL using a legal analytics delivery-only feed. Before the fix: 16 failed, 8 passed. After the fix: all 24 pass within a 408-test focused run.

P2 is intentional and now explicit in the PR's migration guidance: revision/materialization scope conflicts abort the frozen read with ReportingReconciliationError (INVALID_REVISION_OWNERSHIP or INVALID_LEDGER_DEPENDENCY) instead of returning REVISION_SCOPE_MISMATCH on an otherwise usable ledger.

make lint typecheck-all validate-generated and normal commit hooks passed. Full make test, complete PostgreSQL core conformance, and fresh CI are running.

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Sep 26, 2026

@aao-secretariat aao-secretariat 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.

Ladon verdict: Approve

Approve — clean subsequent pass with no blocking, medium, or unresolved prior findings.

What I checked:

  • The delta in src/adcp/reporting/_reconcile.py:1136 lifts the applicable-corrections evidence requirement out of the consumer_receipt-only branch into a broader if revision: guard, so MISSING_MATCHING_ADJUSTMENT_RECEIPT now correctly fires in delivery_only mode too. This is a correctness tightening.
  • Two well-constructed new tests back the change; test files across the conformance and unit suites were expanded.
  • No public-surface removals/renames/type-signature changes, no discriminated-union fallback removals, no type-system import-layering violations, no generated-code hand-edits, no CI gate disablement, no credentials in ctx_metadata.

Gate checks:

  • No no-auto-approve team match (row 7 N/A).
  • gated_paths: false — row 2 N/A even though review_decision is REVIEW_REQUIRED.
  • high_risk: false — rows 3/5 N/A.
  • Prior decision was approve, so sticky escalation (row 6) N/A.
  • Zero findings — rows 1, 4, 8 N/A.

Falls through to row 9 → approve.

@bokelley bokelley left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

APPROVE

(Filed as a review comment for the same reason as last time: GitHub refuses a formal APPROVE when the reviewer session and the PR author share the bokelley account.)

Re-reviewed at 735168bd. Both findings are resolved.

P1 — fixed

evaluate_reporting_ledger now runs the adjustment-leaf check under its own if revision: block (_reconcile.py:1628-1640) instead of inside the consumer_receipt branch. Verified independently on this branch, not just via the new tests:

case result
delivery_only + unreceipted adjustment, pending_adjustment_count=1 (my original repro) definitive=False, ('MISSING_MATCHING_ADJUSTMENT_RECEIPT',)
same, seller omits pending_adjustment_count definitive=False, ('MISSING_MATCHING_ADJUSTMENT_RECEIPT',)
delivery_only + accepted adjustment leaf definitive=True, () — no false block
delivery_only, no adjustments definitive=True, () — no false block
consumer_receipt + accepted adjustment receipt definitive=True, () — unchanged
consumer_receipt + unreceipted adjustment definitive=False, single reason — no double-append

Since _validate_read_ledger already requires an adjustment's target to be official, and select_reporting_revision rejects multiple officials, an applicable adjustment always targets the selected revision — so this restores main's ADJUSTMENT_RECONCILIATION_REQUIRED conservatism while correctly clearing once the leaf is accepted.

Coverage is better than I asked for. test_delivery_only_adjustments_require_each_accepted_current_leaf is 20 cases (missing / rejected / accepted / accepted-leaf / partial × declared-vs-omitted pending count × implicit-vs-explicit ownership) and validates the fixture against the real core/reporting-obligation.json. test_delivery_only_adjustment_blocks_definitive_without_receipt_counts covers it end to end over MCP and A2A against a real seller projection, asserting health == "complete" and all three receipt counts None — precisely the shape that previously read as definitive.

P2 — resolved

The description now names REVISION_SCOPE_MISMATCH, both replacement codes (INVALID_REVISION_OWNERSHIP / INVALID_LEDGER_DEPENDENCY), states the escalation is intentional, and tells callers to handle the typed error and re-read. That was the whole ask.

What I ran

make lint OK, make typecheck-all OK. pytest tests/test_reporting_frozen_read_safety.py tests/test_reporting_frozen_read_corrections.py tests/test_reporting_reconciliation.py tests/conformance/reporting/ -> 2225 passed, 2042 skipped, 7 errors. All 7 errors are built_distribution-fixture packaging tests (test_reporting_notification_packaging.py, test_reporting_materializer_packaging.py) that shell out to python -m build; the repo's own harness classifies them {"trace":[[152,["disk_full"]]],"classification":"recognized"} and this sandbox is at 99% disk. Neither file is touched by the PR, they passed on 737c3698 earlier today, and the Installed production reporting / Installed frozen feed CI jobs cover them properly. I also ran the other 17 tests/test_reporting_*.py files -> 456 passed. The two new tests pass locally on the memory backend (mcp-memory, a2a-memory); the postgres parameterizations skip here and are covered by the PG conformance jobs in CI.

No further findings. Good to merge once CI is green on 735168bd.

Copy link
Copy Markdown
Contributor Author

Main now includes the squash merges of #1218 and #1219. This PR currently reports a merge conflict; please rebase your branch onto current main, resolve it, and rerun the required gates. I will merge it after green CI, resolved threads, and renewed independent approval on the rebased head.

Copy link
Copy Markdown
Contributor Author

I checked the current merge conflict read-only: it is confined to .github/workflows/ci.yml. Your branch sets a 30-minute timeout for both PG lanes; main now has a 30-minute core / 15-minute process matrix after #1219. Keep main’s matrix during the rebase; the buyer source changes merge cleanly.

@bokelley
bokelley force-pushed the conductor/reporting-buyer-frozen-history-safety branch from 735168b to 2622275 Compare September 26, 2026 17:24
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Sep 26, 2026

@aao-secretariat aao-secretariat 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.

Ladon verdict: Approve

Approve — buyer frozen-read/reconciliation rewrite with comprehensive test coverage.

This PR rewrites the buyer frozen-read/reconciliation path in src/adcp/reporting/_reconcile.py (+827/−140) and adds ~1,700 lines of memory + PostgreSQL, MCP + A2A conformance/unit tests. Changes stay within the reporting module — no touches to type-system layering, wire/creative models, ctx_metadata/credentials, generated code, or CI-gate config.

Verification highlights:

  • Fail-closed load/mutation-detection logic and receipt-chain integrity verified.
  • Redaction behavior confirmed.
  • The fix: prefix is defensible: reasons is tuple[str,...] (not an enum), max_bytes is additive, and the documented raise-vs-return behavior change is confined to an unreleased rc6 slice — no shipped semver break.

No Critical/High/Medium findings. Not high-risk, no gated paths, no author-team gate. Prior decision was also approve; this run remains clean. Falls through to row 9.

Non-blocking nit: confirm full CI is green post-rebase.

@bokelley bokelley left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

APPROVE (unchanged — rebase verified)

(Review comment again: GitHub refuses a formal APPROVE when reviewer and author share the bokelley account.)

Re-checked at 2622275f on c67b97e0. origin/main is an ancestor; six commits, with the duplicate PG-timeout commit correctly dropped.

The change I approved is byte-identical. git diff 735168bd 2622275f -- src/adcp/reporting/_reconcile.py is empty, and none of the four newly merged commits (#1216 / #1218 / #1219 / #1220) touched that file. The PR's own test files are unchanged across the rebase too — the only tree delta is main's new work.

The clean textual merge is also a clean semantic one. _reconcile.py imports nothing from adjustment_evidence, submissions, or service_lifecycle, and no generated_poc/core/reporting_* model changed in the merged set — so #1220's raw-adjustment-evidence work and this PR's _validate_read_ledger digest checks meet only at the unchanged model contract.

Re-ran my own P1 probes on the rebased head — identical to 735168bd:

case result
delivery_only + unreceipted adjustment, pending_adjustment_count=1 False, ('MISSING_MATCHING_ADJUSTMENT_RECEIPT',)
same, pending count omitted False, same reason
delivery_only + accepted adjustment leaf True, ()
delivery_only, no adjustments True, ()
consumer_receipt + accepted adjustment receipt True, ()
consumer_receipt + unreceipted adjustment False, single reason

Local run: make lint OK, make typecheck-all OK, pytest tests/test_reporting_frozen_read_safety.py tests/test_reporting_frozen_read_corrections.py tests/test_reporting_reconciliation.py tests/test_reporting_adjustment_evidence.py tests/conformance/reporting/ -> 2373 passed, 2149 skipped, 0 failed, 0 errors. The 7 disk_full packaging errors I reported on the previous head are gone — sandbox disk went 99% -> 79%, so that selection now includes the built_distribution packaging tests passing.

On the conflict resolution: agree with keeping main's config. Main gives the core lane 30 job-minutes and process 15, against prior observed runtimes of 15m58s and 8m57s — comfortable either way. One small delta for awareness, not worth a commit on its own: the dropped commit also added a step-level timeout-minutes: 25 to the "Run Postgres conformance tests" step, which main does not have. That only affects diagnostics (pytest reporting its own timeout before GitHub kills the job), not the budget.

No new findings. Approval stands; good to merge once CI is green on 2622275f.

Copy link
Copy Markdown
Contributor Author

The last required check failed on infrastructure timing, not an assertion: Test Python 3.12 reached 99% with no test failures, then Run tests with coverage hit its 45-minute step timeout at 18:32:33 (job 108450852339). The job budget is 60 minutes. Please raise the coverage step budget (and job budget enough to retain setup/cleanup margin), preserving the test selection, then push for a fresh green run. This may affect the other reporting PRs using the same full-suite matrix.

@bokelley bokelley left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

APPROVE (workflow delta only)

(Review comment again: GitHub refuses a formal APPROVE when reviewer and author share the bokelley account.)

Reviewed 01944f0e. git diff 2622275f 01944f0e touches .github/workflows/ci.yml and nothing else, and src/adcp/reporting/_reconcile.py is still byte-identical to 735168bd — the buyer code is exactly what I approved.

One functional change (45 -> 55 on the 3.12 coverage step); the rest of the -16/+6 is comment rewriting. Parsed the result to be sure:

test job timeout-minutes: 60
  step: Run tests            -> 45 | if: matrix.python-version != '3.12'
  step: Run tests with coverage -> 55 | if: matrix.python-version == '3.12'

The budget checks out against the run that failed. From the 3.12 job in run 36258899936:

segment measured
set-up job -> type-ignore contract (checkout, Python, install, ruff, mypy, mypy --strict) 17:45:52 -> 17:47:21 = 1m29s
Run tests with coverage 17:47:21 -> 18:32:33 = 45m12s (hit the old bound)
post-steps + complete job 18:32:33 -> 18:32:34 = 1s

So 55m step + ~1m30s overhead = 56m30s against the 60-minute ceiling, leaving 3m30s of margin. The invariant the old comment was protecting — step bound fires before the job ceiling, so a stuck suite fails one named step instead of being silently cancelled — still holds. The suite was at 99% when killed at 45m12s, so ~46m is the realistic need and 55m is roughly 20% headroom. tests/ collects 14,175 here, which matches the "over 14k" in the commit message.

Dropping the old comment's specific figures is fine — they had gone stale (it claimed ~16m plain and ~30m coverage; actuals are ~35m and 45m+), and the replacement keeps the part that matters, which is why the step bound must sit below the job ceiling.

Approval stands. No findings on this delta.

Non-blocking: 3.10 is the next lane to trip

Same run, Test Python 3.10: the plain Run tests step took 17:34:47 -> 18:09:53 = 35m06s against the unchanged 45-minute bound — 78% consumed, ~22% headroom. That is the same growth curve that just pushed 3.12 past 45m. Not something to change in this PR, but worth raising the plain-suite bound proactively rather than after a red matrix, or splitting the suite.

Also for awareness: main moved to 7c5a1239 (#1223) after your rebase. It only touches src/adcp/server/serve.py and tests/test_serve_lifespan_hooks.py, so there is no ci.yml conflict and GitHub still reports MERGEABLE — no action needed.

Comment thread .github/workflows/ci.yml
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Sep 26, 2026

@aao-secretariat aao-secretariat 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.

Ladon verdict: Approve

Approve.

Subsequent pass over PR #1222. The prior run was clean (no findings) and the delta since is a single non-generated file, .github/workflows/ci.yml, well under the largest-file threshold.

Medium findings

  • .github/workflows/ci.yml — Raising the coverage step to 55m without lifting the 60m job ceiling leaves ~5m for the 3.12 leg's checkout/install/ruff/mypy/type-ignore pre-work plus cleanup; a hung coverage run could trip the job ceiling before the named step timeout. Worth a look but non-blocking.

No critical/high findings. Only one medium finding (well under the 3-medium comment threshold), not in a data-loss/schema/infra category. high_risk is false and gated_paths is false, so the REVIEW_REQUIRED status does not gate. No no-auto-approve team match. Prior decision was approve. Falls through the decision table to row 9 → approve.

Copy link
Copy Markdown
Contributor Author

The Medium timeout-margin finding is blocking. #1221 applied the same narrow correction: 55-minute Python 3.12 coverage step with a 70-minute Python 3.12 job cap, leaving other matrix cells unchanged. Please push the equivalent fix now so the current CI run need not finish before the next review.

@aao-secretariat aao-secretariat 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.

Ladon verdict: Approve

Approve — CI timeout tuning only, no blocking findings.

This delta pass covers a single file, .github/workflows/ci.yml. The change raises the 3.12 job cap to 70 min (60 elsewhere) and the coverage step bound 45→55, keeping ~15 min setup/cleanup headroom on both lanes and keeping each step bound below its job cap so a hang fails the named step rather than the whole job. Conditional-expression syntax is valid; no code, wire-shape, public export, or CI gate is affected — the gates stay armed. The prior run was a clean approve, and no blockers were introduced.

Decision-table walk: no critical/high/medium findings (row 1 n/a); gated_paths false (row 2 n/a); high_risk false with no deletions (rows 3/5 n/a); no medium data-loss/schema/infra (row 4 n/a); prior decision was approve not escalate (row 6 n/a); no no-auto-approve team match (row 7 n/a); zero medium findings (row 8 n/a). Falls through to row 9 → approve.

Note: review_decision is REVIEW_REQUIRED, but that only forces escalate when gated_paths is true (row 2), which it is not here.

@bokelley bokelley left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

APPROVE (renewed on 4c05106a)

(Review comment again: GitHub refuses a formal APPROVE when reviewer and author share the bokelley account.)

git diff 01944f0e 4c05106a touches .github/workflows/ci.yml and nothing else, and src/adcp/reporting/_reconcile.py is still byte-identical to 735168bd. Buyer code unchanged; this is a three-line workflow delta.

timeout-minutes: ${{ matrix.python-version == '3.12' && 70 || 60 }}

This is the right fix, and I was wrong to wave the previous margin through. My last pass measured the actual 3.12 overhead at 1m29s and called 3m30s of slack "comfortable" — true as an arithmetic fact, but aao-secretariat's structural point is the better invariant: every lane should keep the same setup allowance, so the step bound stays the thing that fires. 4c05106a restores it exactly:

lane job cap step bound non-test allowance
3.12 (coverage) 70 55 15m
3.10 / 3.11 / 3.13 60 45 15m

Against measured overhead of 1m29s (3.12) and 54s (3.10), 15m is ~10-16x headroom on every lane. Verified by parsing the file:

test job timeout-minutes: "${{ matrix.python-version == '3.12' && 70 || 60 }}"
  step: Run tests               -> 45 | if: matrix.python-version != '3.12'
  step: Run tests with coverage -> 55 | if: matrix.python-version == '3.12'

Checks that matter here:

  • Expression is legal in this position. pg-conformance in the same file already uses timeout-minutes: ${{ matrix.timeout_minutes }} and resolves correctly in practice (core 30 / process 15, observed at 15m58s and 8m57s), so job-level timeout-minutes demonstrably accepts matrix context here.
  • Ternary is sound. GHA's a && b || c falls through to c when b is falsy; b is 70, non-zero, so 3.12 -> 70 and everything else -> 60.
  • Required check names are untouched. name: Test Python ${{ matrix.python-version }} is unchanged, so branch protection keeps matching.
  • workflow-security runs actionlint + zizmor on every push, so this is machine-validated in CI too — currently pending on 4c05106a.

Approval stands. No findings.

Non-blocking

The neighbouring pg-conformance job expresses the same idea as a matrix.include with an explicit timeout_minutes per lane. That form is immune to the && || falsy-operand footgun if someone later edits the number, and would read consistently with the rest of the file. Not worth a commit now — just something to prefer if this grows a third case.

My earlier note still stands on its own: 3.10's plain Run tests step ran 35m06s against its unchanged 45-minute bound, so that lane is the next to trip.

This branch has not been deployed

No deployments
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.

1 participant