Skip to content

Polecat#46: Tighten reflective surface annotations from class-level to call-site#107

Merged
jeremydmiller merged 3 commits into
mainfrom
feature/reflective-audit-46
May 18, 2026
Merged

Polecat#46: Tighten reflective surface annotations from class-level to call-site#107
jeremydmiller merged 3 commits into
mainfrom
feature/reflective-audit-46

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Replaces class-level [UnconditionalSuppressMessage] umbrellas on Polecat's reflective surfaces with method-level annotations that name the exact contract. Downstream IsAotCompatible=true consumers now see a precise punch-list instead of a wall of silenced classes. Ticks the "Audit reflective surfaces that aren't covered by Polecat.CodeGeneration. Annotate or migrate." checkbox on #46.

Polecat.AotSmoke (from #102/#106) still builds clean — the gate confirms no AOT regression.

Before/after

File Class-level attrs (before) Class-level attrs (after) Method-level attrs added
Projections/PolecatCompositeProjection.cs 2 (IL2026, IL3050) 0 2 on Snapshot<T>
Projections/PolecatProjectionOptions.cs 2 (IL2026, IL3050) 0 4 across Snapshot<T> + AddSnapshotProjection<T>
Linq/PolecatLinqQueryProvider.cs 4 (IL2026, IL2070, IL2075, IL3050) 0 25 across 12 methods (CreateQuery, ExecuteAsync, ExecuteGroupBy/ExecuteGroupJoin, all Invoke*Handler*Async, FindOriginalProperty, FindDeepestMemberExpression, RebuildMemberAccess)
Events/Linq/EventLinqQueryProvider.cs (sibling) 2 (IL2075, IL3050) 0 14 across 6 methods
Internal/DocumentSessionBase.cs 2 (IL2026, IL3050) 0 2 method-level UnconditionalSuppressMessage on InsertEventAsync
Linq/IPolecatAsyncQueryProvider.cs (propagation) 0 0 2 on the interface ExecuteAsync<TResult> so impls match

Why method-level UnconditionalSuppressMessage on DocumentSessionBase instead of RUC/RDC?
The reflective surface in DocumentSessionBase is two Serializer.ToJson(object) call sites in InsertEventAsync (a private method). Promoting [RUC]/[RDC] up the call chain would propagate to IDocumentSession.SaveChangesAsync — a public interface change beyond #46's scope. Per the chip's style guide, method-level UnconditionalSuppressMessage is the documented fallback when "no method-level annotation expresses the constraint cleanly" — in this case "cleanly" includes "without widening scope to the public surface." Justifications name the exact call sites.

EventLinqQueryProvider wasn't in the chip's primary four but came in scope because adding RUC/RDC to IPolecatAsyncQueryProvider.ExecuteAsync (required to match PolecatLinqQueryProvider's impl annotation) cascaded an IL2046 mismatch on the only other implementer. Tightening both impls + the interface in one go is cleaner than leaving the sibling with a stale class-level umbrella.

Verification

  • dotnet build src/Polecat/Polecat.csproj -c Release — clean (same 7 pre-existing IL warnings as main, all in files outside this PR's scope; no new warnings introduced)
  • dotnet build src/Polecat.AotSmoke/Polecat.AotSmoke.csproj -c Release — clean (0 warnings, 0 errors on net9.0 + net10.0); smoke surface migrated from opts.Projections.Snapshot<Quest>(...) (now correctly RUC/RDC) to the AOT-safe opts.Projections.Add<QuestProjection>(ProjectionLifecycle.Inline) pattern with a concrete SingleStreamProjection<Quest, Guid> subclass
  • dotnet build src/Polecat.EntityFrameworkCore.Tests/Polecat.EntityFrameworkCore.Tests.csproj -c Release — clean (only pre-existing CS8767 nullability noise unrelated to AOT)
  • dotnet run --project src/Polecat.AotSmoke -c Release — exits 0 on both net9.0 + net10.0

Local test suite couldn't be run end-to-end — the Docker SQL Server image is linux/amd64 and the host is arm64, so emulated connection handshakes time out before tests can even open a connection (every test fails at ConnectionSource.DetectNativeJsonSupport() with Connection Timeout Expired, handshake=3881ms). All test failures are environmental; the changes are annotation-only with no runtime semantics, so the existing CI run on amd64 is the authoritative verification.

Out of scope (per chip's "Don't widen scope")

Pre-existing class-level suppressions remain in: DocumentStore.{EventStoreExplorer,ProjectionReplay}.cs, AdvancedOperations.cs, AdvancedSqlResultReader.cs, Serialization/Serializer.cs, Events/EventGraph.cs, Events/EventOperations.cs, Events/Daemon/PolecatEventLoader.cs, Events/Internal/PcEventsRowReader.cs, Internal/QuerySession.cs, Internal/DocumentProvider*.cs, Internal/Batching/*.cs, Storage/DocumentMapping*.cs, Linq/{NonStaleDataExtensions,SoftDeletes/SoftDeletedExtensions,Metadata/*,Selectors/DeserializingSelector,Joins/JoinResultSelectorRewriter,Parsing/*,QueryHandlers/*,PolecatQueryableExtensions,LinqExtensions}.cs, Patching/*.cs, Projections/{PolecatProjectionStorage,Flattened/{StatementMap,EventDeleter}}.cs, PolecatConfigurationExpression.cs, PolecatStoreServiceCollectionExtensions.cs. A follow-up chip can sweep these.

Also out of scope: migrating reflective patterns to GenericFactoryCache / source generators (separate Polecat#46 checkbox under the cold-start pillar).

Commit shape

  1. ae97c3b — Projections cluster (PolecatCompositeProjection + PolecatProjectionOptions) + AotSmoke surface migration
  2. 112da8b — LINQ providers (PolecatLinqQueryProvider + IPolecatAsyncQueryProvider + EventLinqQueryProvider sibling)
  3. b1de9f6 — DocumentSessionBase

References #46.

🤖 Generated with Claude Code

jeremydmiller and others added 3 commits May 18, 2026 08:46
Replace class-level [UnconditionalSuppressMessage] on PolecatCompositeProjection
and PolecatProjectionOptions with method-level [RequiresDynamicCode] +
[RequiresUnreferencedCode] on the two Snapshot<T> entry points — the only
reflective surface in each class. Both call CloseAndBuildAs<ProjectionBase>
which internally uses Type.MakeGenericType to close SingleStreamProjection<,>
over (T, T.Id) and resolves T's Id property via DocumentMapping reflection.

AOT smoke test migrated from opts.Projections.Snapshot<Quest>(...) to
opts.Projections.Add<QuestProjection>(ProjectionLifecycle.Inline) with a
concrete SingleStreamProjection<Quest, Guid> subclass — the registration
shape AOT consumers must use. The reflective shortcut is now correctly
flagged at the call site instead of silently silenced.

References #46.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace class-level [UnconditionalSuppressMessage] on
PolecatLinqQueryProvider (4 attrs) and EventLinqQueryProvider (2 attrs) with
per-method [RequiresDynamicCode] + [RequiresUnreferencedCode] on every method
that hosts a reflective call site:

- CreateQuery(Expression) — MakeGenericType + Activator.CreateInstance on
  PolecatLinqQueryable<>. Suppress IL2046/IL3051 at the impl with a
  call-site UnconditionalSuppressMessage since IQueryProvider's interface
  method isn't annotated in the BCL.
- ExecuteAsync<TResult> — routes to handler invocations that close generic
  handler types via MakeGenericType.
- ExecuteGroupByAsync / InvokeGroupByListHandlerAsync — GroupByListHandler<>.
- ExecuteGroupJoinAsync / InvokeJoinHandlerAsync — Func<,,> + JoinListHandler<,,>.
- InvokeListHandlerAsync / InvokeScalarListHandlerAsync /
  InvokeProjectionHandlerAsync / InvokeOneResultHandlerAsync — selector +
  handler types over the document type, plus MethodInfo.Invoke on HandleAsync
  and reflective GetProperty on Task<>.Result.
- FindOriginalProperty / FindDeepestMemberExpression / RebuildMemberAccess —
  reflect over the document type's properties/fields when rebuilding join
  order-by member access. [RequiresUnreferencedCode] only (no MakeGenericType).

IPolecatAsyncQueryProvider.ExecuteAsync<TResult> picks up matching attributes
so the interface contract is precise; EventLinqQueryProvider (the only other
implementer) gets the same per-method treatment.

References #46.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace class-level [UnconditionalSuppressMessage] for IL2026/IL3050 with
method-level [UnconditionalSuppressMessage] on InsertEventAsync — the only
method in DocumentSessionBase that hosts the [RequiresUnreferencedCode] /
[RequiresDynamicCode] surface (two Serializer.ToJson(object) call sites for
@event.Data and @event.Headers).

Method-level UnconditionalSuppressMessage (rather than RUC/RDC promotion) is
the chip's documented fallback when the propagation chain would reach the
public interface surface — in this case IDocumentSession.SaveChangesAsync,
which is outside the scope of #46's reflective-callsite tightening.

The AOT escape hatch remains: AOT consumers supply a source-generator-backed
ISerializer; the default reflective STJ ISerializer is the only thing that
trips RUC/RDC on these call sites.

References #46.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 6f1b77c into main May 18, 2026
7 checks passed
jeremydmiller added a commit that referenced this pull request May 18, 2026
CI surfaced the broader scope of JasperFx#276 Phase 1's fail-fast contract:
any closed `SingleStreamProjection<TDoc, TId>` (or sibling) instantiated by
the live-aggregation path — `AggregateStreamAsync<T>`, `FetchLatest<T>`,
`FetchForWriting<T>`, `Projections.Snapshot<T>` — relies on the source
generator emitting a standalone `TEvolver` for the self-aggregating
document type `T`. The SG only emits when `T` is `partial` AND the
assembly references the SG analyzer.

Marks the following document types `partial`:

- Polecat.Tests:
  - Bug4197Aggregate, DeletableAggregate, StudentCourseEnrollment,
    QuestAggregate, InlineSeAggregate, InvoiceAggregate, OrderAggregate,
    Report, ScenarioQuestParty, CompositeQuestParty, QuestStats,
    QuestParty, CustomerSummary, SelfAggregatingStringQuest,
    StringQuestParty, SnapshotParty, SnapshotPartyByString,
    MonthlyAccountActivity, Payment, Payment2
- Polecat.AspNetCore.Testing:
  - StreamingQuestParty (+ wires
    JasperFx.Events.SourceGenerator as an Analyzer PackageReference)

Adds two new sections to docs/migration-guide.md:

- "Projections and self-aggregating documents must be `partial`" under
  Event-sourcing API changes — covers the fail-fast diagnostic, the
  partial requirement, the analyzer-PackageReference requirement for
  consumer assemblies, the EvolveAsync / DetermineActionAsync override
  escape hatch, and the surfaces unaffected (FlatTableProjection,
  EF Core projections).
- "Inline-lambda projection registration APIs removed" — covers the
  Project<T>(lambda) / CreateEvent / DeleteEvent / ProjectEvent
  removals from jasperfx#276 / #286 and the conventional-method
  replacement shape.

Refreshes the foundation pin table (JasperFx alpha.11→alpha.13,
JasperFx.Events alpha.4→alpha.11, SG alpha.2→alpha.4) and extends the
AOT/codegen posture section to reference #107 (reflective surface
tightening) and #106 (Polecat.AotSmoke CI gate) plus the new FEC-free
posture from #276.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit that referenced this pull request May 18, 2026
…#109)

* Bump JasperFx.Events to 2.0.0-alpha.11 + SourceGenerator to 2.0.0-alpha.4

JasperFx#276 Phase 1 (alpha.11) removed the FastExpressionCompiler
fallback for projection apply-method dispatch. Source-generated
dispatchers ([GeneratedEvolver]) are now the only path, with fail-fast
at registration when no generated dispatcher is found.

SG alpha.4 pulls in a downstream fix
(JasperFx/jasperfx#291) for a sync-DetermineActionAsync emit bug that
surfaced on Polecat projections combining sync Apply + ShouldDelete
(StringQuestPartyProjection, QuestPartyProjection, DeletableAggregate).
Without alpha.4 the generated dispatcher fails to compile with CS8135.

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

* Verify Polecat projections register generated dispatchers (JFx#276 Phase 3)

JasperFx.Events 2.0.0-alpha.11 (#276 Phase 1) made source-generated
dispatchers the only path; DocumentStore fail-fasts on projection types
without [GeneratedEvolver]. Two consumer-side changes needed:

1. **partial class** on every projection registered through
   Projections.Add<T>(...) or Snapshot<T>(...) — so the SG can emit a
   partial override of Evolve / EvolveAsync / DetermineActionAsync.
   Affects: QuestLogProjection, MultiEventQuestLogProjection,
   SimpleEnrichmentProjection, EnrichmentCallOrderProjection,
   DbLookupEnrichmentProjection, MonthlyAccountActivityProjection,
   CompositeOrderProjection, OrderShippingNotificationProjection,
   StringQuestPartyProjection, CustomerSummaryProjection, and the
   nested InlineSeProjection (whose outer test class also becomes
   partial).

2. **SG analyzer wired into Polecat.AotSmoke.csproj** as an
   Analyzer-only PackageReference, and QuestProjection declared
   partial. The smoke gate now exercises the SG-only dispatch path
   end-to-end; without it, AotSmoke runtime throws
   InvalidProjectionException at host build.

Test-side migrations off the removed inline-lambda registration API
(JasperFx#276 / #286 dropped Project<TEvent>(lambda) +
CreateEvent/DeleteEvent/ProjectEvent overloads):
- event_projection_enrichment_tests.cs (3 projections) — converted
  constructor `Project<TEvent>((e, ops) => ...)` calls to conventional
  `public void Project(TEvent e, IDocumentSession ops)` methods.
- event_projection_tests.cs — removed QuestLogWithLambdaProjection +
  event_projection_with_lambda test; the API they exercised no longer
  exists and the conventional path is already covered by
  QuestLogProjection.

FlatTableProjection unaffected — it has its own Apply dispatch via
_handlers dictionary, doesn't use JasperFx.Events Apply discovery.
EF Core projections (EfCoreSingleStreamProjection etc.) override
DetermineActionAsync directly, also bypassing the SG-required path.

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

* Bump Polecat to 4.0.0-alpha.5

First Polecat alpha consuming the FEC-free JasperFx.Events alpha
(2.0.0-alpha.11 + SourceGenerator 2.0.0-alpha.4). All projections
registered through Polecat now go through source-generated dispatchers
exclusively — no FastExpressionCompiler fallback.

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

* Mark self-aggregating document types `partial` + migration guide

CI surfaced the broader scope of JasperFx#276 Phase 1's fail-fast contract:
any closed `SingleStreamProjection<TDoc, TId>` (or sibling) instantiated by
the live-aggregation path — `AggregateStreamAsync<T>`, `FetchLatest<T>`,
`FetchForWriting<T>`, `Projections.Snapshot<T>` — relies on the source
generator emitting a standalone `TEvolver` for the self-aggregating
document type `T`. The SG only emits when `T` is `partial` AND the
assembly references the SG analyzer.

Marks the following document types `partial`:

- Polecat.Tests:
  - Bug4197Aggregate, DeletableAggregate, StudentCourseEnrollment,
    QuestAggregate, InlineSeAggregate, InvoiceAggregate, OrderAggregate,
    Report, ScenarioQuestParty, CompositeQuestParty, QuestStats,
    QuestParty, CustomerSummary, SelfAggregatingStringQuest,
    StringQuestParty, SnapshotParty, SnapshotPartyByString,
    MonthlyAccountActivity, Payment, Payment2
- Polecat.AspNetCore.Testing:
  - StreamingQuestParty (+ wires
    JasperFx.Events.SourceGenerator as an Analyzer PackageReference)

Adds two new sections to docs/migration-guide.md:

- "Projections and self-aggregating documents must be `partial`" under
  Event-sourcing API changes — covers the fail-fast diagnostic, the
  partial requirement, the analyzer-PackageReference requirement for
  consumer assemblies, the EvolveAsync / DetermineActionAsync override
  escape hatch, and the surfaces unaffected (FlatTableProjection,
  EF Core projections).
- "Inline-lambda projection registration APIs removed" — covers the
  Project<T>(lambda) / CreateEvent / DeleteEvent / ProjectEvent
  removals from jasperfx#276 / #286 and the conventional-method
  replacement shape.

Refreshes the foundation pin table (JasperFx alpha.11→alpha.13,
JasperFx.Events alpha.4→alpha.11, SG alpha.2→alpha.4) and extends the
AOT/codegen posture section to reference #107 (reflective surface
tightening) and #106 (Polecat.AotSmoke CI gate) plus the new FEC-free
posture from #276.

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

* Split self-aggregating docs that mix Guid Id with string streams

Polecat 3.x's FEC-based live aggregator was happy to construct
SingleStreamProjection<TDoc, TId> with a TId that didn't match the
document's Id property — the FEC dispatcher just used whatever TId
the call site passed. Two tests relied on this latent flexibility:

- project_latest_with_string_key_includes_pending_events and
  project_latest_merges_committed_and_pending_for_string_key registered
  Report (Guid Id) against string-keyed streams.
- always_enforce_consistency_with_string_stream_id registered
  QuestAggregate (Guid Id) against string-keyed streams.

Polecat 4 routes through the JasperFx.Events source generator, which
keys the generated evolver on the document's Id property type. The
SG emits IGeneratedSyncEvolver<Report, Guid>, runtime instantiates
<Report, string>, and lookup fails.

Splits each affected document type into a separate string-id sibling:

- new StringReport with `string Id` for the project_latest string-key
  tests
- new StringQuestAggregate with `string Id` for the
  always_enforce_consistency string-id test

Same events, same Apply/Create methods — only the Id type differs.

Remaining CI failures on this PR (QuestParty / DeletableAggregate /
StringQuestParty / SelfAggregatingStringQuest with conventional
ShouldDelete methods routed through the assembly-registered evolver
lookup) are blocked on JasperFx/jasperfx#298 — a guard bug in
JasperFxAggregationProjectionBase.tryUseAssemblyRegisteredEvolver
short-circuits before checking IGeneratedSyncDetermineAction (the
interface the SG emits for ShouldDelete docs and which handles
ShouldDelete natively). 5-line upstream fix sketched in the issue.

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

* Bump JasperFx.Events 2.0.0-alpha.11 → alpha.12 + SourceGenerator alpha.4 → alpha.5

JasperFx.Events alpha.12 ships JasperFx/jasperfx#300 (closes #298) —
the fix for tryUseAssemblyRegisteredEvolver's guard that was
short-circuiting on HasShouldDeleteMethods() before reaching the
IGeneratedSyncDetermineAction branch (the very interface the SG emits
for self-aggregating docs with ShouldDelete, which handles deletions
natively).

Unblocks the ~25 Polecat test failures on this PR from the previous
CI run — all on the closed generic
`SingleStreamProjection<TDoc, Guid>` where TDoc was QuestParty,
StringQuestParty, or DeletableAggregate (each has a ShouldDelete
method).

JasperFx.Events.SourceGenerator alpha.5 is a coordinated version bump
that came with the alpha.12 release — the SG package itself is
unchanged from alpha.4 in any way that affects Polecat (the fix lives
in the JasperFx.Events runtime, not the SG).

Verified locally via Polecat.AotSmoke pointed at alpha.12 — bare
`opts.Projections.Add<SingleStreamProjection<DocWithShouldDelete, Guid>>(...)`
now boots cleanly. Local SQL-based test verification was blocked by a
Docker daemon issue; CI on amd64 is the authoritative run.

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

* Resolve live-aggregator TId via DocumentMapping (strong-typed-id fix)

CI surfaced 4 of 1105 test failures — all on strong-typed-id aggregates
(Payment with PaymentId wrapping Guid; Payment2 with Payment2Id wrapping
string). The closed runtime SingleStreamProjection<TDoc, TId> instances
were built with TId = the underlying *primitive* (Guid / string), but
the source generator keys its emitted IGeneratedSyncEvolver on TDoc's
actual Id property type — the *wrapper* (PaymentId / Payment2Id). The
TId mismatch tripped JasperFxAggregationProjectionBase.tryUseAssembly
RegisteredEvolver's interface-assignability check, so the dispatcher
wasn't found and the post-FEC fail-fast threw.

Two surfaces to fix:

1. **EventGraph.Build<TDoc>()** — the live-aggregation entry. Was
   hardcoding `idType = StreamIdentity == AsGuid ? typeof(Guid) : typeof(string)`,
   missing the wrapper. Now resolves via
   `new DocumentMapping(typeof(TDoc), _options).IdType`, the same shape
   PolecatProjectionOptions.AddSnapshotProjection<T>() already uses.
   This unblocks the no-explicit-registration paths
   (AggregateStreamAsync<Payment>, FetchLatest<Payment2>, etc).

2. **using_guid_based_strong_typed_id_for_aggregate_identity.cs** —
   two tests explicitly registered
   `Add<SingleStreamProjection<Payment, Guid>>(...)` instead of
   `Add<SingleStreamProjection<Payment, PaymentId>>(...)`. Worked under
   Polecat 3.x's FEC fallback (which didn't care about TId);
   under SG-only dispatch the registration must match the SG's emitted
   evolver shape. Both inline + async variants updated.

(The Payment2 string-based test file already registers with the
wrapper type Payment2Id; only the live-aggregation path needed
fixing there.)

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit that referenced this pull request May 19, 2026
Brings docs/migration-guide.md to release-readiness. Four additions on
top of the existing scaffolding from #109 + #111:

1. **Pin table refreshed + extended**
   - JasperFx.Events alpha.12 → alpha.15 (the post-#306 SG fixes already
     baked into Polecat 4.0.0-alpha.6 from #111)
   - JasperFx.Events.SourceGenerator alpha.5 → alpha.8 (ditto)
   - Add JasperFx.RuntimeCompiler 5.0.0-alpha.4 row, with a callout
     warning consumers off the parallel stale 2.0.x line on NuGet
   - Add JasperFx.SourceGeneration 2.0.0-alpha.5 row
   - "pin all FIVE packages" → "pin all SEVEN" + lockstep callout

2. **SG-only dispatch section extended** (under the existing
   "Projections and self-aggregating documents must be `partial`"):
   - Convention-method visibility — `Apply` / `Create` / `ShouldDelete`
     must be `public` (the FEC fallback reflected over private /
     internal / protected too; the SG only sees public)
   - `[Identity]` attribute for non-conventional id-property names
   - Required-member aggregate `Create` factory recipe (preferred over
     the `new T { Required = default! }` fallback the SG emits when no
     factory is supplied)
   - Explicit cross-link to Marten's migration guide section that
     covers the same shared consumer contract

3. **Inline-lambda removal section cross-linked to Marten**.
   The migration recipe is identical between products; cross-link
   instead of forking the longer worked example.

4. **AOT publishing promoted to a top-level `### Publishing AOT`
   section** with three cross-links:
   - JasperFx's live AOT guide (https://jasperfx.github.io/codegen/aot)
   - Marten's AOT publishing walkthrough
     (https://martendb.io/configuration/aot-publishing)
   - `Polecat.AotSmoke` (#106) as the canonical in-tree consumer
     example, plus the `IsAotCompatible=true` flag on Polecat.csproj
     itself
   The existing AOT call-outs about #107 / FEC removal / #106's
   WarningsAsErrors set stay; "Publishing AOT" is the new section header
   that frames them as the consumer-side recipe.

`npm run docs-build` (VitePress) renders clean: build complete in
4.27s, exit code 0.

References [#46](#46) (master
plan — "Migration guide Polecat 3 → Polecat 4" item).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant