Story Statement
As a maintainer running the coverage guardrail
I want CI to persist a raised coverage baseline back to tech/coverage-baseline.md — opt-in, ratcheting only upward
So that improvements are locked in automatically instead of relying on someone remembering to bump the file by hand, while the human-committed default stays available for projects that want the baseline under review
Where: .github/workflows/ci.yml's Coverage guardrail step + the shipped coverage-gate.sh, driven by a flag in way-of-working.md › Quality Gates.
Epic Context
Parent Epic: nessuna — follow-up di #282 (coverage baseline + CI guardrail). Apparteneva a #210, chiusa il 26/07: serve una decisione di riallocazione, non un'assegnazione automatica.
Status: Refined
Priority: P2 (Could-Have) — era P3/deferred nel piano; resta la meno urgente delle tre del batch.
Status Workflow
- Refined: Story is detailed, estimated, and ready for development
- In Progress: Story is actively being developed
- Done: Story delivered and accepted
Classification
risk:yellow · cost:green · coupling: not assessed — no domain artifacts
Matrix — per dimension
| Dimension |
Tier |
Source |
Note |
| Service/domain criticality |
yellow |
KB default |
no Criticality Table declared ⇒ Medium (D21) |
| Change/diff risk |
yellow |
story scope |
touches CI; a wrong loop guard costs every subsequent run, and a write-back needs a token |
| Business impact |
green |
subdomain class |
Integration & Process Standardization (Supporting); no product surface |
| Security relevance |
yellow |
path heuristic |
introduces a write credential into CI and a commit from an automated actor — authorization surface, even though it grants nothing to a PR author |
| Coupling balance |
not assessed |
absent |
no new integration point |
Acceptance Criteria
Functional Requirements
Given-When-Then Format:
-
Given Coverage baseline commit-back: disabled (the default, and the framework default)
When CI runs with coverage above the baseline
Then nothing is written and behaviour is byte-identical to today — the baseline stays human-committed (option A)
-
Given the flag is enabled and a run measures line coverage above a baseline.<type> value
When the guardrail step completes
Then the new value is written to .pair/adoption/tech/coverage-baseline.md and committed back, and the commit message marks it as automated
-
Given the flag is enabled and coverage is equal to or below the baseline
When the guardrail step completes
Then nothing is committed — the ratchet only ever moves up, and a drop still fails the gate exactly as now
-
Given a commit-back has just landed
When CI reacts to it
Then it does not start another coverage run that commits again — the loop terminates, proven by an explicit skip condition, not by luck
-
Given the run is a pull request rather than a push to the base branch
When coverage is above the baseline
Then the behaviour is the one this story explicitly decides and documents (write to the PR branch, or defer to the post-merge run) — never left implicit, because a PR from a fork has no write access
-
Given the write-back is refused (missing token, protected branch, insufficient scope)
When the step runs
Then the run degrades to a warning naming the reason and the gate's verdict is unchanged — a persistence failure must never turn a passing gate red, nor a failing one green
Business Rules
- Ratchet is monotonic: the file is only ever raised. Lowering a baseline stays a deliberate human commit.
- Default off: the framework default ships
disabled; pair's own opt-in is a project-level line in way-of-working.md, exactly as Coverage guardrail itself is.
- The gate's verdict and the persistence are independent:
coverage-gate.sh decides pass/fail; the write-back is a side effect that can fail alone.
- Least privilege: the write credential is scoped to what the commit needs and is documented; no broader token is introduced for convenience.
- Config format unchanged:
^baseline.<type>=<int> lines (today baseline.shared=84, baseline.frontend=19) — the writer edits values in place, never rewrites the surrounding markdown.
Edge Cases and Error Handling
- Invalid Input: a malformed/absent
coverage-summary.json keeps the conservative fail-safe (TIER=red ⇒ fail) and commits nothing.
- System Errors: a push rejected by branch protection → warning + unchanged verdict (AC6).
- Boundary Conditions: coverage exactly equal to the baseline ⇒ no write (AC3). A type present in the report but absent from the config ⇒ no write, reported.
- Exceptional Scenarios: two concurrent runs both raising the same baseline — the second must not clobber a higher value written by the first (re-read before write, or push with lease semantics).
Definition of Done Checklist
Dependencies
Notes
Origin: filed during #281's review round 2 as the deferred half of the coverage work. The plan classified it P3, deliberately deferred ("da riprendere solo dopo il lavoro ad alto valore") — it enters this batch because it is one of the few residual stories that is file-disjoint from the six parked PRs, not because its priority rose.
Task Breakdown
Dependency Graph
T-1 ──┬── T-2 ── T-3 ──┬── T-4
│ └── T-5
└── T-6
AC Coverage
| AC |
Tasks |
| AC-1 (default off, unchanged) |
T-3, T-6 |
| AC-2 (raise ⇒ commit back) |
T-2, T-3 |
| AC-3 (never lowers) |
T-2 |
| AC-4 (no CI loop) |
T-4 |
| AC-5 (PR vs push decided) |
T-1, T-3 |
| AC-6 (refusal degrades) |
T-5 |
T-1: Decide and record PR-vs-push behaviour + credential model
Priority: P0 | Estimated Hours: 2h | Bounded Context: Integration & Process Standardization
Summary: The two decisions that shape everything else: does a PR run write back, and with what credential.
Description: A fork PR has no write access, and after #234 the base branch is protected. Decide: write only on the post-merge push to the base branch (simplest, protection-compatible), or on the PR branch (immediate, but fails on forks). Record it as an ADL with the rejected option and why — this is the decision a later reader will question.
T-2: Monotonic ratchet writer
Priority: P0 | Estimated Hours: 3h | Bounded Context: Integration & Process Standardization
Summary: Given a measured coverage map and the config, produce the updated config — raising only.
Description: Lives in the owning package as a tested module (ADL 2026-07-13), not in the shell script. Edits ^baseline.<type>= values in place, leaving the markdown around them untouched. Re-reads before writing so a concurrent higher value is not clobbered. Unit-tested for: raise, equal, drop, unknown type, malformed config.
T-3: Flag + CI wiring
Priority: P0 | Estimated Hours: 2h | Bounded Context: Integration & Process Standardization
Summary: Coverage baseline commit-back flag read from the adoption; the CI step delegates to the module.
T-4: Loop termination
Priority: P0 | Estimated Hours: 2h | Bounded Context: Integration & Process Standardization
Summary: The automated commit must not trigger another coverage run that commits again.
Description: Explicit skip predicate (commit-message marker or actor check) — not [skip ci] alone if the coverage job is what must be skipped. Demonstrate it: a real sequence, or a smoke assertion over the predicate. An infinite CI loop is the failure mode that makes this story net-negative.
T-5: Degradation on refused write
Priority: P1 | Estimated Hours: 1h | Bounded Context: Integration & Process Standardization
Summary: Missing token / protected branch / insufficient scope ⇒ warning, verdict untouched.
T-6: Documentation
Priority: P1 | Estimated Hours: 1h | Bounded Context: Integration & Process Standardization
Summary: The flag, its default (disabled), the token requirement and the PR-vs-push decision, in the KB guideline and the adoption line — where a consumer reads them, not only in the workflow YAML.
Story Statement
As a maintainer running the coverage guardrail
I want CI to persist a raised coverage baseline back to
tech/coverage-baseline.md— opt-in, ratcheting only upwardSo that improvements are locked in automatically instead of relying on someone remembering to bump the file by hand, while the human-committed default stays available for projects that want the baseline under review
Where:
.github/workflows/ci.yml'sCoverage guardrailstep + the shippedcoverage-gate.sh, driven by a flag inway-of-working.md› Quality Gates.Epic Context
Parent Epic: nessuna — follow-up di #282 (coverage baseline + CI guardrail). Apparteneva a #210, chiusa il 26/07: serve una decisione di riallocazione, non un'assegnazione automatica.
Status: Refined
Priority: P2 (Could-Have) — era P3/deferred nel piano; resta la meno urgente delle tre del batch.
Status Workflow
Classification
risk:yellow·cost:green· coupling: not assessed — no domain artifactsMatrix — per dimension
Acceptance Criteria
Functional Requirements
Given-When-Then Format:
Given
Coverage baseline commit-back: disabled(the default, and the framework default)When CI runs with coverage above the baseline
Then nothing is written and behaviour is byte-identical to today — the baseline stays human-committed (option A)
Given the flag is
enabledand a run measures line coverage above abaseline.<type>valueWhen the guardrail step completes
Then the new value is written to
.pair/adoption/tech/coverage-baseline.mdand committed back, and the commit message marks it as automatedGiven the flag is
enabledand coverage is equal to or below the baselineWhen the guardrail step completes
Then nothing is committed — the ratchet only ever moves up, and a drop still fails the gate exactly as now
Given a commit-back has just landed
When CI reacts to it
Then it does not start another coverage run that commits again — the loop terminates, proven by an explicit skip condition, not by luck
Given the run is a pull request rather than a push to the base branch
When coverage is above the baseline
Then the behaviour is the one this story explicitly decides and documents (write to the PR branch, or defer to the post-merge run) — never left implicit, because a PR from a fork has no write access
Given the write-back is refused (missing token, protected branch, insufficient scope)
When the step runs
Then the run degrades to a warning naming the reason and the gate's verdict is unchanged — a persistence failure must never turn a passing gate red, nor a failing one green
Business Rules
disabled; pair's own opt-in is a project-level line inway-of-working.md, exactly asCoverage guardrailitself is.coverage-gate.shdecides pass/fail; the write-back is a side effect that can fail alone.^baseline.<type>=<int>lines (todaybaseline.shared=84,baseline.frontend=19) — the writer edits values in place, never rewrites the surrounding markdown.Edge Cases and Error Handling
coverage-summary.jsonkeeps the conservative fail-safe (TIER=red⇒ fail) and commits nothing.Definition of Done Checklist
coverage-gate.shlogic covered by unit tests in its owning package; the workflow step stays a thin entrypoint (ADL 2026-07-13)pnpm quality-gategreenpr-template.mdDependencies
coverage-gate.sh..github/workflows/ci.ymland.pair/knowledge/assets/coverage-gate.share untouched by all six, so this story is parallel-safe with the parked batch.pair-reviewandpair-explicit-approvalrequired contexts and flipsenforce_admins: trueonce a PR has merged through the rule. A CI job that pushes a commit to a protected base branch must satisfy that protection, or be exempted deliberately. AC5/AC6 must be decided with that regime in mind rather than against today's unprotectedmain— otherwise this lands working and breaks the day [US-234] feat: PR state flow (gate≠review) + pair review as required check #390 merges.Notes
Origin: filed during #281's review round 2 as the deferred half of the coverage work. The plan classified it P3, deliberately deferred ("da riprendere solo dopo il lavoro ad alto valore") — it enters this batch because it is one of the few residual stories that is file-disjoint from the six parked PRs, not because its priority rose.
Task Breakdown
Dependency Graph
AC Coverage
T-1: Decide and record PR-vs-push behaviour + credential model
Priority: P0 | Estimated Hours: 2h | Bounded Context: Integration & Process Standardization
Summary: The two decisions that shape everything else: does a PR run write back, and with what credential.
Description: A fork PR has no write access, and after #234 the base branch is protected. Decide: write only on the post-merge push to the base branch (simplest, protection-compatible), or on the PR branch (immediate, but fails on forks). Record it as an ADL with the rejected option and why — this is the decision a later reader will question.
T-2: Monotonic ratchet writer
Priority: P0 | Estimated Hours: 3h | Bounded Context: Integration & Process Standardization
Summary: Given a measured coverage map and the config, produce the updated config — raising only.
Description: Lives in the owning package as a tested module (ADL 2026-07-13), not in the shell script. Edits
^baseline.<type>=values in place, leaving the markdown around them untouched. Re-reads before writing so a concurrent higher value is not clobbered. Unit-tested for: raise, equal, drop, unknown type, malformed config.T-3: Flag + CI wiring
Priority: P0 | Estimated Hours: 2h | Bounded Context: Integration & Process Standardization
Summary:
Coverage baseline commit-backflag read from the adoption; the CI step delegates to the module.T-4: Loop termination
Priority: P0 | Estimated Hours: 2h | Bounded Context: Integration & Process Standardization
Summary: The automated commit must not trigger another coverage run that commits again.
Description: Explicit skip predicate (commit-message marker or actor check) — not
[skip ci]alone if the coverage job is what must be skipped. Demonstrate it: a real sequence, or a smoke assertion over the predicate. An infinite CI loop is the failure mode that makes this story net-negative.T-5: Degradation on refused write
Priority: P1 | Estimated Hours: 1h | Bounded Context: Integration & Process Standardization
Summary: Missing token / protected branch / insufficient scope ⇒ warning, verdict untouched.
T-6: Documentation
Priority: P1 | Estimated Hours: 1h | Bounded Context: Integration & Process Standardization
Summary: The flag, its default (
disabled), the token requirement and the PR-vs-push decision, in the KB guideline and the adoption line — where a consumer reads them, not only in the workflow YAML.