Hmz.Core.SharedKernel 0.1.17

dotnet add package Hmz.Core.SharedKernel --version 0.1.17
                    
NuGet\Install-Package Hmz.Core.SharedKernel -Version 0.1.17
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Hmz.Core.SharedKernel" Version="0.1.17" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Hmz.Core.SharedKernel" Version="0.1.17" />
                    
Directory.Packages.props
<PackageReference Include="Hmz.Core.SharedKernel" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Hmz.Core.SharedKernel --version 0.1.17
                    
#r "nuget: Hmz.Core.SharedKernel, 0.1.17"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Hmz.Core.SharedKernel@0.1.17
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Hmz.Core.SharedKernel&version=0.1.17
                    
Install as a Cake Addin
#tool nuget:?package=Hmz.Core.SharedKernel&version=0.1.17
                    
Install as a Cake Tool

Hmz.Core.SharedKernel

Hmz.Core.SharedKernel 0.1.14 targets .NET 10 and provides reusable application, domain-support, EF Core, advanced-search, authorization, and ASP.NET Core boundary contracts.

Results and errors

Use Result and Result<T> for expected application failures:

using Hmz.Core.SharedKernel.Results;

public Result<Order> CreateOrder(string customerId)
{
    if (string.IsNullOrWhiteSpace(customerId))
    {
        return Result.Failure<Order>(Error.Validation(
            "order.customer-required",
            "Customer is required.",
            "customerId"));
    }

    return Result.Success(new Order(customerId));
}

Errors carry a stable code, safe message, transport-neutral category, and optional validation target. Failed results may contain multiple errors of one category. Use Match, Map, and Bind for composition. Unexpected failures remain exceptions and are handled at the application boundary.

Generate diagnostic codes with ErrorCode.Create:

public static readonly string ProductItemsNotFound = ErrorCode.Create(
    "CM",
    ErrorType.NotFound,
    "product-items");

// CM.NOTF.PRODUCT-ITEMS

Use a short, stable application/module prefix. Do not repeat the error type in the semantic suffix.

Pagination

using Hmz.Core.SharedKernel.Contracts.Pagination;

return new PagedResponse<OrderResponse>(items, page, pageSize, totalCount);

Messaging and identity

Hmz.Core.SharedKernel.Messaging provides ICommand, IQuery, INotification, and corresponding handler contracts over Mediator abstractions.

Hmz.Core.SharedKernel.Identity.Abstractions.ICurrentUser is the shared current-user boundary. Applications own authentication, claims mapping, profile lookup, and authorization policy.

Integration events and messages

The Messaging sub-namespaces add cross-module communication for modular monoliths. They distinguish two kinds of communication:

Integration event Message
Meaning A fact that already happened in one module A notification that only matters now
Durability Written to the producer's outbox in the same transaction as the state change; kept and replayable Not persisted; lost if the process stops
Delivery At least once, with retry, backoff, and dead-lettering; consumers are idempotent through an inbox At most once, no retry
Use for Keeping modules consistent Cache invalidation, live notifications, telemetry
Namespace Messaging.IntegrationEvents, .Outbox, .Inbox, .Transport Messaging.Messages

Domain events (EventDispatcherInterceptor) remain in-process, after-commit side effects inside one module and are not a substitute for either.

Defining and publishing an integration event

[IntegrationEventName("media.attachments-changed.v1")] // stable wire name; version it, never rename it
public sealed record MediaAttachmentsChanged(...) : IntegrationEvent;

// UseCase: each module exposes its own outbox interface, like its unit of work.
public interface ICatalogOutbox : IIntegrationEventOutbox;

outbox.Add(new MediaAttachmentsChanged(...)); // tracked only
await unitOfWork.SaveChangesAsync(ct);        // commits the row change and the outbox row together

Infrastructure wires the outbox into the module DbContext:

// OnModelCreating
modelBuilder.AddOutboxMessages();   // producer: <schema>.OutboxMessages
modelBuilder.AddInboxMessages();    // consumer: <schema>.InboxMessages

// DbContext options
options.AddOutboxNotifications(serviceProvider); // wakes the processor right after commit

// Services
services.AddIntegrationEventOutbox<AppDbContext>(configuration);
services.AddScoped<ICatalogOutbox, CatalogOutbox>(); // : EfIntegrationEventOutbox<AppDbContext>
services.AddIntegrationEventHandler<MediaAttachmentsChanged, Handler, MediaDbContext>(configuration);

Delivery flow

  1. A request handler changes state and calls outbox.Add(event); one SaveChangesAsync commits both rows.
  2. OutboxNotificationInterceptor signals the module's OutboxProcessor<TDbContext> after the commit (it also polls every Messaging:Outbox:PollInterval).
  3. The processor leases a batch in one short statement (UPDATE … SET "LockedUntil" … WHERE "Id" IN (SELECT … FOR UPDATE SKIP LOCKED) RETURNING *) and commits. No transaction stays open during delivery, any number of hosts can run processors, and rows of a crashed host become claimable when their lease expires.
  4. Events are delivered outside any transaction, up to Messaging:Outbox:DeliveryConcurrency at a time, through the transport selected by Messaging:Transport. The built-in InMemoryIntegrationEventTransport is a bounded channel that completes only when every subscriber acknowledges; broker transports complete on the broker's publisher confirm. Each outcome is written back individually and releases the lease; a late failure never overwrites another host's success.
  5. IntegrationEventDispatcher runs each subscription in its own DI scope. The inbox wrapper opens a transaction on the consumer's DbContext, skips events already in its inbox, runs the handler, and records the inbox row in the same transaction.
  6. On success the outbox row is marked processed. On failure it is retried with exponential backoff up to Messaging:Outbox:MaxAttempts, then dead-lettered. Processed rows are kept for ProcessedRetention and OutboxReplayer<TDbContext> can requeue them; consumers that already handled a replayed event skip it.

Handlers run in background scopes without an HTTP user, so they must not depend on ICurrentUser; carry the actor in the event if it matters.

Events published while a handler runs inherit its correlation id and record the handled event as their CausationId (IntegrationEventScope); outside a handler the correlation id is the current trace id.

Transports and scaling out

// A broker package registers its transport and its own consumer host:
services.AddIntegrationEventTransport<RabbitMqIntegrationEventTransport>("RabbitMq");
// The consumer host delivers each queue to one consumer, so queues retry independently:
await dispatcher.DispatchToConsumerAsync(envelope, consumerName, cancellationToken);

Switch environments with Messaging:Transport (InMemory by default); an unregistered name fails at startup resolution with a clear error. Broker consumer hosts can enumerate IntegrationEventRegistry.All to declare one queue per consumer.

Observability

Register MessagingTelemetry.Name (Hmz.Core.Messaging) with OpenTelemetry AddSource and AddMeter. Activities: deliver {event} (producer) and handle {event} (consumer) tagged with message id, correlation id, causation id, consumer, and attempt. Metrics: hmz.messaging.outbox.delivered, .failed, .dead_lettered, hmz.messaging.outbox.delivery.duration (ms), hmz.messaging.inbox.handled, and hmz.messaging.inbox.duplicates.

Fire-and-forget messages

services.AddMessageHandler<ProductCacheInvalidated, InvalidateProductCache>();
messageBus.Publish(new ProductCacheInvalidated(productId)); // returns false when dropped

Configuration

Key Default Purpose
Messaging:Enabled true Runs processors, transports, and housekeeping; disable for tooling-only hosts.
Messaging:Transport InMemory Registered transport name to use.
Messaging:Outbox:LeaseDuration 00:02:00 Claim lease; keep above the slowest delivery.
Messaging:Outbox:DeliveryConcurrency 1 Deliveries in flight per processor; 1 keeps per-instance order.
Messaging:Inbox:Retention 45.00:00:00 Inbox purge age; must be at least ProcessedRetention.
Messaging:Outbox:PollInterval 00:00:05 Fallback poll when no commit signal arrives.
Messaging:Outbox:BatchSize 50 Rows claimed per batch.
Messaging:Outbox:MaxAttempts 10 Attempts before dead-lettering.
Messaging:Outbox:MaxBackoff 00:05:00 Backoff cap.
Messaging:Outbox:ProcessedRetention 30.00:00:00 Replay window before processed rows are purged.
Messaging:TransportCapacity / TransportConcurrency 256 / 4 In-memory transport queue and consumers.
Messaging:MessageBusCapacity 10000 Fire-and-forget queue length before messages are dropped.

The outbox claim query uses PostgreSQL SKIP LOCKED syntax. Applications own the outbox and inbox migrations in each module schema.

Known limitations (deferred)

Recorded during the 0.1.15 review and intentionally not implemented yet; none can deadlock, and the first only matters with several instances.

  • Lease per batch. A batch shares one lease taken at claim time. With several instances and sequential delivery, rows late in a large batch can outlive LeaseDuration and be redelivered by another instance (the inbox absorbs it). Planned: renew the lease per message before delivery.
  • Claim sort under backlog. The pending index leads with NextAttemptAt while the claim orders by OccurredAt, so a large backlog is sorted on each poll. Planned: a pending partial index on (OccurredAt, Id).
  • Unbatched retention deletes. Outbox and inbox purges run one delete per sweep. Planned: bounded batch deletes.

Secret comparison

Use FixedTimeSecretComparer.Equals(expected, actual) for application-owned API keys, webhook signatures, and similar shared-secret checks. The helper hashes both values to fixed-size buffers before using CryptographicOperations.FixedTimeEquals, so differing input lengths do not reintroduce an early-exit comparison.

Keep secret resolution and rotation in the consuming application. This helper does not read configuration, log values, define an authentication scheme, or replace a real identity provider when user identity and authorization are required.

Permission authorization

Hmz.Core.SharedKernel.Authorization provides transport-neutral permission contracts. Keep ICurrentUser focused on identity and populate a request-scoped IPermissionSnapshotAccessor from application data after the external identity has been resolved.

var snapshot = new PermissionSnapshot(
    userId,
    permissions.ToHashSet(StringComparer.Ordinal),
    userPermissionStamp,
    rolePermissionStamps);

permissionSnapshotAccessor.Set(snapshot);

PermissionChecker grants an exact permission or a deliberately configured wildcard such as USER-MANAGEMENT.ROLES.*. Missing and empty snapshots deny access. Applications should combine permissions from every assigned role before constructing the snapshot.

The ASP.NET Core integration is under Hmz.Core.SharedKernel.Http.Authorization:

services.AddPermissionAuthorization();

endpoint.RequirePermission(UserManagementPermissions.Roles.Manage);

Combine permissions with AND and OR through PermissionRule. A permission code converts to a rule implicitly; each RequirePermission... call adds one requirement and every requirement must pass, so chained calls are AND:

endpoint.RequireAllPermissions(Users.Read, Brands.Read);       // A and B
endpoint.RequireAnyPermission(Product.Read, Product.Manage);   // A or B
endpoint.RequireAnyPermission(Quotes.Approve,                  // A or (B and C)
    PermissionRule.All(Quotes.Manage, Prices.Read));
endpoint.RequirePermission(Users.Read).RequirePermission(Brands.Read); // A and B

// Outside endpoints, against the current snapshot:
if (permissionChecker.Satisfies(PermissionRule.Any(Product.Read, Product.Manage))) { ... }

Rules validate their codes when built, render as text (A or (B and C)) for documentation, and add PermissionRuleMetadata to the endpoint. Prefer a dedicated permission over deep nesting when a combination describes a real job.

Use application-owned constants at endpoints rather than string literals.

Resource policies (resource- and attribute-based rules)

Permission checks answer "may this user do this kind of action at all". A resource policy answers "may this user do it to this record", combining the permission snapshot with the record (author, owner, status, amount) and the current user. Write one policy per resource type, in the feature folder of the use case that owns the resource:

public sealed class StoryPostPolicy(IPermissionChecker permissions, ICurrentUser user)
    : IResourcePolicy<StoryPost>
{
    public ValueTask<AccessDecision> EvaluateAsync(StoryPost post, string action, CancellationToken ct) =>
        ValueTask.FromResult(action switch
        {
            StoryPostActions.Delete when post.AuthorId == user.UserId => AccessDecision.Allow(),
            StoryPostActions.Delete when permissions.Satisfies(Feeds.Moderate) => AccessDecision.Allow(),
            StoryPostActions.Delete => AccessDecision.Deny("Only the author or a moderator.", StoryErrorCodes.NotAuthor),
            _ => AccessDecision.DenyUnknownAction(action),
        });
}

services.AddResourcePolicy<StoryPost, StoryPostPolicy>();

// In a handler, a bot command or a background job:
var decision = await access.CheckAsync(post, StoryPostActions.Delete, ct);
if (!decision.Allowed) return Result.Failure(decision.ToError(StoryErrorCodes.NotAuthor));
  • Child resources check their parent first: AccessDecision.AllAsync(() => access.CheckAsync(comment.Post, "read", ct), () => OwnRules(comment)) stops at the first denial.
  • IResourceCapabilities<TResource, TCapabilities> returns what one record allows (for example CanEdit, CanReadCost). Query handlers use it to mask response fields, and clients use it to show actions.
  • Business invariants that hold for every actor (status transitions, totals, separation of duties written into business law) belong in the domain model. Policies decide only who may act. A long action switch usually means status rules sit in the policy instead of the aggregate. Dispatch each action to a private method, and split a policy per action only when one action grows large.
  • Lists are filtered in the database (data scopes and named query filters), never row by row through a policy.
  • IAuthorizationService.AuthorizeAsync(user, resource, new ResourceActionRequirement(action)) evaluates the same policy. IAccessDecisionObserver receives every decision, for audit logs. Permission catalogs, role assignments, persistence, cache invalidation, and version-stamp resolution remain responsibilities of the owning application. Do not make JWT permission claims or a claims transformer the application permission source of truth.

Hmz.Core.SharedKernel.AdvancedSearch provides a validated, allow-listed query pipeline. Clients never supply raw Dynamic LINQ.

services.AddAdvancedSearch();

A feature owns its ISearchConfiguration, source query, projection, and optional external resolvers:

public ValueTask<PagedResponse<ProductListItem>> ExecuteAsync(
    AdvancedSearchRequest request,
    CancellationToken cancellationToken = default) =>
    advancedSearch.ExecutePageAsync(
        dbContext.Products.AsNoTracking(),
        product => new ProductListItem(product.Id, product.Name, product.Price),
        request,
        ProductSearchConfiguration.Instance,
        cancellationToken: cancellationToken);

The projection overload projects the source query before applying filters, global search, ordering, counting, and paging. Therefore, MatchKey resolves against the projected result type, not the persistence entity. This supports client-facing field names, renamed fields, SmartEnum .Value members, and aggregate values exposed by the projection while keeping the query provider-translatable.

Important behavior:

  • fields inside one global-search item are OR-ed;
  • separate search items use the requested SearchLogic;
  • filters default to AND;
  • multiple positive values inside one filter are OR-ed;
  • negative multi-value operators are AND-ed;
  • external no-match results remain match-nothing;
  • default ordering should end with a unique field;
  • validation errors use exact request paths;
  • scalar conversion and enum parsing use bounded, safe messages.
  • filter searchValues accepts JSON strings or numbers and normalizes both forms before typed validation.

TODO: Consider optimizing multi-value scalar equality filters to an SQL IN-style value-collection Contains expression (and NOT IN for negative filters). Keep the current expanded equality clauses as the compatibility baseline until translation is verified across supported EF Core providers and projection shapes.

AdvancedSearch.OpenApi supplies reusable metadata and a schema transformer. Endpoint-specific examples remain in the API feature that owns the search contract.

EF Core data support

Hmz.Core.SharedKernel.Data includes:

  • auditable, soft-deletable, and row-version entity bases;
  • EfRepositoryBase<T>;
  • IUnitOfWork and EfUnitOfWork;
  • entity-configuration helpers;
  • audit, soft-delete, and domain-event interceptors.

Repository mutation methods may only update the DbContext change tracker. Commit through the owning module's unit of work:

await repository.AddAsync(entity, cancellationToken);
await unitOfWork.SaveChangesAsync(cancellationToken);

Keep provider-specific constraint interpretation and cross-context transaction policy in application Infrastructure.

Every query filter is named. A specification can bypass single filters with Query.IgnoreQueryFilters([names]) (from Data.Specifications); the others still apply. Ardalis' parameterless IgnoreQueryFilters() still drops all of them. EfRepositoryBase<T> evaluates specifications with HmzSpecificationEvaluator, which applies the named form for lists, projections, CountAsync and AnyAsync:

public sealed class DeletedNotesSpecification : Specification<Note>
{
    public DeletedNotesSpecification() =>
        Query.IgnoreQueryFilters([QueryFilterNames.SoftDelete]).Where(note => note.IsDeleted);
}

The filter name belongs to whoever declares the filter: QueryFilterNames for SharedKernel's own, an application constant for its data scopes.

Named query filters and data scopes

The soft-delete filter is named QueryFilterNames.SoftDelete. Applications add their own named filters for an interface, once, and SharedKernel applies them to every root entity type that implements it:

modelBuilder.ApplyQueryFilter<IBrandOwned>(
    "Brand",
    owned => !BrandScopeEnforced || owned.BrandId == ActiveBrandId);

Read request state from the DbContext (its own members, or an interface it implements): EF Core swaps that reference for the running context on each query, while any other captured value is frozen into the cached model.

Data.Scoping describes a partition such as a tenant or brand: IDataScope<TKey> is the request scope (IsEnforced, ScopeId), IDataScopeKind<TKey> names the filter and key property, and an entity implements IScopedEntity<TScope, TKey> once per scope it belongs to. DataScopeWriteGuardInterceptor<TScope, TKey> refuses to save a scoped entity outside the enforced partition (DataScopeViolationException); register it before the audit, soft-delete and domain-event interceptors.

Persistence errors

Use cases depend on the provider-neutral IPersistenceErrorClassifier from Data.Errors and its checks: IsUniqueViolation(ex, constraint?), IsForeignKeyViolation(ex, constraint?), IsStringTooLong, IsConcurrencyConflict and IsTransient. Register the PostgreSQL implementation once with services.AddPostgresPersistenceErrorClassifier(). Modules add constraint-specific checks as their own extension members (for example IsCollectionCodeUniqueViolation(ex)) rather than defining classifier interfaces.

Save interceptors

AuditableInterceptorBase and SoftDeleteInterceptorBase handle both SaveChanges and SaveChangesAsync. Derived interceptors override the before-save pair together, or the after-save pair together, and let ChangeTracker.Entries() run DetectChanges.

Demo data seeds through Data.Seeding: context.SeedIfEmptyAsync(() => samples) inserts only into an empty table (soft-deleted and other-scope rows count), and context.HasAnyRowsAsync<T>() guards seeds that build dependent rows. Reference data the application depends on is seeded item by item instead.

Validation helpers

Hmz.Core.SharedKernel.Validation has two layers.

Domain invariants throw for the named parameter and return the value for inline use:

Code = code.RequireWithMaxLength(MaxCodeLength, nameof(code)).ToUpperInvariant();
Description = description.OptionalWithMaxLength(MaxDescriptionLength, nameof(description));
BrandId = brandId.RequirePositive(nameof(brandId));

Strings: IsBlank, TrimToNull, ExceedsMaxLength (after trimming), RequireWithMaxLength, OptionalWithMaxLength. Numbers (any INumber<T>): RequirePositive, RequireNonNegative, RequireInRange, and for nullable values OptionalNonNegative, OptionalInRange.

Request validation returns a validation Error with the field target, or null when the value is valid, so a validator reports every field at once:

var validation = FieldErrors.Collect(
    request.Sku.RequiredError(code, "sku", "SKU"),
    request.Name.MaxLengthError(MaxNameLength, code, "name", "Name"),
    request.WidthMm.NonNegativeError(code, "widthMm", "Width"));
if (validation.IsFailure)
    return Result.Failure<ProductDetails>(validation.Errors);

HTTP boundary support

Hmz.Core.SharedKernel.Http currently provides:

  • Result-to-Problem-Details mapping;
  • validation problem results;
  • global and request-body exception handlers;
  • strong ETag parsing/formatting;
  • precondition results and conditional-request OpenAPI helpers;
  • streamed single-file multipart uploads (Http.Uploads).

These types use ASP.NET Core and belong only at HTTP boundaries. Domain and UseCase projects should remain transport neutral.

ETags use quoted invariant row versions such as "42". Clients should treat the complete quoted value as opaque and return it unchanged through If-Match or If-None-Match.

Register the shared Problem Details behavior in an API host:

services.AddHmzProblemDetails();

Malformed request binding is sanitized. Only messages represented by Serialization.SafeJsonException may expose bounded actionable details.

Streamed file uploads

MultipartUploads.ReadSingleFileAsync reads a multipart/form-data request without buffering it and hands the first file section to a callback. The section content is a BoundedReadStream that throws UploadTooLargeException one byte past MaxFileBytes, so oversized files fail instead of being truncated.

app.MapPost("files", (HttpContext context, ISender sender, CancellationToken ct) =>
    MultipartUploads.ReadSingleFileAsync(
        context,
        new MultipartUploadOptions(maxFileBytes),
        async (file, token) =>
        {
            var result = await sender.Send(
                new UploadFileCommand(file.Content, file.FileName, file.DeclaredLength),
                token);
            return result.IsFailure ? result.ToProblem(context) : TypedResults.Ok(result.Value);
        },
        ct))
    .DisableAntiforgery();
Condition Response Code
Not multipart/form-data 415 problem http.upload-unsupported-media-type
Missing/invalid boundary, truncated or malformed body 400 validation problem (request) http.upload-invalid-boundary, http.upload-malformed
Content-Length above MaxFileBytes + MultipartHeadroomBytes (default 1 MiB), file above MaxFileBytes, or any UploadTooLargeException from the callback 413 problem http.upload-too-large
No file section 400 validation problem keyed by FileField http.upload-file-required

Rules:

  • The callback must consume Content before returning.
  • FileName and ContentType are client-supplied: detect the real type from content and never use the name as a storage path.
  • Application exceptions may derive from UploadTooLargeException (for example a per-purpose limit enforced after storage) and still map to 413.
  • Other exceptions, including storage IOExceptions, propagate to the global exception handler.

Additional dependencies

The current package references Mediator abstractions, ASP.NET Core/OpenAPI, EF Core (including EF Core Relational for messaging persistence), Npgsql (PostgreSQL error classification), System.Linq.Dynamic.Core, and temporary Ardalis building blocks for guards, shared entities, specifications, SmartEnum, repositories, and service introspection. Versions are centrally managed in Directory.Packages.props.

Build, test, and pack

dotnet build Hmz.Core.SharedKernel/Hmz.Core.SharedKernel.csproj `
  --configuration Release

dotnet run `
  --project tests/Hmz.Core.SharedKernel.UnitTests/Hmz.Core.SharedKernel.UnitTests.csproj `
  --configuration Release `
  --no-build `
  -- --minimum-expected-tests 1

dotnet pack Hmz.Core.SharedKernel/Hmz.Core.SharedKernel.csproj `
  --configuration Release `
  --output <package-output>

Current version: 0.1.17, prepared for NuGet publication. Inspect the package before publishing and never republish the same version; advance the project version and changelog for any subsequent package-content change.

Extension rules

  • Add only cross-project contracts with demonstrated reuse.
  • Keep feature-specific models and policies in the owning application.
  • Keep HTTP and EF Core dependencies in their explicit namespaces and out of Domain code.
  • Add tests for compatibility-sensitive behavior and public contracts.
  • Preserve existing comments; add or update comments only when behavior changes make them inaccurate or clarification is important.
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Hmz.Core.SharedKernel:

Package Downloads
Hmz.Core.CrudKit

CRUD toolkit for Hmz projects, built on Hmz.Core.SharedKernel

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.17 65 10/21/2026
0.1.16 91 10/19/2026
0.1.15 82 10/13/2026
0.1.14 95 10/9/2026
0.1.13 87 10/9/2026
0.1.13-pre03 80 10/9/2026
0.1.13-pre01 79 10/9/2026
0.1.12 89 10/8/2026
0.1.11 243 9/26/2026
0.1.10 128 9/21/2026
0.1.9 138 9/12/2026
0.1.8 134 9/7/2026
0.1.7 144 5/8/2026
0.1.6 134 5/7/2026
0.1.5 125 5/6/2026
0.1.4 129 4/23/2026
0.1.3 139 4/22/2026
0.1.1 134 4/20/2026
0.1.0 135 3/22/2026