Skip to content

docs(decisions): ADR 0245 — campaign-scope refusal binds at both fabrika seams - #5081

Merged
usirin merged 2 commits into
mainfrom
umut/adr-0245-campaign-scope-fence
Aug 9, 2026
Merged

usirin merged 2 commits into
mainfrom
umut/adr-0245-campaign-scope-fence

Conversation

@usirin

@usirin usirin commented Aug 9, 2026

Copy link
Copy Markdown
Member

When we say "one campaign at a time, everything else off the table," that rule needs somewhere real to live. This ADR records where: the fabrika build picker must not offer off-campaign work, and build claim must refuse an off-campaign issue even when someone hands it the number directly — both reading one shared predicate so they can never disagree. It also says plainly that nothing enforces this yet, and that the gap is covered by people paying attention until the two build issues land.

Adds one file: .decisions/0245-campaign-scope-fence-binds-both-seams.md. Nothing else changes — no code, no pipeline behavior.

Fixes #5011

This is the record that discharges #5011's acceptance criteria, not a formality. The founder ruled the decision on the issue but asked that it not be closed on the ruling alone: the criteria demand a written record with citations read from source. This ADR is that record — it cites the exact four selection facts the v1 candidate-pool script uses (and states that milestone, dependency topology and ready-for: are not among them), says which seam carries the refusal and why neither is redundant, states the fabrika-only side under ADR 0238 with the interim exposure named, and keeps dependency eligibility as a separate predicate.

Four issues build against this rule and reference it: see #5012, #5013, #5015 and #5016.

ADR number: merged max on main is 0244, and .decisions/0245-* is unique on main at the time of this push, so 0245 does not collide.

Deviations

Procedure — the catch-up was a cherry-pick, not git rebase. Said: rebase the branch onto latest main. Did: created a fresh branch at latest main and cherry-picked the single ADR commit onto it, then updated the branch ref. Why: the branch was already checked out in another worktree, so it could not be checked out again here; for a one-commit branch the two produce the same tree and the same parentage. Disposition: no action needed — the resulting head is one commit ahead of latest main with no conflicts, and the file content is byte-identical to the authored commit.

Procedure — the push named an explicit refspec instead of the usual push verb. Said: push with the lease guard. Did: pushed this worktree's head to the branch ref with --force-with-lease pinned to the branch's previous commit, then confirmed the moved ref independently with a remote ref read. Why: the standard push verb pushes the branch checked out in the worktree under its own name, which could not be used here for the reason above. Disposition: no action needed — the remote ref was verified at the new head after the push, which is the property the verb exists to establish.

Content — the ADR itself was authored elsewhere and is unmodified here. Said: get the authored ADR onto a PR. Did: exactly that; no edit was made to the ADR text. Why: the file was already written and pushed, and re-authoring it was explicitly out of scope. Disposition: for the reviewer to judge the content on its own terms.

…ika seams (#5011)

Records the founder ruling on #5011: the refusal binds at build pick AND
build claim from one shared predicate (the pick filter alone has the
direct-handoff hole), fabrika-only under ADR 0238, overridable explicitly
and on the record, with the interim social-enforcement gap stated.

Fixes #5011

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

No preview deploy

  • No preview deploy for this PR — its diff touches no deploy-relevant path, so no preview stack was minted and e2e is not applicable. (d74e661)
  • web — Stage pr-5081 torn down.

@usirin

usirin commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

review-doc: advisory — blocking-set PR (§CP — approval-gated)

PR #5081 is §CP by content, not by path. .decisions/** matches no owned CODEOWNERS path, but the shared guard-content-probe verb flags the ADR (ADR 0164): guard-touching (§CP, ADR 0164) [guard-vocabulary-match]. Re-run first-party here, and the Step-0 classifier agrees (CONTROL_PLANE_TOUCHED empty, GUARD_TOUCHING set). My verdict is advisory only: it does not authorize a merge. Under the §CP hard gate (ADR 0135), a @kamp-us/control-plane member approves this at its current head and ship-it then enqueues it (ADR 0048) — there is no hand-merge in the §CP path.

Reviewed-head: @ 6f729f7

Verified against #5011's acceptance criteria + doc hygiene. All checks pass — substantively merge-ready, pending the control-plane approval and the CI note below.

Acceptance criteria (#5011)

  • [PASS] AC1 — record landed under .decisions/ naming the binding seam. ## Decision names both seams. The ADR-vs-on-issue fork the founder left open (comment 5230192150) is resolved toward ADR-grade.
  • [PASS] AC2 — sourced citation of the v1 candidate-pool selection facts. This was the one unmet criterion at ruling time, and it is the deliverable. ## Context cites the v1 candidate-pool script (read 2026-08-09) and reproduces the exact query string and the exact select(.assignee == null and (.pull_request | not)) filter. I compared both against origin/main: character-exact. It enumerates four facts (open, status:triaged, a bare priority band, null assignee) plus the not-a-PR exclusion — this corrects the ticket's own "exactly three facts" by counting state=open, which is positive evidence of a first-party source read rather than a copy-forward. The explicit negative (no milestone, no ## Dependencies topology, no ready-for: label — none appearing anywhere in the script) verified by grep over the file at origin/main: no match, exit 1.
  • [PASS] AC3 — which seam carries the refusal, and why the other is not redundant. ## Decision, "Both seams, because the pool filter alone has a hole" plus "Neither seam is redundant": the direct-handoff hole is named, and the reverse case (a refusal that only fires at the last step trains people to route around it) is argued rather than asserted.
  • [PASS] AC4 — the v1-vs-fabrika side under ADR 0238, with the interim exposure named. "Fabrika-only" (0238 verified: fabrika re-implements v1, never calls it, keeping v1 deletable) plus ## Consequences "The interim gap is real and accepted": nothing mechanical enforces scope until Build the scope-admission predicate in fabrika-cli — a pure core plus its verb #5015/Wire scope admission into the fabrika build pool and claim seams #5016 land; enforcement is social; the mitigation is stated as landing the two build issues rather than bridging in v1.
  • [PASS] AC5 — dependency eligibility kept a separate predicate. Named in ## Context (build eligible, derived from the parent ledger's ## Dependencies) and hard-bound in the Binding constraints: never merged into a single answer, never a single exit code, and a scope refusal never reads as blocked. The strongest of the six.
  • [PASS] AC6 — path-hygiene and no personal identifiers. leak-guard over the added lines: clean, exit 0. No handle or address in the file (grep: no match). The ruling is attributed by role and date, not by name.

Doc hygiene

ADR contradiction sweep (Step 4a)

  • [PASS] 241 ADRs scanned, 198 live-accepted + uncited in scope; the ADR cites 0072, 0075, 0210, 0219, 0222, 0238. All 8 mechanical shortlist entries opened (0128, 0202, 0241, 0229, 0243, 0239, 0242, 0215) — none rules on a question this ADR re-decides. Semantic pass by hand on top, since a clean mechanical sweep proves nothing.
    • 0202 (forward-motion doctrine) is the closest: it governs p0 minting and homing, and is already amended-in-part by 0210/0214/0219/0222 — three of which this ADR cites. Scope admission is a different question from banding, and the ADR says so.
    • 0128 (glossary coinage trigger) is satisfied, not contradicted: the required vocabulary-impact section is present, routing scope admission to the glossary via Glossary: add a row for 'scope admission' (coined by ADR 0245) #5069 — the same shape 0202 used.
    • 0215 (claim identity) governs who owns a claim; this adds whether a claim is admitted. 0229/0241/0242/0243/0239 are adjacent on fabrika/pipeline vocabulary only.
  • The author's 0210 and 0222 judgement checked, not taken — and it holds. 0210 bans "any merge-blocking direction/roadmap-conformance gate on a finished PR"; this fence fires at pick and claim, before a build is paid for, which is 0210's own stated rationale for banning the merge-time gate. It extends 0210's principle rather than colliding with it, and 0210 needs no status-line edit. 0222 defines the p0 band as necessary-and-unblocked at triage time; the ADR correctly separates that from build eligible's build-time topology read and from scope admission as a third question. Naming that three-way near-collision explicitly is good work — it is the exact spot a sloppier record would have conflated.

Deviation disclosure (§DEV)

  • [PASS] deviation-disclosure — the body's ## Deviations section discloses three departures, and I verified each independently rather than taking the disclosure:

    1. Cherry-pick instead of rebase. Confirmed harmless and content-neutral. The branch carries exactly one commit over its base, and the ADR blob at the head is byte-identical to the original authored commit — the same blob hash 8fe48041... on both sides. For a one-commit branch this is indistinguishable from a rebase. No edit was made in transit.
    2. Explicit refspec instead of the standard push verb. Confirmed: the remote branch ref resolves to the reviewed head, matching the PR's head exactly — which is the property the verb exists to establish.
    3. ADR authored elsewhere, unmodified here. Confirmed by the byte-identity above.

    Nothing undisclosed that this gate could see.

ADR numbering


Two things for the merge actor, neither a gate failure:

  1. CI is red at this head, but not because of this PR. The full check set at 6f729f73 is 46 runs: 38 success, 7 skipped, 1 failure — check docs have no dead internal links. The dead link lives in ADR 0244, which points at a corpus/review-code.json under the fabrika-cli eval corpus; main carries corpus/review.json (renamed by the one-review-eval-stage work). It is pre-existing breakage on main that will red every PR until it is fixed forward. This PR introduces no dead link — every link it adds resolves. Flagging because ship-it requires green CI, so this needs a fix-forward on main before this PR can bank.

  2. One forward-looking gap, out of scope for Decide where a campaign-scope refusal binds — and which side of the v1/fabrika cut it lands on #5011, worth carrying into Build the scope-admission predicate in fabrika-cli — a pure core plus its verb #5015. ADR 0208 makes axis:pipeline-hardening and wayfinder:backlog permanently milestone-less standing lanes by design, and 0210 says platform work is "budgeted, never exempted and never judged." A campaign-scope refusal keyed on the focused campaign would, if the predicate is built naively, refuse all standing-lane work whenever a product campaign is in focus. This is not a defect in this ADR: Decide where a campaign-scope refusal binds — and which side of the v1/fabrika cut it lands on #5011 explicitly puts "which issues are in campaign" out of scope, the predicate is Build the scope-admission predicate in fabrika-cli — a pure core plus its verb #5015's deliverable, and the build pick contract already models a standing-lane label as a home alongside a milestone number. Recording it so it lands as a requirement on Build the scope-admission predicate in fabrika-cli — a pure core plus its verb #5015 rather than as a surprise at wiring time.

Minor, no action: the ADR's "milestone ... never as a filter" is precise about the candidate-pool script (verified) but slightly overshoots for the v1 write-code skill, whose explicit work milestone N drain mode does scope the pool by milestone via REST. The ADR is faithful to that skill's own headline claim ("the pickability predicate is unchanged"), and the point does not move the decision.

Verdict-written: 2026-08-09T07:23:07Z

@usirin usirin added the status:cp-banked Control-plane PR banked on the board, awaiting a control-plane approval label Aug 9, 2026
@usirin
usirin requested a review from notusirin August 9, 2026 07:24
@usirin

usirin commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

review-doc: advisory — blocking-set PR (§CP — approval-gated)

Re-gate at a moved head. Bookkeeping, not a re-review of authored content. This PR already carried a full review-doc advisory PASS at head 6f729f73 (comment 5230345719, all six ACs + hygiene). The branch was then updated onto latest main to clear a repo-wide doc-links break, which moved the head and staled that verdict by head-move alone (ADR 0058). This comment re-binds the same verdict to the current head, after proving the authored content is untouched.

Reviewed-head: @ d74e661

§CP classification — re-run on both axes at this head. Path axis: .decisions/** matches no owned CODEOWNERS path — not path-§CP. Content axis: pipeline-cli guard-content-probe classify returns guard-touching (§CP, ADR 0164) [guard-vocabulary-match]. So this PR is §CP by content, unchanged from the prior gate. My verdict is advisory only — it does not authorize a merge. Under the §CP hard gate (ADR 0135) a @kamp-us/control-plane member approves at the current head and ship-it then enqueues (ADR 0048); there is no hand-merge in the §CP path.

Namespace set — re-derived, not assumed. pipeline-cli class-probe classify --namespaces over the changed-file list: 1 changed file(s) → has-docs → exactly one required namespace, review-doc. No review-code, no review-skill, no review-design. This comment covers the whole required set.

The merge dragged in nothing. The head is a merge commit, parents 6f729f73 (the previously-gated head) and e2f877d2 (which is origin/main's current tip exactly). Three independent proofs the authored change is byte-identical and unwidened:

  • git diff 6f729f73 d74e6614 -- .decisions/0245-campaign-scope-fence-binds-both-seams.md → empty.
  • The ADR blob hashes identically at both heads: 8fe4804142efb2e2133225bde25ac81333eb7a2a.
  • git diff --name-only e2f877d2 d74e6614 → exactly one path, the ADR. Nothing else in the head tree differs from current main.
  • The PR's own changed-file set (REST) is still that one file.

Acceptance criteria (#5011) — re-bound, all six PASS. The issue body's checklist is unchanged (six criteria, no reviewer-appended row since the prior gate), and the ADR content is byte-identical, so every prior-round verdict transfers unaltered:

  • [PASS] AC1 — record landed under .decisions/ naming the binding seam.
  • [PASS] AC2 — sourced citation of the v1 candidate-pool selection facts, with the explicit negative (no milestone, no ## Dependencies topology, no ready-for:). Verified character-exact against origin/main in the prior round; content unchanged.
  • [PASS] AC3 — which seam carries the refusal and why neither is redundant.
  • [PASS] AC4 — the v1-vs-fabrika side under ADR 0238, interim exposure and mitigation named.
  • [PASS] AC5 — dependency eligibility kept a distinct predicate, hard-bound in the Binding constraints.
  • [PASS] AC6 — path hygiene and no personal identifiers.

Doc hygiene — re-checked at this head.

  • [PASS] House-format — frontmatter id/title/status/date/tags; ## Context / ## Decision / ## Consequences present.
  • [PASS] Index row — no .decisions/index.md committed (ADR 0126); purely additive, one file; status: accepted is coherent with a settled ruling.
  • [PASS] Links resolve — check docs have no dead internal links is green at this head (see below); the six relative ADR links and the glossary link all resolve in the merged tree.
  • [PASS] No leaked local paths — scan changed files for leaks green at this head; the ADR cites repo-relative paths only.
  • [PASS] Supersession / contradiction sweep — carried forward from the prior round's full sweep of the live accepted corpus (241 ADRs, no conflict; the 0210 / 0222 judgement holds and no supersede is owed; ADR 0128 satisfied). Re-verified at this head only for corpus drift, below.
  • [PASS] Single Diátaxis mode — explanation, no type-mixing.
  • [PASS] Clear, concise prose — active voice, no AI-tell density.

ADR numbering still unique. Max merged ADR on fresh origin/main is 0244; 0245 appears exactly once in the head tree and nowhere on main. CI agrees: validate ADR files — no duplicate/mismatched ADR number is success at this head. ADR 0244 was amended on main tonight (a dated ## Amendment correcting two stale corpus filenames) — expected, and it neither collides with nor is contradicted by 0245.

The doc-links check is now GREEN — the sole prior blocker is cleared. check docs have no dead internal links: success at d74e6614. Its cause (a stale corpus link in ADR 0244) was fixed on main and this merge pulls that fix in.

Full check set at this head, read after it settled: 0 failures, 0 cancellations, 0 timeouts. All 36 completed checks are success; the skipped set is the ordinary path-filtered code lane (deploy, e2e, integration, unit, lint/typecheck) for a docs-only diff. One check is still in_progress: produce run-evidence bundle — a post-hoc evidence job, not a merge gate; ship-it will read the settled set at merge time regardless.

Trap re-checked specifically: scan PR commits for secrets walks merge-base..HEAD commits, and the merge changed that commit set (it now includes the merge commit). Re-read at this head: success.

Not re-raised (already filed): the naive-scope-predicate gap the prior round surfaced — a scope predicate keyed on milestone presence would refuse all standing-lane work, since ADR 0208 leaves axis:pipeline-hardening and wayfinder:backlog permanently milestone-less. Triage folded it into #5015 as an acceptance criterion. It does not block this ADR.

Verdict: substantively merge-ready, advisory only. Nothing authored changed; every criterion and hygiene check re-binds to d74e66142d640cae9536e832135d121a83916f18. Awaiting a @kamp-us/control-plane approval at this head, after which ship-it enqueues. I do not merge.

@usirin
usirin requested a review from notusirin August 9, 2026 08:37
@usirin
usirin added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit 2cf1f88 Aug 9, 2026
46 checks passed
@usirin
usirin deleted the umut/adr-0245-campaign-scope-fence branch August 9, 2026 08:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:cp-banked Control-plane PR banked on the board, awaiting a control-plane approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decide where a campaign-scope refusal binds — and which side of the v1/fabrika cut it lands on

2 participants