Skip to content

docs: the optimistic attempt and the table-size guard - #59

Open
Kiran01bm wants to merge 1 commit into
mainfrom
kiran01bm/docs-optimistic-attempt
Open

docs: the optimistic attempt and the table-size guard#59
Kiran01bm wants to merge 1 commit into
mainfrom
kiran01bm/docs-optimistic-attempt

Conversation

@Kiran01bm

Copy link
Copy Markdown
Collaborator

Adds docs/optimistic-attempt.md — the deep treatment of the bounded optimistic attempt and the table-size guard — and cross-links it from the docs index, high-level-design, and limitations.

Why

The two front doors are introduced in high-level-design.md, but the questions the bounded attempt keeps raising — why gamble at all, why proven routes skip the size guard, what a wrong guess costs on a small vs a large table, whether the attempt is a stopgap until copy-and-swap — had no single authoritative answer in the doc set. This page settles them once, with the full entry-to-exit map every statement walks.

What

  • New docs/optimistic-attempt.md: the escalation ladder (proof → bounded attempt → refuse/copy), the four-lane entry-to-exit flow diagram with a gate-by-gate walkthrough and typed exit inventory, the Options.MaxTableSizeBytes knob, proof-based vs budget-based routes, Q&A, wrong-guess timelines, peer-tool comparison, and the invariants the page rests on.
  • Index row in docs/README.md plus pointers from the "decided shape" section, the front-doors section of high-level-design.md, and the size-threshold paragraph of limitations.md.
  • Drift fix in high-level-design.md: the size guard measures pg_total_relation_size (heap + indexes + TOAST, all partitions), not pg_class.relpagespkg/preflight explicitly treats planner estimates as never the guard's authority.

Before / after

before                                    after
──────                                    ─────
high-level-design.md                      high-level-design.md
  "two front doors" intro ──── (ends;       "two front doors" intro ─┐
   size-guard rationale,                                             │ deep link
   exit map, cost model                   docs/optimistic-attempt.md ▼
   scattered or unwritten)                  ladder ▸ entry-to-exit map
                                            ▸ size-guard knob ▸ proof vs
limitations.md                              budget ▸ Q&A ▸ timelines ▸
  size threshold = policy ── (why the       peer tools ▸ invariants
   guard covers only that                    ▲               ▲
   path: unexplained)                        │               │
                                          limitations.md   README.md index
guard measured as                         guard measured as
"pg_class.relpages" (stale)               pg_total_relation_size (matches code)

The two front doors were introduced in high-level-design.md but the
bounded attempt's full design — the entry-to-exit map, which routes the
size guard covers and why, the wrong-guess cost model, and how the
ladder evolves once copy-and-swap lands — had no home. Also corrects
high-level-design.md's stale claim that the guard measures
pg_class.relpages (it measures pg_total_relation_size).
@Kiran01bm
Kiran01bm marked this pull request as ready for review August 27, 2026 02:44
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@aparajon

Copy link
Copy Markdown
Collaborator

🤖 Adversarial correctness review, requested by @aparajon and performed by their agent. Reviewed at head 191a8efb, in a worktree, with the binary built from this branch and every falsifiable claim on the page run against a live PostgreSQL — an 80 MB table at --max-table-size 10MiB, the forced-override cases, the declarative door through migrate.RunDesired, and all 26 links and anchors resolved.

Verdict: the page is excellent and one of its central claims is wrong. The entry-to-exit map is the artifact this repo has been missing, the wrong-guess timelines are the clearest explanation of the design I've read, and the "why gamble at all" answer is genuinely persuasive. But the TL;DR's rung 1 and the Q&A both say proof-based routes are never size-checked, and three of the five planner reasons that reach execution — including metadata-only, the one the sentence names — are size-guarded. The page's own §"Who gets size-checked" table, its own invariant 2, the CLI's --max-table-size help, and limitations.md in this same PR all state it correctly, so this is a wrong sentence sitting on top of a right model rather than a wrong model.

Not blocking in the mechanical sense — docs-only, no code, nothing deployed — and the same shape as #57, which I stamped with the correction as the top action item. Stamping on the same terms: Finding 1 should land before merge.

# Finding Severity
1 TL;DR rung 1 and the Q&A say proof-based routes are never size-checked; metadata-only, fast-default and binary-coercible are all guarded correctness
2 Exits 3a/3b are typed with the executor's failure codes; a budget refusal publishes not-native-safe-budget-exceeded + cause correctness
3 The exit inventory has no row for outcome: failed — the only ending that leaves committed DDL on the table completeness
4 "Nothing below differs between the doors — except lane F" — the declarative door has its own plan-level admission gate correctness

1. Three of the five executing proof reasons are size-guarded, including the one the sentence names

Rung 1 of the ladder:

Shapes it can prove are metadata-only, or that it can substitute with a known online idiom … run without any size check — their online-safety comes from the proof, not from a bet.

and the Q&A, which is the page's canonical answer to the question it was written to settle:

Proof-based routes — the planner classified the statement as metadata-only, or substituted a known online idiom. Their safety comes from the plan; table size is irrelevant, so they pass NoSizeLimit and are never size-checked.

Both are false for metadata-only. Every executing planner reason, submitted to the CLI built from this branch against an 80 MB table with --max-table-size 10MiB:

Submitted statement planner reason Guarded? Result
ALTER TABLE t ADD COLUMN c1 int metadata-only yes refused / not-native-safe-table-too-large
ALTER TABLE t ADD COLUMN c2 int NOT NULL DEFAULT 7 fast-default yes refused / not-native-safe-table-too-large
ALTER TABLE t ALTER COLUMN s TYPE varchar(200) binary-coercible yes refused / not-native-safe-table-too-large
CREATE INDEX idx_n ON t (n) safer-idiom (substituted) no executed-natively
ALTER TABLE t ALTER COLUMN amt SET NOT NULL safer-idiom (substituted) no executed-natively
CREATE INDEX CONCURRENTLY idx_oi ON t (amt) online-idiom no executed-natively
ALTER TABLE t ADD CONSTRAINT ck CHECK (…) NOT VALID online-idiom no executed-natively
ALTER TABLE t VALIDATE CONSTRAINT ck online-idiom no executed-natively

ADD COLUMN c1 int is exactly "shapes it can prove are metadata-only", and it is refused:

{
  "outcome": "refused",
  "reason": "not-native-safe-table-too-large",
  "statement": "ALTER TABLE z59m.t ADD COLUMN c1 int",
  "detail": "table is 83771392 bytes on disk (heap, indexes, and TOAST), above the configured 10485760-byte size threshold. …"
}

The axis is not proof-vs-bet. sizeGuardApplies(p, substituted) is !substituted && !onlineIdiomPlan(p), and ReasonOnlineIdiom means something narrower than "an online idiom was used" — it is "already the safe native form (CONCURRENTLY, NOT VALID, VALIDATE, USING INDEX)", i.e. the operator submitted the safe form themselves. So the real rule is:

exempt iff the SQL about to run is a sequence the planner authored, or a form the operator already submitted in its concurrent / NOT VALID / VALIDATE shape. Guarded iff the executor is about to run the submitted form as one blind statement under ACCESS EXCLUSIVE — however confidently the planner classified it.

And the reason that rule is right is the most interesting thing this page could say, so it's worth saying rather than trading away: metadata-only is a prediction from parse plus catalog introspection, not an assertion PostgreSQL honours. If the prediction is wrong — a version edge, an unexpected type, an extension-owned column — the statement rewrites the table and you land in Case 3 of the timeline section, the exact outage the page spends its best paragraphs describing. The guard hedges against the classifier being wrong, not against the absence of a classification. Which also explains why the exempt set is exempt: those statements never hold ACCESS EXCLUSIVE for the long work, so they cannot produce Case 3 no matter how wrong the plan is.

Three places on the page already have this right, which is why I read it as a sentence-level fix rather than a rethink:

  • §"Who gets size-checked" keys its table on "Blind bounded attempt of the submitted form" — correct. (Its lead-in sentence, "exactly the executions whose safety is a bounded bet rather than a proof", is the wrong axis again.)
  • Invariant 2: "Planner-proven idioms and substituted sequences pass NoSizeLimit; blind attempts of the submitted form … are always size-guarded." — correct, and notably does not say metadata-only.
  • The diagram puts "metadata-only or rewrite? PostgreSQL cannot say without asking the server" inside lane B, which is where a metadata-only-classified statement actually goes. The diagram is closer to right than the prose above it.

Outside the page, limitations.md in this same PR states it exactly:

Add a column | Converges. Runs as a bounded attempt of the submitted form, so the table-size guard applies

the guard covers only the blind bounded attempt of a submitted form — planner-proven online sequences are exempt

as does the CLI help ("Planner-proven online steps (concurrent index builds, constraint validation) are not size-guarded") and NoSizeLimit's doc comment ("the size guard protects blind attempts, not planner-proven online idioms"). The nouns matter: steps, sequences, idioms — never proofs.

Why this one is worth landing before merge rather than as a follow-up: this page is now the authoritative deep treatment, and the claim is load-bearing in the direction that costs an operator something. A reader who believes rung 1 concludes that adding a nullable column always converges, and at the 1 GiB default that is refused on any table big enough to care about. limitations.md calls that out explicitly — "with the default 1 GiB threshold, adding a column to a larger table refuses until the threshold is raised" — which is the single most surprising behaviour in the tool, and rung 1 currently tells the reader it can't happen.

One knock-on: cli-output-examples.md (not touched by this PR) carries the same error in its reason table — "the table exceeds the configured bound and the change is not provably metadata-only" — and so does the live refusal detail string, which says "pg-sprite cannot yet prove this change is instant on a table this size" about a statement the planner just classified metadata-only. Both are follow-ups, not this PR's job, but they're the same sentence and they'll be easiest to fix while the model is fresh.

2. The exit inventory publishes the failure-path codes for the refusal exits

Exits 3a and 3b are typed as budget-lock-exceeded and budget-statement-exceeded — in the diagram, in the inventory's "Typed as" column, and in the parenthetical under the timelines. Six occurrences.

Those are pkg/executor.Code values. They appear on "outcome": "failed", which is what execution-model.md shows them doing ("outcome": "failed", "code": "budget-lock-exceeded"). A bounded attempt that overruns its budget is a refusal, and the refusal publishes a different pair:

{
  "outcome": "refused",
  "reason": "not-native-safe-budget-exceeded",
  "cause": "statement-budget",
  "statement": "ALTER TABLE z59m.t ALTER COLUMN v TYPE varchar(300)",
  "detail": "cancelled after the 150ms statement budget: the change does real rewrite work, not an in-place catalog change, …",
  "forced": true
}

cli-output-examples.md documents this correctly, including that verdict.Reasons() pins the set by test. The new page names not-native-safe-budget-exceeded nowhere and never mentions cause at all — which is the field that distinguishes 3a from 3b in the artifact automation actually reads.

This matters more than a token typo because of the page's own invariant 4 — "automation never branches on error prose" — and invariant 5 — "a budget refusal is not a failure." The inventory contradicts invariant 5 in the table two hundred lines above it: it labels a refusal with the code that only appears on the failure it says this isn't. An integrator wiring retry policy from this page's inventory writes a matcher that never fires.

Narrow fix, and it doesn't touch the prose at line 262 ("stable codes — budget-lock-exceeded and budget-statement-exceeded (pkg/executor)"), which is accurate about the executor layer. Just the two inventory rows, the two diagram leaves, and the parenthetical after Case 4:

Exit Typed as
3a not-native-safe-budget-exceeded, cause: lock-budget (SQLSTATE 55P03; executor code budget-lock-exceeded)
3b not-native-safe-budget-exceeded, cause: statement-budget (SQLSTATE 57014; executor code budget-statement-exceeded)

Same treatment would help row 4, which is typed *preflight.SizeError — the Go type — where the other rows carry published tokens. The verdict says not-native-safe-table-too-large. Worth picking one layer per column and naming the other in parentheses.

3. The inventory has no row for the one ending that leaves DDL on the table

Six exits, and the "DDL executed?" column answers Yes / Yes / No / No / No / Yes. There is no row for outcome: failed, which is the only ending where the answer is partially, and what committed stays committed. The diagram and the walk both fold lane A's non-success ending into "typed executor refusal (invalid-index leftovers, cancelled, …)", but a mid-sequence stop is not a refusal — it is an operational failure, exit code 1, with a committed prefix. Reproduced from the CLI on this branch:

$ pg-sprite migrate --alter 'ALTER TABLE z59m.nn ALTER COLUMN v SET NOT NULL' --json
{
  "outcome": "failed",
  "code": "execution-failed",
  "failed_step": 2,
  "failed_step_sql": "ALTER TABLE \"z59m\".\"nn\" VALIDATE CONSTRAINT \"nn_v_not_null\"",
  "detail": "sequence step 2 of 4 failed; the 1 committed steps' state remains — …",
  "executed_sql": [
    "ALTER TABLE \"z59m\".\"nn\" ADD CONSTRAINT \"nn_v_not_null\" CHECK (\"v\" IS NOT NULL) NOT VALID"
  ]
}
exit=1

and the residue is real — nn_v_not_null | f, a NOT VALID constraint now on the live table.

The page's opening paragraph hands this off — "what a mid-sequence failure leaves behind is [execution-model.md]" — which is a fine division of labour for the mechanics. But this page owns the claim that it maps every exit ("the full entry-to-exit map every statement walks", "Every ending is typed", "the exit inventory catalogs the endings"), and an inventory whose safety-relevant column never says "yes, partially" is the one place a reader would confidently conclude that no ending can leave debris — which is the opposite of the guarantee the rest of the page is careful about. One row, pointing at execution-model.md for the detail, closes it:

| 7 | Sequence stopped mid-flight (step failed, external cancellation) | outcome: failed + executor code, exit 1 | Partially — the committed prefix remains (execution-model.md) |

4. The declarative door has a whole-plan admission gate that the map doesn't have

Nothing below differs between the doors — except lane F, which the declarative door rejects outright (ErrForceNotSupported).

ErrForceNotSupported is right, and it's a nice catch to have documented. But it isn't the only difference. RunDesired runs admitPlan before any statement enters the walk, and admitPlan refuses the whole plan, all-or-nothing, for four reasons that exist only on this door:

  • plan-fingerprint-mismatch — the plan derived at execution time isn't the pinned one
  • unsupported-statement — the target table doesn't exist
  • destructive-change — any planned statement discards live structure
  • a router disposition refusal, when the plan as a whole doesn't route to execute

Live, against a desired file that drops one column and adds another:

{
  "outcome": "refused",
  "reason": "destructive-change",
  "detail": "planned statement 1 discards live structure (ALTER TABLE z59.d2 DROP v); desired-state execution runs no destructive statement — run it deliberately through the imperative front door; admission is all-or-nothing, so the plan's other statement, even if non-destructive, was not run"
}

No verdicts array — nothing entered the per-statement walk at all. The ADD COLUMN extra beside it was planned, was non-destructive, and did not run.

This is worth a node in the map rather than a footnote, because it's the difference between "each statement is judged" and "the plan is judged, then each statement is judged", and it's the gate an embedder hits first. The per-statement point matters too: the size guard is not a plan-level check, so a caller that pre-flights the plan can still take a size refusal on statement 3 of 5 with statements 1 and 2 already committed — which is exactly the committedPrefixDetail path, and lands back in Finding 3.

Also

(nit) The page's only use of the word "migration" is the anchor high-level-design.md#two-migration-front-doors-optimistic-vs-classified. The page itself is clean, but the link pins a heading that's on the wrong side of the terminology line; whenever that heading is converted, this link and the one in docs/README.md move with it.

(nit, not this PR) "the 1 committed steps' state remains" — the detail string doesn't singularize. Visible in the Finding 3 output above.

(nit) The peer table gives pg_osc / pg_repack one row with cost "full copy even for a metadata-only change". pg_repack doesn't run ALTERs — it repacks a table to remove bloat — so "a metadata-only change" isn't a choice a user brings to it. Splitting the row or dropping pg_repack from it would make the comparison harder to argue with, which is the point of the table.


Action items

  1. (Finding 1 — before merge) Fix rung 1 and the Q&A to key the exemption on what will execute rather than on whether the planner proved something: substituted sequences and already-online submitted forms are exempt; every blind attempt of the submitted form is guarded, metadata-only included. Worth adding the reason — the guard hedges against the classifier, not against the absence of a classification — and worth publishing the reason → guarded/exempt table, which is the whole page in one glance. Same fix applies to the lead-in sentence of §"Who gets size-checked".
  2. (Finding 2) Retype exits 3a/3b as not-native-safe-budget-exceeded with cause: lock-budget / statement-budget, naming the executor codes and SQLSTATEs as the underlying layer. Same for row 4 (not-native-safe-table-too-large vs *preflight.SizeError).
  3. (Finding 3) Add an inventory row for outcome: failed / committed prefix, and stop calling the lane-A mid-sequence stop a refusal in the diagram and the walk.
  4. (Finding 4) Add the declarative door's admitPlan gate to the map, and note that the size guard is per-statement, not plan-level.
  5. (follow-up, separate PR) The same metadata-only claim in cli-output-examples.md's reason table and in the SizeError detail string.

Verified — tried to break, couldn't

Every link and anchor resolves — 26 of them, including both anchors into high-level-design.md (#two-migration-front-doors-optimistic-vs-classified and #two-front-ends-declarative-and-imperative) and all 11 intra-page table-of-contents anchors.

The relpages drift fix is correct. high-level-design.md said the threshold reads pg_class.relpages; the PR corrects it to pg_total_relation_size including all partitions. Matches pkg/preflight/preflight.go, which sums pg_total_relation_size(c.oid) plus every child in pg_partition_tree. The live detail confirms it: 83771392 bytes on disk (heap, indexes, and TOAST) against an 80 MB table.

The size-guard knob section is accurate. DefaultOptions is MaxTableSizeBytes: 1 << 30, brief lock budget 3 s, statement budget 30 s — matching "1 GiB", "3 s of lock wait and 30 s of statement runtime". A zero or negative value is rejected by opts.validate() before any connection is used (invariant 6 holds).

Lane F really is still size-guarded (invariant 2's second half, and the §"Who gets size-checked" row): --force on the 80 MB table at a 10 MiB limit refuses with not-native-safe-table-too-large, not a blind attempt.

Multi-statement smuggling really is refused at parseinput must contain exactly one SQL statement: got 2. This was a finding of mine on an earlier page; it's genuinely closed, and the claim on line 148 is now true.

Both wrong-guess timelines reproduce. Case 2 (small table, rewrite fits): varchar(50)varchar(10) forced under a 400 ms budget completed in 885 ms — a "wrong" guess that succeeded, atttypmod 54 → 14. Case 3 (rewrite can't fit): textvarchar(300) forced under 150 ms refused with cause: statement-budget and the column unchanged. Transactional DDL left no residue in either direction.

ErrForceNotSupported exists and fires at the top of RunDesired, before planning — so the lane-F exclusion claim is true even though the door has other differences (Finding 4).

The MySQL mapping paragraph checks out. INSTANT ↔ lane B succeeding, INPLACE-with-long-work ↔ lane A, COPY ↔ lane C is the right correspondence, and the asymmetry it rests on — MySQL refuses the ALGORITHM=INSTANT assertion upfront for free, PostgreSQL offers no equivalent — is accurate for both engines.

Invariant 3 is correctly scoped. pkg/executor/optimistic.go emits SET LOCAL lock_timeout = …; SET LOCAL statement_timeout = … inside the attempt's transaction, so the "regardless of session defaults" claim holds — and the page is right to scope it to the attempt, since the safer-sequence path runs autocommit-each-step and SET LOCAL is unavailable there.

CI is green — DCO, all-green, and changes pass; the code lanes correctly skip on a docs-only diff. No bot review comments to fold in (Codex hit its usage limit and posted nothing reviewable).

This review was generated by Claude Code (claude-opus-5).

@aparajon

Copy link
Copy Markdown
Collaborator

🤖 Second pass, requested by @aparajon and performed by their agent — two lenses this time: OSS adoption ease (can someone outside this team decide to trust pg-sprite from this page?) and orchestrator integration (can an embedder wire routing from it?). Nothing here blocks; the correctness pass is the other comment.

Both lenses land on the same missing artifact: a reason → guarded/exempt table. For an evaluator it's the membership test that turns the lane model from a diagram into something they can run against their own DDL. For an embedder it's the input to a routing decision. And a test that pins it against sizeGuardApplies would have caught the correctness finding before the sentence was written. One table, three payoffs.

# Suggestion Lens
1 Publish reason → guarded/exempt as a table, and pin it by test both
2 Add a "check your own DDL" recipe — the page gives a model with no way to test membership adoption
3 Surface the 1 GiB default's consequence at the top, not in limitations.md adoption
4 Add a routing contract: per refusal reason, what an orchestrator should do integration
5 State that the guard is per-statement, and that RunDesired is library-only integration

Lens 1 — adoption

What this page does that nothing else in the repo does: it answers the actual objection. Every evaluator arrives at "you attempt DDL on my production table and hope" and stops there. This page takes the objection seriously enough to name it a gamble in its own voice, then earns the position back with the PostgreSQL-specific argument. The peer table is fair to the competition rather than dismissive, and the amplifiers paragraph — the fair lock queue draining serially, "you can't roll back an outage" — concedes the worst case in a way that makes the rest credible. That honesty is the asset. Keep it under editing pressure.

1. Publish the mapping, don't just describe it. The page teaches a four-lane model and then leaves the reader unable to answer the only question they have: which lane is my ALTER? planner.Reason is the answer, it's already a closed vocabulary of twelve, and five of them reach execution. A table costs six rows:

planner reason Size-guarded?
metadata-only yes — blind attempt of the submitted form
fast-default yes
binary-coercible yes
online-idiom no — already the safe native form
safer-idiom no — the planner substituted its own sequence
everything else (volatile-default, type-rewrite, …) n/a — refused at plan time

That's the entire page in one glance, it's the fix for the correctness finding, and it's the thing a reader will screenshot.

2. Give them a way to check. The model is only useful if a reader can test membership, and today they can't — there's no recipe on the page. There is one, it's one command, and it's how I built the finding matrix in about four minutes:

$ pg-sprite migrate --dry-run --json --alter 'ALTER TABLE t ADD COLUMN c int' \
    | jq -r '.statements[].decisions[].reason'
metadata-only

Three lines next to the table above and the page stops asking to be trusted and starts asking to be verified — which is the posture the whole argument is making anyway. It also front-loads the honest caveat: the classifier told you metadata-only and the guard still applies, which is precisely the nuance the correctness finding is about.

3. Move the default's consequence up. The most surprising behaviour in the tool is that at defaults, adding a nullable column to a table over 1 GiB is refused. limitations.md says so plainly — "with the default 1 GiB threshold, adding a column to a larger table refuses until the threshold is raised" — but this page now owns the guard, and its §"The size guard knob" gives the default as a bare number with "a default to tune, not a recommendation." An evaluator meets this on their first real database, not in the limitations page. One sentence under Default naming the consequence, and the reader is calibrated instead of surprised.

4. docs/README.md earned its rows. Adding capabilities.md, execution-model.md, and safer-sequences.md alongside the new page — files that already existed and weren't indexed — is the kind of thing that quietly turns a docs directory into a map. Worth doing even though it's outside the stated scope.

Lens 2 — orchestrator integration

5. The exit inventory is a contract, so its layering matters. The correctness finding (exits 3a/3b typed with the executor's failure codes) reads as a token typo but behaves as a seam bug: an orchestrator that writes its retry matcher from this table matches budget-lock-exceeded against a payload carrying reason: not-native-safe-budget-exceeded, cause: lock-budget, and never fires. The page is the right place to say which fields are the contract and at which layer — outcome and reason (+ cause) are the verdict surface, code is the executor surface and appears on failed, SQLSTATE is the PostgreSQL surface underneath both. Three sentences, and every downstream matcher gets written against the right field.

6. The routing contract is the table this page is one step from owning, and nothing else owns it. The page already has the raw material — invariant 5 says a budget refusal is not a failure; the Phase 5 section says which refusals become routing signals — but it never tabulates the operational consequence. For an embedder that consequence is the whole integration:

Refusal What an orchestrator should do
not-native-safe-budget-exceeded, cause: lock-budget transient — retry off-peak, same plan
not-native-safe-budget-exceeded, cause: statement-budget terminal today; the future copy-and-swap on-ramp
not-native-safe-table-too-large policy — the operator raises the threshold deliberately
insufficient-privileges operator action; detail names the exact GRANT
backend-unavailable, destructive-change, plan-fingerprint-mismatch human decision — never auto-retried

The distinction that matters most: "retry later" versus "stop and tell a person" is, for an orchestrator, the difference between a transient blip and a blocked merge gate. That judgement lives in this engine's head today and in no document.

7. Pin the table by test — third time I've asked for this shape, and this one is the cheapest. pkg/verdict/docs_test.go already does exactly this for the refusal-reason table: iterate verdict.Reasons(), assert docs/cli-output-examples.md has a row for each. planner.Reasons() exists and enumerates all twelve. The sibling test writes itself:

for _, r := range planner.Reasons() {
    assert.Contains(t, doc, fmt.Sprintf("| `%s` |", string(r)),
        "docs/optimistic-attempt.md is missing a size-guard row for %q", r)
}

Stronger still: build a synthetic single-decision plan per reason and assert the doc's row agrees with sizeGuardApplies. Either version would have failed at authoring time — the table can't be written without deciding metadata-only's answer — and both stop the page drifting silently the first time a new reason starts executing. (The earlier ask from #51, pinning the StepKind → budget-class table, is still open; this is the same shape and less work.)

8. The guard is per-statement, and the declarative door is library-only. Two clauses for embedders. RunDesired loops execute per planned statement, so the size check is not a plan-level gate: a caller that pre-flights the plan can still take a size refusal at statement 3 with 1 and 2 already committed. And the entry paragraph presents both front doors as peers, but RunDesired has no CLI verb today — an integrator reading the map will go looking for one. limitations.md says "library-only today"; this page should too, since it's the one that maps the door.


Action items

  1. Publish the reason → guarded/exempt table (also resolves Finding 1 of the correctness pass).
  2. Add the --dry-run --json | jq .reason recipe beside it.
  3. Name the 1 GiB default's consequence under Default in §"The size guard knob".
  4. Add the routing contract table — per refusal, what an orchestrator does — and state which field lives at which layer.
  5. Pin the guarded/exempt table with a planner.Reasons() docs test, in the shape of pkg/verdict/docs_test.go.
  6. Note that the size guard is per-statement rather than plan-level, and that the declarative door is library-only today.

This review was generated by Claude Code (claude-opus-5).

@aparajon aparajon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Approving — the correctness and adoption/integration passes are in the two comments above. Finding 1 (the TL;DR and Q&A say proof-based routes are never size-checked; metadata-only, fast-default and binary-coercible are all guarded) is worth landing before merge, but it's a sentence-level fix on top of a model the rest of the page — and limitations.md in this same PR — already state correctly.

This review was generated by Claude Code (claude-opus-5).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants