Skip to content

Commit d06caf7

Browse files
PhysShellclaude
andcommitted
docs(evidence): N1 competitor gate pre-registration and reproductions
Four confirmed N1 fixes (whizbang#1204, rockbot#638, NexusKit#34, BareWire#73) as buggy/fixed reproductions with stubbed third-party types; CA2000 (default, AllPaths, ownership-transfer variants, AllPathsOnlyNotDisposed), IDisposableAnalyzers 4.0.8 and Sonar 10.35 with every rule on. GO-CANDIDATE only if >=2 pairs are missed by every configuration; harness control must pass first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TFkNAFFQz9KoTzpPws8vYG
1 parent 46e9a9d commit d06caf7

9 files changed

Lines changed: 653 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# N1 competitor gate — pre-registration
2+
3+
Written 2026-10-20, 21:11 +05. The four fix diffs have been read in order to build the reproductions. No analyzer
4+
has been run on any reproduction.
5+
6+
Authority: owner decision `GO-N1-COMPETITOR-GATE`, given after `KILL-N3-PILOT` was accepted. This gate is not a GO
7+
on an N1 PoC.
8+
9+
- **Budget.** 0.25 agent-day.
10+
- **Rules of the work.** No third-party project is built and no package is downloaded. The analyzers come from the
11+
.NET SDK and from the local NuGet cache, the same versions as direction-research Phase 5.
12+
13+
## Question
14+
15+
N1: a disposable is acquired, a step that can throw runs before ownership is handed over (return, field or
16+
collection store, wrapping), and no dispose runs on the throw path.
17+
18+
Do existing analyzers miss the four confirmed N1 defects of Phase 0 once they are configured to look? The question
19+
matters most for CA2000 with exception paths on (`dispose_analysis_kind = AllPaths`), because that is the
20+
documented setting for exactly this case.
21+
22+
## Why the earlier "not detected" does not answer it
23+
24+
Phase 5 ran the analyzers over loose source trees. Those runs had about 35,000 compile errors (whizbang: 34,859 on
25+
base), so the disposable types were mostly unresolved, and CA2000 ran with its default kind. Those misses therefore
26+
say little about what CA2000 can do.
27+
28+
## Pairs (fixed now)
29+
30+
| Id | Fix | Buggy method | Leaked resource and acquisition | Throwing step before hand-over | Hand-over on success |
31+
|---|---|---|---|---|---|
32+
| W | whizbang-lib/whizbang#1204 | `AzureServiceBusConnectionRetry.CreateClientWithRetryAsync` | `new ServiceBusClient(cs)` (`IAsyncDisposable`), inside a retry loop's `try` | `await VerifyNamespaceReachableAsync(…)` | `return client;` |
33+
| R | MarimerLLC/rockbot#638 (record 638-2) | `McpBridgeService.ConnectServerAsync` | `await McpClient.CreateAsync(transport, …)`, an async factory (`IAsyncDisposable`) | `await newClient.ListToolsAsync(…)`, prompt listing | `_clients[name] = newClient;` (dictionary field) |
34+
| X | NexusFFXIV/NexusKit#34 (record 34-2) | `PluginHostBuilder.BuildAsync` | `services.BuildServiceProvider(validateScopes: false)`, a factory (`ServiceProvider`, `IDisposable` + `IAsyncDisposable`) | `await DbInitializer.InitializeAsync(…)`, service resolution | `return new PluginHost(provider);` (wrapping) |
35+
| B | Wizard-Software/BareWire#73 | `RabbitMqTransportAdapter.ConsumeAsync` (an `async` iterator) | `await _connection.CreateChannelAsync(…)`, an async factory (`IChannel`, `IAsyncDisposable` + `IDisposable`) | `await consumerChannel.BasicQosAsync(…)` | `_activeConsumerChannels[id] = consumerChannel;` (dictionary field) |
36+
37+
The four fixes are by different owners.
38+
39+
## Reproductions
40+
41+
For each pair: one file `repro/<Id>.cs` with two self-contained classes, `<Id>Buggy` and `<Id>Fixed`. The
42+
acquisition line carries the marker comment `// ACQ:<Id>`.
43+
44+
- **Kept from the real code.**
45+
- the acquisition expression form: `new`, factory call, or awaited async factory;
46+
- the throwing step, as an awaited call that can throw;
47+
- the success hand-over (return, wrap, or dictionary store);
48+
- the enclosing control flow: the retry loop with `try`/`catch` in W, the `try` around the attempt in R, and the
49+
`async` iterator in B;
50+
- each fix's exact cleanup shape: W `try`/`catch` + `DisposeAsync` + rethrow, with acquisition through a
51+
`Func<…>` seam; R a nullable local, a `published` flag and cleanup on the failure path; X `try`/`catch` + wrap +
52+
`DisposeAsync` + rethrow; B early registration into the dictionary, then `try`/`catch` + release.
53+
- **Third-party types are stubs** with the real disposable interfaces and the members used: `ServiceBusClient`,
54+
`McpClient`, `IChannel`/`IConnection`. `ServiceCollection`/`ServiceProvider` are the real types from the local
55+
ASP.NET Core reference pack.
56+
- **Lines not on the resource's path are dropped**: logging, metrics, unrelated locals.
57+
- The reproductions must compile with **0 errors**. An analyzer that sees a compile error proves nothing.
58+
59+
## Analyzers and configurations
60+
61+
One SDK-style project, `net10.0`, with no `PackageReference`. Analyzers are referenced by path:
62+
63+
- the .NET SDK `Microsoft.CodeAnalysis.NetAnalyzers` (CA2000);
64+
- `IDisposableAnalyzers` 4.0.8 (IDISP001–IDISP026);
65+
- `SonarAnalyzer.CSharp` 10.35.0.4138 (S2930 and others).
66+
67+
Every rule of every analyzer is set to warning (`dotnet_analyzer_diagnostic.severity = warning`). Configurations:
68+
69+
| K | `dispose_analysis_kind` | ownership transfer at constructor | ownership transfer at method call |
70+
|---|---|---|---|
71+
| K1 | default (`NonExceptionPaths`) | default (false) | default (false) |
72+
| K2 | `AllPaths` | false | false |
73+
| K3 | `AllPaths` | **true** | false |
74+
| K4 | `AllPaths` | false | **true** |
75+
| K5 | `AllPathsOnlyNotDisposed` | false | false |
76+
77+
**Harness control.** A two-method file, `Control.cs`:
78+
79+
- `new StreamReader(path)`, then a call that can throw, then `Dispose()`. CA2000 must report it under K2 and must
80+
not report it under K1.
81+
- the same with `using`: silent in every configuration.
82+
83+
If this control fails, the harness is invalid and the gate stops as `INVALID`, not as KILL.
84+
85+
## Scoring (fixed now)
86+
87+
- **Relevant diagnostic.** A diagnostic whose message is about disposal or ownership, located on the leaked
88+
resource's acquisition statement or its declared variable, in the buggy method.
89+
- **Pair verdict.**
90+
- **`CAUGHT`:** at least one configuration gives a relevant diagnostic on the buggy reproduction. This holds even
91+
when the fixed reproduction also gets one: a flag on the buggy site counts against N1's novelty. Whether the
92+
flag also distinguishes the fix is recorded separately.
93+
- **`MISSED`:** no relevant diagnostic on the buggy reproduction in any of K1–K5, from any of the three analyzers.
94+
95+
**Decision.**
96+
- **`GO-CANDIDATE-N1`** (for an owner decision, not a PoC GO) if at least **2** pairs are `MISSED`.
97+
- **`KILL-N1`** otherwise.
98+
99+
Fidelity risk: a stub or a dropped line can change an analyzer's result. Every `MISSED` is therefore re-checked by
100+
listing the analyzer's documented limits for that shape: a factory, an `async` iterator or a dictionary store. A
101+
`MISSED` that is explained only by a reproduction deviation from the real code becomes `UNRESOLVED` and does not
102+
count toward GO.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// B: Wizard-Software/BareWire#73, RabbitMqTransportAdapter.ConsumeAsync (an async iterator).
2+
using System;
3+
using System.Collections.Concurrent;
4+
using System.Collections.Generic;
5+
using System.Runtime.CompilerServices;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Stubs;
9+
10+
namespace Repro;
11+
12+
public sealed class BBuggy
13+
{
14+
private IConnection? _connection;
15+
private readonly ConcurrentDictionary<string, IChannel> _activeConsumerChannels = new();
16+
17+
public async IAsyncEnumerable<string> ConsumeAsync(string endpointName, ushort prefetch,
18+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
19+
{
20+
IChannel consumerChannel = await _connection!.CreateChannelAsync(cancellationToken).ConfigureAwait(false); // ACQ:B
21+
22+
await consumerChannel.BasicQosAsync(prefetchSize: 0, prefetchCount: prefetch, global: false,
23+
cancellationToken: cancellationToken).ConfigureAwait(false);
24+
25+
string consumerChannelId = Guid.NewGuid().ToString("N");
26+
_activeConsumerChannels[consumerChannelId] = consumerChannel;
27+
28+
string assignedTag;
29+
try
30+
{
31+
assignedTag = await consumerChannel.BasicConsumeAsync(endpointName, autoAck: false, cancellationToken)
32+
.ConfigureAwait(false);
33+
}
34+
catch
35+
{
36+
_activeConsumerChannels.TryRemove(consumerChannelId, out _);
37+
throw;
38+
}
39+
40+
yield return assignedTag;
41+
}
42+
}
43+
44+
public sealed class BFixed
45+
{
46+
private IConnection? _connection;
47+
private readonly ConcurrentDictionary<string, IChannel> _activeConsumerChannels = new();
48+
49+
public async IAsyncEnumerable<string> ConsumeAsync(string endpointName, ushort prefetch,
50+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
51+
{
52+
IChannel consumerChannel = await _connection!.CreateChannelAsync(cancellationToken).ConfigureAwait(false); // ACQ:B
53+
54+
string consumerChannelId = Guid.NewGuid().ToString("N");
55+
_activeConsumerChannels[consumerChannelId] = consumerChannel;
56+
57+
string assignedTag;
58+
try
59+
{
60+
await consumerChannel.BasicQosAsync(prefetchSize: 0, prefetchCount: prefetch, global: false,
61+
cancellationToken: cancellationToken).ConfigureAwait(false);
62+
63+
assignedTag = await consumerChannel.BasicConsumeAsync(endpointName, autoAck: false, cancellationToken)
64+
.ConfigureAwait(false);
65+
}
66+
catch
67+
{
68+
await ReleaseConsumerChannelSafeAsync(consumerChannelId).ConfigureAwait(false);
69+
throw;
70+
}
71+
72+
yield return assignedTag;
73+
}
74+
75+
private async Task ReleaseConsumerChannelSafeAsync(string channelId)
76+
{
77+
if (_activeConsumerChannels.TryRemove(channelId, out IChannel? channel))
78+
{
79+
try { await channel.CloseAsync().ConfigureAwait(false); } catch { }
80+
await channel.DisposeAsync().ConfigureAwait(false);
81+
}
82+
}
83+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Harness control (00-preregistration.md): CA2000 must report ControlLeakOnThrow under AllPaths (K2)
2+
// and not under the default kind (K1); ControlUsing must stay silent everywhere.
3+
using System.IO;
4+
5+
namespace Repro;
6+
7+
public static class Control
8+
{
9+
public static string ControlLeakOnThrow(string path)
10+
{
11+
var reader = new StreamReader(path); // ACQ:control
12+
var text = MayThrow(reader.ReadLine());
13+
reader.Dispose();
14+
return text;
15+
}
16+
17+
public static string ControlUsing(string path)
18+
{
19+
using var reader = new StreamReader(path); // ACQ:control-using
20+
return MayThrow(reader.ReadLine());
21+
}
22+
23+
private static string MayThrow(string? s) => s ?? throw new InvalidDataException();
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<!-- N1 competitor gate reproductions. No PackageReference: analyzers are referenced by path
3+
(pass -p:NuGetRoot=<global packages folder>). Built from a copy outside any tree with
4+
corporate MSBuild settings. -->
5+
<PropertyGroup>
6+
<TargetFramework>net10.0</TargetFramework>
7+
<OutputType>Library</OutputType>
8+
<Nullable>enable</Nullable>
9+
<ImplicitUsings>disable</ImplicitUsings>
10+
<EnableNETAnalyzers>true</EnableNETAnalyzers>
11+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
12+
<NoWarn>$(NoWarn);CS1591</NoWarn>
13+
</PropertyGroup>
14+
<ItemGroup>
15+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
16+
<Analyzer Include="$(NuGetRoot)/idisposableanalyzers/4.0.8/analyzers/dotnet/cs/IDisposableAnalyzers.dll" />
17+
<Analyzer Include="$(NuGetRoot)/sonaranalyzer.csharp/10.35.0.4138/analyzers/SonarAnalyzer.CSharp.dll" />
18+
</ItemGroup>
19+
</Project>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// R: MarimerLLC/rockbot#638, McpBridgeService.ConnectServerAsync.
2+
using System;
3+
using System.Collections.Concurrent;
4+
using System.Collections.Generic;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Stubs;
8+
9+
namespace Repro;
10+
11+
public sealed class RBuggy
12+
{
13+
private readonly ConcurrentDictionary<string, McpClient> _clients = new();
14+
private readonly ConcurrentDictionary<string, IList<string>> _serverTools = new();
15+
16+
public async Task ConnectServerAsync(string name, Uri url, int maxAttempts, CancellationToken ct)
17+
{
18+
for (var attempt = 1; attempt <= maxAttempts; attempt++)
19+
{
20+
try
21+
{
22+
var transport = new HttpClientTransport(url);
23+
var newClient = await McpClient.CreateAsync(transport, null, cancellationToken: ct); // ACQ:R
24+
25+
var tools = await newClient.ListToolsAsync(cancellationToken: ct);
26+
var prompts = await newClient.ListPromptsAsync(ct);
27+
28+
if (_clients.TryRemove(name, out var oldClient))
29+
{
30+
try { await oldClient.DisposeAsync(); }
31+
catch { /* Best-effort cleanup */ }
32+
}
33+
34+
_clients[name] = newClient;
35+
_serverTools[name] = tools;
36+
return;
37+
}
38+
catch (OperationCanceledException) when (ct.IsCancellationRequested)
39+
{
40+
throw;
41+
}
42+
catch (Exception)
43+
{
44+
if (attempt == maxAttempts) return;
45+
await Task.Delay(100 * attempt, ct);
46+
}
47+
}
48+
}
49+
}
50+
51+
public sealed class RFixed
52+
{
53+
private readonly ConcurrentDictionary<string, McpClient> _clients = new();
54+
private readonly ConcurrentDictionary<string, IList<string>> _serverTools = new();
55+
56+
public async Task ConnectServerAsync(string name, Uri url, int maxAttempts, CancellationToken ct)
57+
{
58+
for (var attempt = 1; attempt <= maxAttempts; attempt++)
59+
{
60+
McpClient? newClient = null;
61+
var published = false;
62+
try
63+
{
64+
var transport = new HttpClientTransport(url);
65+
newClient = await McpClient.CreateAsync(transport, null, cancellationToken: ct); // ACQ:R
66+
67+
var tools = await newClient.ListToolsAsync(cancellationToken: ct);
68+
var prompts = await newClient.ListPromptsAsync(ct);
69+
70+
if (_clients.TryRemove(name, out var oldClient))
71+
{
72+
await DisposeQuietlyAsync(oldClient);
73+
}
74+
75+
_clients[name] = newClient;
76+
published = true;
77+
_serverTools[name] = tools;
78+
return;
79+
}
80+
catch (OperationCanceledException) when (ct.IsCancellationRequested)
81+
{
82+
throw;
83+
}
84+
catch (Exception)
85+
{
86+
if (attempt == maxAttempts) return;
87+
await Task.Delay(100 * attempt, ct);
88+
}
89+
finally
90+
{
91+
if (!published && newClient is not null)
92+
{
93+
await DisposeQuietlyAsync(newClient);
94+
}
95+
}
96+
}
97+
}
98+
99+
private static async Task DisposeQuietlyAsync(IAsyncDisposable disposable)
100+
{
101+
try { await disposable.DisposeAsync(); }
102+
catch { /* Best-effort cleanup */ }
103+
}
104+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Stubs for third-party types used by the N1 reproductions. Each keeps the real type's disposable
2+
// interfaces and the members the reproduced method calls; bodies are irrelevant to the analyzers.
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace Stubs;
9+
10+
// Azure.Messaging.ServiceBus.ServiceBusClient : IAsyncDisposable
11+
public class ServiceBusClient : IAsyncDisposable
12+
{
13+
public ServiceBusClient(string connectionString) { }
14+
public ValueTask DisposeAsync() => default;
15+
}
16+
17+
public class ServiceBusException : Exception { }
18+
19+
// ModelContextProtocol.Client.McpClient : IAsyncDisposable, created by an async factory
20+
public abstract class McpClient : IAsyncDisposable
21+
{
22+
public static Task<McpClient> CreateAsync(IClientTransport transport, object? options = null,
23+
CancellationToken cancellationToken = default) => throw new NotImplementedException();
24+
public abstract Task<IList<string>> ListToolsAsync(CancellationToken cancellationToken = default);
25+
public abstract Task<IList<string>> ListPromptsAsync(CancellationToken cancellationToken = default);
26+
public abstract ValueTask DisposeAsync();
27+
}
28+
29+
public interface IClientTransport { }
30+
31+
public sealed class HttpClientTransport : IClientTransport
32+
{
33+
public HttpClientTransport(Uri endpoint) { }
34+
}
35+
36+
// RabbitMQ.Client 7: IChannel : IAsyncDisposable, IDisposable; created by IConnection.CreateChannelAsync
37+
public interface IChannel : IAsyncDisposable, IDisposable
38+
{
39+
Task BasicQosAsync(uint prefetchSize, ushort prefetchCount, bool global, CancellationToken cancellationToken = default);
40+
Task<string> BasicConsumeAsync(string queue, bool autoAck, CancellationToken cancellationToken = default);
41+
Task CloseAsync(CancellationToken cancellationToken = default);
42+
}
43+
44+
public interface IConnection : IAsyncDisposable, IDisposable
45+
{
46+
Task<IChannel> CreateChannelAsync(CancellationToken cancellationToken = default);
47+
}

0 commit comments

Comments
 (0)