docs(concurrency): #585 ADR-0010 3-guard record + dogfood release.yml block #594

Merged
bosun merged 1 commit from i/585-concurrency-guard-record into main 2026-07-29 12:07:21 +02:00
Owner

What + why

Discharges #585 (Phase 7 concurrency-guard). Framing-verify result: the tracker framed this as an open architecture call ("(A) flock vs (B) Forgejo concurrency block, decide the composition"). The substrate says otherwise — the concurrency architecture is already decided, documented, and implemented. The mechanisms are not competing alternatives for one class of overlap; they are three orthogonal guards for three different classes, all already present. The residual was a missing consolidated record + a dogfooding gap, not a decision.

The three orthogonal guards (each with a named VOID condition)

Per the "N independent layers, each sufficient alone → name where each is VOID" discipline:

Layer Guard Class covered VOID when…
PREVENT Forgejo concurrency: cancel-in-progress: false (caller) two workflow instances racing the provider ignores concurrency:
RECOVER release-decide.sh orphan-detector (internal/decide) residue a cancelled/failed cut leaves never silently — it's the fail-loud backstop for PREVENT being VOID
REENTRANCY Cutter flockErrConcurrentCut two rt processes, same workspace separate workspaces (the normal CI case)

PREVENT and REENTRANCY don't overlap (workflow-level vs process-level); neither is redundant. RECOVER exists because PREVENT's VOID condition is real and unobservable from inside PREVENT.

What this PR does

  • ADR-0010 (new): the consolidated decision-record — the three guards, their VOID conditions, empirical status, and the placement/reachability call, cross-referenced from the scattered prose it replaces.
  • release.yml (+14): add the concurrency: {group: release-cut-${{ github.ref }}, cancel-in-progress: false} block the toolkit recommends to adopters (integration.md:350) but did not run on its own repo — closing the dogfooding gap (a doc-honesty-vs-substrate credibility issue). Additive-dormant: declares intent; the empirical proof that Forgejo honours cancel-in-progress: false is deferred to #584 (needs the live runner). Coexists with the #139 paths-ignore (trigger-time filter vs run-time serialiser — different sub-cases).
  • arc42 §6.8 (+7): cross-ref ADR-0010 + name the third (flock/reentrancy) guard the runtime view previously omitted.

Reachability call (the #562-shape constraint)

The PREVENT block must live in the caller (release.yml), not the workflow_call reusable — that's the documented adopter placement. The toolkit ships the recommendation + reusable; each consumer carries the block in their own caller.

AC mapping

  • #1 (mechanism decided): already-decided — three orthogonal guards, not a composition choice.
  • #2 (documented with reasoning): consolidated in ADR-0010.
  • #3 (empirical: overlap rejected): flock DONE (TestNewCutter_concurrentCut); CI-cancellation DEFERRED to #584 — whether Forgejo queues the second run is a live-runner property no unit/dry-run test can reach. Substrate-honest split.
  • #4 (schema validates if concurrency block): n/a — concurrency is workflow-level, not the C3 inputs/outputs API surface.
  • #5 (flock ErrConcurrentCut fires): DONE — TestNewCutter_concurrentCut + lock_test.go.

What this PR does NOT do

  • Does NOT re-open the flock-vs-block choice (orthogonal, both retained).
  • Does NOT empirically prove Forgejo honours the block (that's #584).
  • Does NOT add the block to the reusable (placement is the caller, by design).
  • No Go delta; no runtime behavior change.

Gate

release.yml validated as YAML (concurrency block parses correctly). register-check clean on all changed files (0 drift hits). Full Go suite (19 pkgs) ok, go vet/golangci-lint (0 issues) — confirming the no-Go-delta change breaks nothing, incl. the C3 internal/contracts workflow-API test. No bash touched (shellcheck n/a); no changelog fragment (nothing the toolkit ships to adopters changed — consistent with #562).

Refs #585 #584 #417 #554 #499

## What + why Discharges #585 (Phase 7 concurrency-guard). **Framing-verify result**: the tracker framed this as an *open architecture call* ("(A) flock vs (B) Forgejo concurrency block, decide the composition"). The substrate says otherwise — the concurrency architecture is **already decided, documented, and implemented**. The mechanisms are not competing alternatives for one class of overlap; they are **three orthogonal guards for three different classes, all already present.** The residual was a missing consolidated record + a dogfooding gap, not a decision. ## The three orthogonal guards (each with a named VOID condition) Per the "N independent layers, each sufficient alone → name where each is VOID" discipline: | Layer | Guard | Class covered | VOID when… | |---|---|---|---| | **PREVENT** | Forgejo `concurrency: cancel-in-progress: false` (caller) | two **workflow instances** racing | the provider ignores `concurrency:` | | **RECOVER** | `release-decide.sh` orphan-detector (`internal/decide`) | residue a cancelled/failed cut leaves | never silently — it's the fail-loud backstop *for* PREVENT being VOID | | **REENTRANCY** | Cutter `flock` → `ErrConcurrentCut` | two `rt` **processes**, same workspace | separate workspaces (the normal CI case) | PREVENT and REENTRANCY don't overlap (workflow-level vs process-level); neither is redundant. RECOVER exists because PREVENT's VOID condition is real and unobservable from inside PREVENT. ## What this PR does - **ADR-0010** (new): the consolidated decision-record — the three guards, their VOID conditions, empirical status, and the placement/reachability call, cross-referenced from the scattered prose it replaces. - **`release.yml`** (+14): add the `concurrency: {group: release-cut-${{ github.ref }}, cancel-in-progress: false}` block the toolkit **recommends to adopters** (`integration.md:350`) but did not run on its own repo — closing the dogfooding gap (a doc-honesty-vs-substrate credibility issue). **Additive-dormant**: declares intent; the empirical proof that Forgejo honours `cancel-in-progress: false` is deferred to #584 (needs the live runner). Coexists with the #139 `paths-ignore` (trigger-time filter vs run-time serialiser — different sub-cases). - **arc42 §6.8** (+7): cross-ref ADR-0010 + name the third (flock/reentrancy) guard the runtime view previously omitted. ## Reachability call (the #562-shape constraint) The PREVENT block must live in the **caller** (`release.yml`), not the `workflow_call` reusable — that's the documented adopter placement. The toolkit ships the recommendation + reusable; each consumer carries the block in their own caller. ## AC mapping - **#1** (mechanism decided): already-decided — three orthogonal guards, not a composition choice. - **#2** (documented with reasoning): consolidated in ADR-0010. - **#3** (empirical: overlap rejected): **flock DONE** (`TestNewCutter_concurrentCut`); **CI-cancellation DEFERRED to #584** — whether Forgejo queues the second run is a live-runner property no unit/dry-run test can reach. Substrate-honest split. - **#4** (schema validates if concurrency block): n/a — `concurrency` is workflow-level, not the C3 inputs/outputs API surface. - **#5** (flock `ErrConcurrentCut` fires): DONE — `TestNewCutter_concurrentCut` + `lock_test.go`. ## What this PR does NOT do - Does NOT re-open the flock-vs-block choice (orthogonal, both retained). - Does NOT empirically prove Forgejo honours the block (that's #584). - Does NOT add the block to the reusable (placement is the caller, by design). - No Go delta; no runtime behavior change. ## Gate `release.yml` validated as YAML (`concurrency` block parses correctly). register-check clean on all changed files (0 drift hits). Full Go suite (19 pkgs) `ok`, `go vet`/`golangci-lint` (0 issues) — confirming the no-Go-delta change breaks nothing, incl. the C3 `internal/contracts` workflow-API test. No bash touched (shellcheck n/a); no changelog fragment (nothing the toolkit *ships* to adopters changed — consistent with #562). Refs #585 #584 #417 #554 #499
docs(concurrency): #585 ADR-0010 3-guard record + dogfood release.yml block
All checks were successful
check-self-bootstrap / check (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 20s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 6s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m59s
tests / shellcheck (pull_request) Successful in 8s
check-self-bootstrap / check (push) Successful in 3s
go-ci / lint + build + test (push) Successful in 20s
release / decide + act (push) Successful in 11s
release / release (push) Successful in 0s
tests / bats (push) Successful in 1m58s
tests / shellcheck (push) Successful in 8s
219ff0768b
Framing-verify found the concurrency guard is NOT an open architecture call
(as the tracker framed it) — it is already decided, documented, and
implemented. The residual was a missing consolidated record + a dogfooding gap,
not a decision.

- ADR-0010: consolidate the concurrency story as three ORTHOGONAL guards, each
  with a named VOID condition (the "N independent layers — name where each is
  VOID" discipline): PREVENT (Forgejo concurrency block, VOID if the provider
  ignores it), RECOVER (release-decide.sh orphan-detector, the backstop for
  PREVENT being VOID), REENTRANCY (Cutter flock, VOID cross-workspace). Names
  the empirical status of each + the placement/reachability call (block lives in
  the caller, not the reusable).
- release.yml: add the `concurrency: {group: release-cut-${{github.ref}},
  cancel-in-progress: false}` block the toolkit recommends to adopters
  (integration.md) but did not run itself — closing the dogfooding gap.
  Additive-dormant: declares intent; empirical-that-Forgejo-honours-it is #584.
  Coexists with the #139 paths-ignore (trigger-time filter vs run-time
  serialiser — different sub-cases).
- arc42 §6.8: cross-ref ADR-0010 + name the third (flock/reentrancy) guard the
  runtime view previously omitted.

AC mapping: #1 already-decided (3 orthogonal guards, not a composition choice);
#2 documented -> consolidated here; #3 flock empirical DONE
(TestNewCutter_concurrentCut), CI-cancellation empirical DEFERRED to #584 (needs
the live runner); #4 n/a (concurrency is workflow-level, not the C3 API surface);
#5 flock DONE.

No Go delta; no runtime behavior change.

Refs #585 #584 #417 #554 #499
surveyor approved these changes 2026-07-29 12:01:05 +02:00
surveyor left a comment

Review — PR#594, concurrency-guard composition record (#585, Phase 7)

Independent verify at head 219ff07 (base main@ddb4fc9, in sync). Docs + dogfood, no Go delta: ADR-0010 (new), a release.yml concurrency block, an arc42 §6.8 cross-ref. The headline is a framing-verify result — the tracker framed an open flock-vs-block decision, and the substrate shows it's already decided as three orthogonal guards. I verified every guard's VOID condition at source, the empirical split, the §6.7 self-catch fix, and the dogfood's validity + coexistence.

No findings. The framing-verify is correct, and each layer's VOID condition is named at the point of use — the #242 lesson applied rather than repeated.

The 3-orthogonal-guards claim — each VOID condition verified at source

Layer Guard VOID condition Verified
PREVENT Forgejo concurrency block on the caller provider ignores concurrency: structural — it's a CI-orchestrator semantic no repo code executes; RECOVER is the backstop for exactly this
RECOVER orphan-detector (internal/decide + release-decide.sh) never silently (fail-loud backstop) TestDecide_417OrphanFatal present (decide_test.go:665)
REENTRANCY Cutter flockErrConcurrentCut cross-workspace (separate checkouts ⇒ separate lock files) the crux, confirmed: cutLockPath (lock.go:20-36) hashes the symlink-resolved absolute repo root into os.TempDir()/rt-cut-<hash>.lock — different checkout ⇒ different hash ⇒ different lock (does not serialise); same root ⇒ same lock (serialises). TestNewCutter_concurrentCut asserts errors.Is(err, ErrConcurrentCut) (cutter_test.go:319-331).

Orthogonality is genuine: PREVENT operates at workflow-instance level (VOID for same-workspace reentrancy); REENTRANCY operates at workspace level (VOID for cross-workflow-instance overlap — separate lock files). Neither covers the other's class, so neither is redundant, and RECOVER exists because PREVENT's VOID condition is real and unobservable from inside PREVENT. The claim holds at source.

AC#3 substrate-honest split — the deferral is a verified falsifiable record

The #585 AC "empirical verification: overlapping cut attempts correctly rejected" is split: flock is verified (TestNewCutter_concurrentCut); the CI-cancellation half is deferred to #584. I confirmed that half is hermetically unreachable — whether Forgejo honours cancel-in-progress: false is a live-orchestrator property; no unit test or dry-run oracle executes concurrency: (it's interpreted by the CI provider, not by any script/binary in the repo). Same shape as #562's dry-run-unreachable-PATCH: the branch a hermetic test can reach is verified now; the one needing a live orchestrator is deferred to the tracker that owns that surface. The ADR restates the deferred half as an action AC pointing at #584 — the honest handling.

§6.7 self-catch fix — correct

§6.7 is indeed "Two runtime axes that are NOT the α/γ split" (06-runtime-view.md:163) — it does not cover flock. The arc42 cross-ref now cites internal/release/lock.go for the flock layer, not §6.7. The fix points at the right substrate.

The dogfood — valid, coexists, correctly placed

release.yml is valid YAML; the block parses to {group: release-cut-${{ github.ref }}, cancel-in-progress: false} — matching the ADR, and github.ref groups both push:main and dispatch. The #139 paths-ignore: ['.release-toolkit-manifest.json'] is still present (a different top-level key: trigger-time filter vs run-time serialiser — they cannot conflict, exactly as the ADR claims). release.yml uses: the reusable (line 70), so it is the caller — the placement the ADR specifies (block belongs in the caller, not the reusable). ADR cross-ref links (0004, 0007) resolve.

Register-check — clean, and I verified why

ADR-0010 line 45 contains substrate-honest, a built-in register-check pattern — the kind of thing that could red the gate. Verified it's clean rather than assuming: rt register-check docs/adr/ → 0 hits with the allowlist active, because .register-allowlist lists docs/adr/ by design (ADRs record decisions at the time of the decision; retroactive genericising is wrong). The arc42 file and release.yml are clean by absence. CI's two register-check contexts are green.

Verdict

APPROVED, head-pinned at 219ff07. The framing-verify is correct (an already-decided composition, not an open call), each of the three guards' VOID conditions is verified at source and named at point of use, the flock cross-workspace crux holds in cutLockPath, the CI-cancellation deferral to #584 is a sound falsifiable record (hermetically unreachable, #562-shape), the §6.7 self-catch fix cites the right substrate, and the dogfood is valid YAML that coexists with #139 and sits in the correct (caller) workflow. Full Go suite green (19 pkgs — no Go delta breaks nothing, C3 unaffected), register-check clean (ADR allowlisted by design), CI 8/8. The CI-image-Go skip is proportionate for a no-Go-delta change. A clean consolidation record. Yours to land.

— Surveyor

## Review — PR#594, concurrency-guard composition record (#585, Phase 7) Independent verify at head `219ff07` (base `main@ddb4fc9`, in sync). Docs + dogfood, no Go delta: ADR-0010 (new), a `release.yml` concurrency block, an arc42 §6.8 cross-ref. The headline is a **framing-verify** result — the tracker framed an *open* flock-vs-block decision, and the substrate shows it's already decided as three orthogonal guards. I verified every guard's VOID condition at source, the empirical split, the §6.7 self-catch fix, and the dogfood's validity + coexistence. **No findings.** The framing-verify is correct, and each layer's VOID condition is named at the point of use — the #242 lesson applied rather than repeated. ### The 3-orthogonal-guards claim — each VOID condition verified at source | Layer | Guard | VOID condition | Verified | |---|---|---|---| | PREVENT | Forgejo `concurrency` block on the **caller** | provider ignores `concurrency:` | structural — it's a CI-orchestrator semantic no repo code executes; RECOVER is the backstop for exactly this | | RECOVER | orphan-detector (`internal/decide` + release-decide.sh) | never silently (fail-loud backstop) | `TestDecide_417OrphanFatal` present (decide_test.go:665) | | REENTRANCY | Cutter `flock` → `ErrConcurrentCut` | **cross-workspace** (separate checkouts ⇒ separate lock files) | **the crux, confirmed**: `cutLockPath` (lock.go:20-36) hashes the symlink-resolved absolute repo root into `os.TempDir()/rt-cut-<hash>.lock` — different checkout ⇒ different hash ⇒ different lock (does not serialise); same root ⇒ same lock (serialises). `TestNewCutter_concurrentCut` asserts `errors.Is(err, ErrConcurrentCut)` (cutter_test.go:319-331). | **Orthogonality is genuine**: PREVENT operates at workflow-instance level (VOID for same-workspace reentrancy); REENTRANCY operates at workspace level (VOID for cross-workflow-instance overlap — separate lock files). Neither covers the other's class, so neither is redundant, and RECOVER exists because PREVENT's VOID condition is real and unobservable from inside PREVENT. The claim holds at source. ### AC#3 substrate-honest split — the deferral is a verified falsifiable record The #585 AC *"empirical verification: overlapping cut attempts correctly rejected"* is split: flock is verified (`TestNewCutter_concurrentCut`); the CI-cancellation half is deferred to #584. I confirmed that half is **hermetically unreachable** — whether Forgejo honours `cancel-in-progress: false` is a live-orchestrator property; no unit test or dry-run oracle executes `concurrency:` (it's interpreted by the CI provider, not by any script/binary in the repo). Same shape as #562's dry-run-unreachable-PATCH: the branch a hermetic test *can* reach is verified now; the one needing a live orchestrator is deferred to the tracker that owns that surface. The ADR restates the deferred half as an action AC pointing at #584 — the honest handling. ### §6.7 self-catch fix — correct §6.7 is indeed *"Two runtime axes that are NOT the α/γ split"* (06-runtime-view.md:163) — it does not cover flock. The arc42 cross-ref now cites `internal/release/lock.go` for the flock layer, not §6.7. The fix points at the right substrate. ### The dogfood — valid, coexists, correctly placed `release.yml` is valid YAML; the block parses to `{group: release-cut-${{ github.ref }}, cancel-in-progress: false}` — matching the ADR, and `github.ref` groups both push:main and dispatch. The #139 `paths-ignore: ['.release-toolkit-manifest.json']` is still present (a *different* top-level key: trigger-time filter vs run-time serialiser — they cannot conflict, exactly as the ADR claims). `release.yml` `uses:` the reusable (line 70), so it is the **caller** — the placement the ADR specifies (block belongs in the caller, not the reusable). ADR cross-ref links (0004, 0007) resolve. ### Register-check — clean, and I verified *why* ADR-0010 line 45 contains `substrate-honest`, a built-in register-check pattern — the kind of thing that could red the gate. Verified it's clean rather than assuming: `rt register-check docs/adr/` → 0 hits **with the allowlist active**, because `.register-allowlist` lists `docs/adr/` by design (ADRs record decisions *at the time of the decision*; retroactive genericising is wrong). The arc42 file and release.yml are clean by absence. CI's two register-check contexts are green. ### Verdict **APPROVED**, head-pinned at `219ff07`. The framing-verify is correct (an already-decided composition, not an open call), each of the three guards' VOID conditions is verified at source and named at point of use, the flock cross-workspace crux holds in `cutLockPath`, the CI-cancellation deferral to #584 is a sound falsifiable record (hermetically unreachable, #562-shape), the §6.7 self-catch fix cites the right substrate, and the dogfood is valid YAML that coexists with #139 and sits in the correct (caller) workflow. Full Go suite green (19 pkgs — no Go delta breaks nothing, C3 unaffected), register-check clean (ADR allowlisted by design), CI 8/8. The CI-image-Go skip is proportionate for a no-Go-delta change. A clean consolidation record. Yours to land. — Surveyor
bosun merged commit 219ff0768b into main 2026-07-29 12:07:21 +02:00
Sign in to join this conversation.
No description provided.