Skip to content

The ratchet fails a run for obeying the allow file — teach it "intermittent" - #2324

Merged
rbuergi merged 3 commits into
mainfrom
fix/gate-ratchet-honours-intermittent-debt
Aug 26, 2026
Merged

rbuergi merged 3 commits into
mainfrom
fix/gate-ratchet-honours-intermittent-debt

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

.github/samples-gate.allow line 32 says, in a comment:

# PensionFund idempotence — INTERMITTENT known debt. Do not remove on a single green run.

The parser strips comments, so the ratchet cannot read that.

The bug

The ratchet's rule — a listed check that starts passing fails the run until its line is removed — assumes the check is deterministic, so one green run proves the debt is paid. For a flapping check that inference is wrong, and the git history shows the loop it produces:

commit action
768f9ba0b Drop the now-stale PensionFund idempotence allow entry
f32047e0e Restore the PensionFund idempotence entry — the check is intermittent

Since then, every run where PensionFund happens to pass fails the gate with stale allow entr(ies) and 0 new failures. That is what is currently reddening #2285, a PR that touches nothing related.

Why not just remove the entry

Both obvious fixes are wrong:

  • Remove it — the file explicitly forbids it, and it was already tried and reverted one commit later.
  • Wait for the flake to fail again — that is waiting on a coin flip, and the next PR hits the same wall.

So the intent moves out of a comment the parser discards and into a token it reads:

PensionFund idempotence intermittent

What the marker does, and what it deliberately does not

An intermittent entry is exempt from stale-detection and nothing else. It still tolerates only the scope/check it names; a failure it does not name is still a new failure. What it gives up is stale-detection for that one line — which is the entire point, not a side effect.

This is a real weakening of one specific guarantee, so it is deliberately narrow: it cannot be applied by accident (a typo throws), it cannot broaden what an entry suppresses, and it leaves the Unverifiable bucket's meaning intact.

Verified in both directions

A marker that cannot fail would be worse than the bug it fixes:

  • an intermittent entry whose check passes is not stale — gate stays green
  • an intermittent entry still suppresses its own failure — unchanged behaviour
  • a typo in the marker (intermitent) throws FormatException rather than silently degrading to a plain entry, which would restore the stale-failure with nothing pointing at the cause

22/22 in MeshWeaver.PluginTester.Test, -c Release -warnaserror clean.

Scope

Found while diagnosing #2285's red. Its failure was Doc content compiles and runs — the same job name as the Northwind compile error fixed in #2308, but a different check inside it, which is why the fix that landed did not clear it.

…ittent"

.github/samples-gate.allow line 32 says, in a comment:

  # PensionFund idempotence — INTERMITTENT known debt. Do not remove on a single green run.

The parser strips comments, so the ratchet cannot read that. Its rule — a listed
check that starts passing fails the run until its line is removed — assumes a
check is DETERMINISTIC, so one green run proves the debt is paid. For a flapping
check that inference is wrong, and the history shows the loop it causes: the
entry was dropped as stale (768f9ba) and RESTORED one commit later
(f32047e, "the check is intermittent"). Since then every run where PensionFund
happens to pass fails the gate with "stale allow entr(ies)" and 0 new failures —
which is what is currently reddening #2285, a PR that touches none of this.

Removing the entry is the wrong fix twice over: the file forbids it, and it was
already tried and reverted. So the intent moves out of the comment and into a
token the parser reads:

  PensionFund idempotence intermittent

An intermittent entry is exempt from stale-detection and NOTHING else. It still
tolerates only the scope/check it names; a failure it does not name is still a
new failure. What it gives up is stale-detection for that one line, which is
the entire point.

Tests, in both directions — a marker that cannot fail would be worse than the
bug:
  • an intermittent entry whose check PASSES is not stale (gate stays green)
  • an intermittent entry still suppresses its own failure (unchanged)
  • a TYPO in the marker throws rather than silently degrading to a plain entry,
    which would restore the stale-failure with nothing pointing at the cause
22/22 in MeshWeaver.PluginTester.Test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 26, 2026 07:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the PluginTester gate allowlist “ratchet” to support an intermittent marker for known-debt entries whose checks can flap, so that a single passing run does not incorrectly classify the entry as stale and fail the gate.

Changes:

  • Extend .allow parsing to accept an optional third token (intermittent) and store it on AllowEntry.
  • Exempt intermittent entries from stale-detection when the corresponding check passes in the current run.
  • Add unit tests for intermittent behavior and update .github/samples-gate.allow to mark the PensionFund idempotence entry as intermittent.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
tools/MeshWeaver.PluginTester/GateAllowlist.cs Adds the intermittent marker support in parsing and verdict evaluation.
test/MeshWeaver.PluginTester.Test/GateAllowlistTest.cs Adds tests covering intermittent parsing and stale-detection behavior.
.github/samples-gate.allow Marks PensionFund idempotence as intermittent so the gate no longer fails on passing runs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/MeshWeaver.PluginTester/GateAllowlist.cs Outdated
Comment thread tools/MeshWeaver.PluginTester/GateAllowlist.cs Outdated
Comment thread tools/MeshWeaver.PluginTester/GateAllowlist.cs Outdated
rbuergi and others added 2 commits August 26, 2026 09:12
…is read

Two review corrections.

1. The signature gate was right and I was wrong. Adding `bool Intermittent = false`
   to AllowEntry's PRIMARY CONSTRUCTOR is a binary-breaking change to a public
   record — precisely the class "Public record signatures (binary compatibility)"
   exists to refuse, and precisely the class I already knew about. It caught it on
   the first push. The flag is now an init-only PROPERTY: same data, same parsing,
   ctor signature untouched.

2. The caveat moves into the FILE, next to the token. A reviewer's point, and the
   load-bearing half of the change: an `intermittent` marker's failure mode is
   SOCIAL, not technical — the next person with a deterministic red they cannot fix
   will find the token and reach for it. A rationale that lives only in a PR nobody
   reopens does not reach them; one written beside the entry does. It says what the
   token is for (a check that genuinely FLAPS), what it is not for (a failure that
   is merely inconvenient), what it buys (stale-detection exemption and nothing
   else), and how to prove the flap before adding one.

The XML doc also now records the symmetry a reviewer named: the ratchet already
refuses to infer from a check that did NOT run (Unverifiable — warned, never
failed). This refuses to infer from a check that ran once and happened to pass.
Both are "one observation is not proof".

22/22, -c Release -warnaserror clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three Copilot findings, all correct.

1. TWO <summary> blocks on one symbol — and it was worse than it read. My earlier
   edit anchored on the `Checks` declaration and inserted BEFORE it, which left
   `Checks`'s summary orphaned above the new marker doc: IntermittentMarker
   inherited a second summary, and `Checks` lost its documentation entirely. The
   summary is back on `Checks`; the marker keeps its own.

2. ToString() dropped the marker. The gate prints entries in its diagnostics
   ("unverifiable allow entry …: {entry}"), and an operator reading that output is
   looking for the LINE TO EDIT — a rendering that does not match the file cannot
   be pasted back into it. Now renders "<scope> <check> intermittent", pinned by a
   round-trip test in both shapes.

3. Passed(report, entry) was evaluated twice per entry, and it walks every package
   and type. Computed once.

23/23, -c Release -warnaserror clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi

rbuergi commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Strong diagnosis of the ratchet bug — the comment-stripping parser making the file unreadable to its own enforcer is exactly right, and the drop/restore commit pair you cite was mine.

But one input to the decision is wrong, and I wrote it. The PensionFund idempotence comment in .github/samples-gate.allow explains the intermittency like this:

Currency.DecimalPlaces carries a C# initializer = 2 … default-suppression drops the property whose value equals the type's default.

That mechanism is not real. DefaultIgnoreCondition = WhenWritingDefault suppresses at default(T) — for int that is 0, never a property's = 2 initializer. Serialization.md states it directly, and MeshNodeVersioning.md uses the same fact as its worked example (Version = 0 would be omitted, which is why versions start at 1). So "decimalPlaces": 2 is never dropped, and my explanation for why the check flaps was wrong.

The real cause was found while fixing #2266/#2271 in #2318: PackageInstaller.AsAuthored re-parsed file.Content with no JsonDocumentOptions — stricter than the primary parse, which strips a leading UTF-8 BOM. PensionFund/Currency/{CHF,EUR,USD}.json are BOM'd (verified by hex dump: EF BB BF), so pre-fix the installer wrote the materialised value on first install, and the second install's unchanged-skip compared against that wrongly-written value. #2318 shares the primary parse's tolerances and removes the wrong write at its source.

Why this matters for your PR: if #2318 lands, PensionFund idempotence may stop flapping and start passing deterministically — at which point it is a genuinely stale entry that should be removed, not marked intermittent. Marking it intermittent would then permanently exempt a check that is actually fixed, which is the failure mode your ratchet exists to prevent, one level up.

Two things worth doing either way, and they are independent of the order the PRs land in:

  1. Your ratchet change is right regardless — a flapping check must be expressible, and a rule readable only by humans is not a rule. Please keep it.
  2. Fix or drop that comment's mechanism while you are in the file, so the next reader does not inherit my wrong explanation. Happy to do it in Fix plugin installer writing values it should not (#2264, #2266, #2271) #2318 instead if you would rather not touch it — say which and I will.

Evidence for the BOM cause is in #2318 (InstallAuthoredContentTest gained a BOM case; full MeshWeaver.PluginCatalog.Test 647/647).

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

   10 files  ±0     10 suites  ±0   6m 9s ⏱️ -31s
1 310 tests ±0  1 309 ✅ ±0  1 💤 ±0  0 ❌ ±0 
1 311 runs  ±0  1 310 ✅ ±0  1 💤 ±0  0 ❌ ±0 

Results for commit 8ca950d. ± Comparison against base commit e61c1bb.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

    9 files  ±0      9 suites  ±0   5m 29s ⏱️ - 1m 4s
1 827 tests ±0  1 823 ✅ ±0  4 💤 ±0  0 ❌ ±0 
2 370 runs  ±0  2 366 ✅ ±0  4 💤 ±0  0 ❌ ±0 

Results for commit 8ca950d. ± Comparison against base commit e61c1bb.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

1 202 tests  ±0   1 202 ✅ ±0   15m 54s ⏱️ -29s
    9 suites ±0       0 💤 ±0 
    9 files   ±0       0 ❌ ±0 

Results for commit 8ca950d. ± Comparison against base commit e61c1bb.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 122 tests  ±0   1 822 ✅ ±0   12m 8s ⏱️ +5s
    9 suites ±0     300 💤 ±0 
    9 files   ±0       0 ❌ ±0 

Results for commit 8ca950d. ± Comparison against base commit e61c1bb.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 819 tests  +4   1 819 ✅ +4   9m 33s ⏱️ +12s
   10 suites ±0       0 💤 ±0 
   10 files   ±0       0 ❌ ±0 

Results for commit 8ca950d. ± Comparison against base commit e61c1bb.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

3 438 tests  ±0   3 438 ✅ ±0   9m 38s ⏱️ +16s
    8 suites ±0       0 💤 ±0 
    8 files   ±0       0 ❌ ±0 

Results for commit 8ca950d. ± Comparison against base commit e61c1bb.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

    55 files  ±0      55 suites  ±0   58m 54s ⏱️ - 1m 30s
11 718 tests +4  11 413 ✅ +4  305 💤 ±0  0 ❌ ±0 
12 262 runs  +4  11 957 ✅ +4  305 💤 ±0  0 ❌ ±0 

Results for commit 8ca950d. ± Comparison against base commit e61c1bb.

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