Skip to content

docs: initial vertz core API design plan#2

Merged
viniciusdacal merged 4 commits intomainfrom
docs/core-api-design
Feb 4, 2026
Merged

docs: initial vertz core API design plan#2
viniciusdacal merged 4 commits intomainfrom
docs/core-api-design

Conversation

@viniciusdacal
Copy link
Copy Markdown
Contributor

Defines the functional API design for:

  • Environment variables with schema validation
  • Middlewares with typed requires/provides
  • Module definitions (contracts)
  • Services with dependency injection
  • Routers with schema validation
  • Module assembly
  • App composition with builder pattern
  • Context immutability patterns

Defines the functional API design for:
- Environment variables with schema validation
- Middlewares with typed requires/provides
- Module definitions (contracts)
- Services with dependency injection
- Routers with schema validation
- Module assembly
- App composition with builder pattern
- Context immutability patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comment thread plans/vertz-core-api-design.md Outdated
Comment thread plans/vertz-core-api-design.md Outdated
Comment thread plans/vertz-core-api-design.md Outdated
Comment thread plans/vertz-core-api-design.md Outdated
Comment thread plans/vertz-core-api-design.md
Comment thread plans/vertz-core-api-design.md Outdated
Comment thread plans/vertz-core-api-design.md Outdated
Comment thread plans/vertz-core-api-design.md Outdated
@viniciusdacal viniciusdacal self-assigned this Feb 3, 2026
viniciusdacal and others added 3 commits February 3, 2026 19:10
- Flatten module folders (remove services/ and routers/ subfolders)
- Replace `as` notation with generics for middleware requires/provides
- Remove next() from middlewares — handler returns state, framework composes
- Make ctx.state immutable (composed from middleware return values)
- Add private service methods example (closure pattern)
- Add integration testing section
- Fix import paths to match flat structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Distinguishes the two contexts clearly:
- `deps` (dependencies) for services — static, created once at startup
- `ctx` (request context) for router handlers — fresh per request

Avoids ambiguity between module-level and request-level context.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- One schema file per endpoint in schemas/ folder
- Naming: {operation}{Entity}{Part} (e.g., createUserBody)
- Natural speech order for non-CRUD ops (bulkCreateUsers, not createBulkUsers)
- CRUD names: create, read, update, delete, list
- Response schema required if handler returns a value (compiler enforced)
- Params and headers remain inline
- Updated router examples with schema imports and response schemas

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@matheuspoleza matheuspoleza left a comment

Choose a reason for hiding this comment

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

TOOP!

@viniciusdacal viniciusdacal merged commit 9a25082 into main Feb 4, 2026
vertz-tech-lead Bot pushed a commit that referenced this pull request Feb 11, 2026
- Rename find() to findOne() for clarity (Josh #1)
- Separate read/write visibility: $insert includes .hidden() columns (Josh #2, Ben C3)
- Fix select union type with never-keyed branches for mutual exclusivity (Ben B4)
- Move cache-readiness primitives to v1.1 preview section (PM scope creep)
- Fix Non-Goal #7 to not claim v1 ships cache primitives (PM)
- Add exhaustiveness guarantee for error hierarchy with Assert pattern (Ben B1)
- Add type error quality section with branded error messages (Ben B2)
- Flag d.ref.many().through() as unvalidated, cap depth at 1 (Ben B3)
- Fix E2E type test assertions to match actual type semantics (Josh #3)
- Fix first example to compile under strict mode with ! assertion (Josh #4)
- Add vertz db init onboarding flow (Josh #5)
- Document zero-match behavior for all mutation methods (Josh #8)
- Add SQL injection prevention / parameter binding note (PM minor)
- Add dry-run mode for migrations (PM minor)
- Clarify d.email() is metadata-only, no runtime validation (Josh #6, Ben N4)
- Add vertz.env() pattern for type-safe DATABASE_URL access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
viniciusdacal pushed a commit that referenced this pull request Feb 11, 2026
* docs(db): add @vertz/db v1 API design plan

Comprehensive design doc for the thin ORM layer covering schema definitions,
type inference, query builder, relations, migrations, error hierarchy,
and metadata-only multi-tenancy markers. Based on approved roadmap,
POC 1 results (28.5% of budget), and all exploration research.

Includes self-review notes from Josh (DX), Ben (feasibility), and PM (scope).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(db-design): address review feedback from Josh, Ben, and PM

- Rename find() to findOne() for clarity (Josh #1)
- Separate read/write visibility: $insert includes .hidden() columns (Josh #2, Ben C3)
- Fix select union type with never-keyed branches for mutual exclusivity (Ben B4)
- Move cache-readiness primitives to v1.1 preview section (PM scope creep)
- Fix Non-Goal #7 to not claim v1 ships cache primitives (PM)
- Add exhaustiveness guarantee for error hierarchy with Assert pattern (Ben B1)
- Add type error quality section with branded error messages (Ben B2)
- Flag d.ref.many().through() as unvalidated, cap depth at 1 (Ben B3)
- Fix E2E type test assertions to match actual type semantics (Josh #3)
- Fix first example to compile under strict mode with ! assertion (Josh #4)
- Add vertz db init onboarding flow (Josh #5)
- Document zero-match behavior for all mutation methods (Josh #8)
- Add SQL injection prevention / parameter binding note (PM minor)
- Add dry-run mode for migrations (PM minor)
- Clarify d.email() is metadata-only, no runtime validation (Josh #6, Ben N4)
- Add vertz.env() pattern for type-safe DATABASE_URL access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: vertz-tech-lead[bot] <2828099+vertz-tech-lead[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: vertz-dev-dx[bot] <260432280+vertz-dev-dx[bot]@users.noreply.github.com>
vertz-tech-lead Bot pushed a commit that referenced this pull request Feb 14, 2026
Addresses reviewer feedback issue #2: missing changeset.
vertz-tech-lead Bot pushed a commit that referenced this pull request Feb 14, 2026
TDD RED → GREEN cycle #2:
- Write test expecting tasks.loading → tasks.loading.value
- Add 'loading' to query signal properties
- Test passes, all quality gates pass

Expanding signal auto-unwrap coverage.
github-actions Bot pushed a commit that referenced this pull request Feb 14, 2026
TDD RED → GREEN cycle #2:
- Write test expecting tasks.loading → tasks.loading.value
- Add 'loading' to query signal properties
- Test passes, all quality gates pass

Expanding signal auto-unwrap coverage.
viniciusdacal pushed a commit that referenced this pull request Feb 14, 2026
…rap (#283)

* test(ui-compiler): add test for query().data auto-unwrap

TDD RED → GREEN cycle #1:
- Write test expecting tasks.data → tasks.data.value
- Implement signal API registry (query with data property)
- Update ReactivityAnalyzer to detect signal API calls
- Update SignalTransformer to auto-unwrap signal properties
- Test passes, typecheck passes, lint passes

Part of signal auto-unwrap feature to eliminate .value from public API.

* test(ui-compiler): add test for query().loading auto-unwrap

TDD RED → GREEN cycle #2:
- Write test expecting tasks.loading → tasks.loading.value
- Add 'loading' to query signal properties
- Test passes, all quality gates pass

Expanding signal auto-unwrap coverage.

* feat(ui-compiler): complete signal auto-unwrap for query, form, createLoader

TDD RED → GREEN cycle #3:
- Add tests for .error property on query()
- Add tests for form() with submitting, errors, values
- Add tests for createLoader() with data, loading, error
- Update signal API registry with all properties
- All tests pass (230 tests total)

Feature complete: Auto-unwrap eliminates .value from public API for all three signal-returning functions.

* feat(ui-compiler): support aliased imports for signal auto-unwrap

TDD RED → GREEN cycle #4:
- Add test for aliased imports (query as fetchData)
- Add test for plain properties (refetch) - already passes
- Implement buildImportAliasMap to track import aliases
- Update ReactivityAnalyzer to resolve aliases before checking signal APIs
- All tests pass (232 tests total)

Edge cases covered: import aliases, plain vs signal property distinction.

* docs(ui-compiler): document plain properties in signal API registry

Add plainProperties field to SignalApiConfig to explicitly document
which properties are NOT signals (refetch, reset, submit, handleSubmit).

This improves code clarity and serves as documentation for developers.
The implementation already handles these correctly - they're not unwrapped
because they're not in the signalProperties set.

No functional change, just documentation.

* chore: add changeset for signal auto-unwrap feature

* fix: prevent double .value bug + classify as MAJOR (TDD: RED→GREEN)

**TDD Cycle:**
1. RED: Add test for double .value case (migration) — fails as expected
2. GREEN: Add guard logic in signal-transformer to skip when .value exists — test passes

**Changes:**
- Add guard in transformSignalApiProperties() to detect existing .value
- Add test: 'should NOT double-unwrap when .value already exists'
- Change changeset from minor → major (BREAKING CHANGE)
- Add comprehensive migration guide to changeset
- Document Set<string> serialization limitation in VariableInfo type

**Addresses reviewer feedback:**
✅ Fix double .value bug with guard logic
✅ Add TDD test FIRST (RED), then fix (GREEN)
✅ Reclassify as major breaking change
✅ Add migration docs
✅ Document Set<string> non-serializable concern

All 233 tests pass (8/8 signal-unwrap tests).

* fix: use vitest import instead of bun:test for CI compatibility

CI uses vitest runner, not bun:test. Changed import to fix module resolution error.

---------

Co-authored-by: auditor <auditor@vertz.dev>
viniciusdacal pushed a commit that referenced this pull request Feb 17, 2026
- Add Cloudflare Worker for GitHub Projects board viewer
- Fetch project #2 (Vertz Roadmap) data via GraphQL API
- Display issue status, assignees, priority labels, and PR links
- Add wrangler.toml configuration following vertz-docs precedent
- Add README with deployment and setup instructions

Closes #377
vertz-tech-lead Bot added a commit that referenced this pull request Feb 22, 2026
Addresses nora's CRITICAL finding in adversarial review #2.

**Problem:** Specific error classes extended FetchError directly, not HttpError:
  const err = new FetchNotFoundError('test');
  err instanceof HttpError  // FALSE! ❌

This broke all code doing:
  if (error instanceof HttpError) { ... }

**Fix:**
1. Changed all specific errors to extend HttpError instead of FetchError
2. Inherit status and serverCode from HttpError constructor
3. Use 'HTTP_ERROR' code to match parent class
4. Added type guards for all specific error classes:
   - isFetchBadRequestError()
   - isFetchUnauthorizedError()
   - isFetchForbiddenError()
   - isFetchNotFoundError()
   - isFetchConflictError()
   - isFetchGoneError()
   - isFetchUnprocessableEntityError()
   - isFetchRateLimitError()
   - isFetchInternalServerError()
   - isFetchServiceUnavailableError()

**Verification:**
  const err = new FetchNotFoundError('test', 'CODE');
  err instanceof HttpError             // true ✅
  err instanceof FetchNotFoundError    // true ✅
  isHttpError(err)                     // true ✅
  isFetchNotFoundError(err)            // true ✅
  err.status === 404                   // true ✅

All 63 fetch tests passing ✅
All 256 server tests passing ✅
viniciusdacal pushed a commit that referenced this pull request Mar 13, 2026
Replace hand-written auth patterns with framework abstractions:
- ProtectedRoute replaces AuthGuard component (#1214)
- OAuthButton replaces hardcoded OAuth URL + GitHub icon (#1215)
- sessionResolver added to dev server for SSR session injection (#1216)
- Update auth-ui-framework-gaps.md to mark gaps #1, #2, #4 as done

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Mar 13, 2026
… fixes (#1208)

* feat(auth): per-provider mapProfile callback for OAuth user creation

Add typed mapProfile callbacks to OAuth providers so provider-specific
data (name, avatarUrl, custom fields) flows through to the created user.

- OAuthProviderConfig<TProfile> generic with typed mapProfile callback
- OAuthUserInfo.raw passes full provider API response
- OAuthProvider.mapProfile required on provider instances
- Typed profiles: GithubProfile, GoogleProfile, DiscordProfile
- Secure spread: framework fields (id, email, emailVerified, role,
  createdAt, updatedAt) always override mapProfile output
- mapProfile errors redirect with ?error=profile_mapping_failed
- Non-object mapProfile returns treated as empty

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(auth): onUserCreated callback and auth-entity bridge

Replace mapProfile with onUserCreated callback that fires after auth
user creation. Developers populate their own entity tables in the
callback instead of mapping profile data into auth_users.

- Add onUserCreated to AuthConfig with discriminated union payload
  (OAuth: { user, provider, profile }, email: { user, signUpData })
- Add deleteUser to UserStore for rollback on callback failure
- Wire entity registry proxy via _entityProxy in createServer
- Remove mapProfile from OAuthProvider, OAuthProviderConfig, providers
- Remove name/avatarUrl from OAuthUserInfo (raw carries full profile)
- Close AuthUser interface (remove [key: string]: unknown)
- Remove safeFields spread into AuthUser on email sign-up
- Add 'general' to AuthValidationError.field for callback errors
- Rollback with error logging on both OAuth and email/password paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(auth): correctly append error params to OAuth error redirect URLs

The OAuth callback handler was naively appending `?error=...` to the
error redirect URL without checking if it already contained query
parameters. When oauthErrorRedirect was set to `/login?error=oauth`,
the resulting URL was `/login?error=oauth?error=token_exchange_failed`
(double `?`). Added an `errorUrl()` helper that uses `&` when `?`
already exists. Also improved GitHub provider to surface token exchange
errors instead of silently swallowing them, and added DOM shim stubs
for addEventListener/removeEventListener needed during SSR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(auth): resolve OAuth login flow bugs across server and client

Server fixes:
- Auto-wire DbOAuthAccountStore in createServer (was missing, OAuth accounts never persisted)
- Fix cookie maxAge to match JWT TTL (was using hardcoded value causing premature expiry)
- Fix db-session-store findActiveSessionById to use raw SQL (ORM gt operator broken)

Client fixes:
- Move useAuth from signal-api to reactive-source in compiler registry and reactivity.json
  (compiler was adding .value on top of wrapSignalProps getters, causing undefined)
- Defer refresh() via setTimeout(0) when no SSR session exists
  (SSR flushes microtasks, so status stayed idle during server render instead of redirecting)
- Update auth-context tests to match deferred-refresh behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(auth): remove unused SessionRecord type and recordToSession method

Leftover from switching findActiveSessionById to raw SQL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(auth): update DbSessionStore tests for raw SQL findActiveSessionById

Tests were mocking the ORM-based auth_sessions.get() path, but the
implementation now uses db.query() with raw SQL. Updated mocks to
match the new interface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(auth): self-review of auth UI framework gaps from Linear clone

Identifies 7 patterns hand-written in the Linear example that should
be framework-level: ProtectedRoute, OAuth buttons, dev server handler
composition, SSR session injection, sign-out redirect, user profile
helpers, and router initialPath auto-detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(auth): address review findings — race condition and conditional auto-wiring

- Cancel deferred refresh timer when signIn/signUp is called, preventing
  a race where refresh() could overwrite authenticated status with
  unauthenticated (the refresh cookie isn't set yet from the sign-in)
- Only auto-wire DbOAuthAccountStore when OAuth providers are configured
- Only create entity registry proxy when onUserCreated callback exists

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* review(auth): adversarial review for OAuth login flow fixes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(server): remove erroneous OrgPlan re-export from rebase conflict

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(auth): add issue reference for raw SQL workaround in DbSessionStore (#1209)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(auth): revert raw SQL workaround in DbSessionStore to ORM get()

Now that #1212 fixed null handling in where clauses (IS NULL instead of
= NULL), all three session lookup methods use the ORM consistently.
Restores recordToSession and SessionRecord for ORM camelCase records.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(auth): use signOut({ redirectTo }) from #1213 in Linear clone

Now that the framework supports signOut with redirect, use it instead
of relying on AuthGuard's implicit redirect. Updated framework gaps
doc to mark item #5 as done.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(linear): adopt framework auth components (#1214, #1215, #1216)

Replace hand-written auth patterns with framework abstractions:
- ProtectedRoute replaces AuthGuard component (#1214)
- OAuthButton replaces hardcoded OAuth URL + GitHub icon (#1215)
- sessionResolver added to dev server for SSR session injection (#1216)
- Update auth-ui-framework-gaps.md to mark gaps #1, #2, #4 as done

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(linear): adopt createRouter auto-detect (#1219), fix error redirect test

- Remove initialPath boilerplate from router.tsx — createRouter now auto-detects
- Update error redirect test to match URL constructor behavior from #1218
- Mark gap #7 (initialPath auto-detect) as done in framework gaps doc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(linear): use app.requestHandler for unified routing (#1221)

Replace manual if/else auth+entity routing with framework's
requestHandler getter. Mark gap #3 as done in framework gaps doc.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(linear): add Linear clone example app files

Commit all Linear clone example files that were previously untracked.
The bun.lock already references examples/linear — CI fails with
--frozen-lockfile because the package.json wasn't committed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: vertz-dev-front[bot] <2828126+vertz-dev-front[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
viniciusdacal pushed a commit that referenced this pull request Mar 14, 2026
…vel allowWhere/allowOrderBy validation, POCs

- Apply expose.select in crud-pipeline to restrict response fields (list, get, create, update)
- Add entity-level allowWhere/allowOrderBy validation to validateVertzQL()
- Wire expose config through route-generator to validateVertzQL()
- Export ExposeValidationConfig from vertzql-parser
- POC #1: allowWhere/allowOrderBy constrained to PublicColumnKeys at type level,
  subset-of-select enforced at runtime (fallback approach — avoids generic bloat)
- POC #2: T | null typing for descriptor-guarded fields via conditional mapped type
- Update design doc with POC results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Mar 14, 2026
…vel allowWhere/allowOrderBy validation, POCs

- Apply expose.select in crud-pipeline to restrict response fields (list, get, create, update)
- Add entity-level allowWhere/allowOrderBy validation to validateVertzQL()
- Wire expose config through route-generator to validateVertzQL()
- Export ExposeValidationConfig from vertzql-parser
- POC #1: allowWhere/allowOrderBy constrained to PublicColumnKeys at type level,
  subset-of-select enforced at runtime (fallback approach — avoids generic bloat)
- POC #2: T | null typing for descriptor-guarded fields via conditional mapped type
- Update design doc with POC results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Mar 14, 2026
…ion control (#1237)

* feat(server): add Entity Expose API — types, factory, and runtime support

Replace `relations` config on EntityConfig/EntityDefinition with a new
`expose` API that unifies field exposure, relation config, filter/sort
allowlists, and AccessRule descriptors into a fractal structure mirroring
the DB query API shape (select, allowWhere, allowOrderBy, include).

- Add ExposeConfig and RelationExposeConfig types with full generics
- select values accept `true | AccessRule` for descriptor-guarded fields
- include constrains to model relation keys with typed target table columns
- Nested include supports recursive relation exposure
- Update entity() factory, crud-pipeline, route-generator, vertzql-parser
- Update extractAllowKeys() to handle object-shaped allowWhere/allowOrderBy
- Comprehensive type-level tests in expose-types.test-d.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(server): complete Phase 1 — expose.select enforcement, entity-level allowWhere/allowOrderBy validation, POCs

- Apply expose.select in crud-pipeline to restrict response fields (list, get, create, update)
- Add entity-level allowWhere/allowOrderBy validation to validateVertzQL()
- Wire expose config through route-generator to validateVertzQL()
- Export ExposeValidationConfig from vertzql-parser
- POC #1: allowWhere/allowOrderBy constrained to PublicColumnKeys at type level,
  subset-of-select enforced at runtime (fallback approach — avoids generic bloat)
- POC #2: T | null typing for descriptor-guarded fields via conditional mapped type
- Update design doc with POC results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(server): add descriptor runtime evaluation for expose API (Phase 2)

- Add expose-evaluator.ts with evaluateExposeDescriptors() that pre-evaluates
  AccessRule descriptors once per request, producing static field sets
- Descriptor-guarded select fields return null when user lacks access
- Descriptor-guarded allowWhere/allowOrderBy fields reject with "not filterable"/"not sortable"
- Add nullGuardedFields() to field-filter for nulling descriptor-denied fields
- Wire evaluation through route-generator for all CRUD handlers
- Add EvaluatedExposeValidation to validateVertzQL() for dynamic field checks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(server): add entity exposure guide — Fields, Relations & Filters (Phase 3)

- New page guides/server/entity-exposure.mdx covering expose config:
  select, allowWhere, allowOrderBy, include, descriptors, null semantics
- Add navigation entry in docs.json after entities
- Add card and feature row in server overview
- Cross-reference from entities.mdx to new guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(server): address review findings — consolidate types, fix unsafe cast

- Change applySelect to accept Record<string, unknown> (it only checks
  key presence, not values — the Record<string, true> type was a lie
  when expose.select contains AccessRule descriptors)
- Remove exposeSelect cast to Record<string, true> in crud-pipeline
- Remove duplicate EvaluatedExposeValidation type — import EvaluatedExpose
  from expose-evaluator instead

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add changeset for entity expose API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: vertz-dev-front[bot] <2828126+vertz-dev-front[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Mar 23, 2026
Enable `{ prepare: true }` on postgres.js `sql.unsafe()` calls in
@vertz/db's postgres driver. This enables server-side prepared statement
caching, reducing per-query overhead by ~60% (0.57ms → 0.24ms/op).

Remove the manual `coerceValue` timestamp coercion layer — postgres.js
handles type conversion natively via built-in OID parsers when connected
normally.

In @vertz/schema, optimize the hot parse path:
- Memoize shape keys Set in ObjectSchema constructor (avoid per-parse alloc)
- Lazy issues array in ParseContext (skip alloc for valid parses)
- Skip unknown key filtering in strip mode (the default)

Benchmarked via rinha-de-backend URL shortener challenge: Vertz achieved
2443 req/s (#1) vs Go 1528 req/s (#2), with p95 latency of 54ms vs 79ms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Mar 23, 2026
…1749)

Enable `{ prepare: true }` on postgres.js `sql.unsafe()` calls in
@vertz/db's postgres driver. This enables server-side prepared statement
caching, reducing per-query overhead by ~60% (0.57ms → 0.24ms/op).

Remove the manual `coerceValue` timestamp coercion layer — postgres.js
handles type conversion natively via built-in OID parsers when connected
normally.

In @vertz/schema, optimize the hot parse path:
- Memoize shape keys Set in ObjectSchema constructor (avoid per-parse alloc)
- Lazy issues array in ParseContext (skip alloc for valid parses)
- Skip unknown key filtering in strip mode (the default)

Benchmarked via rinha-de-backend URL shortener challenge: Vertz achieved
2443 req/s (#1) vs Go 1528 req/s (#2), with p95 latency of 54ms vs 79ms.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Apr 18, 2026
- Correct error codes in docs/changeset: E0761 (not E0763) for
  innerHTML+children, E0764 is SVG-only (no void-element diagnostic
  exists at the compiler level)
- Split "void and SVG" section into separate SVG (E0764) and
  Void elements (runtime no-op, no compile error) sections
- Fix changeset grammar: "helper exports from" -> "helper is exported from"
- Add review Resolution section

BrandedIcon coverage gap (review should-fix #2) deferred to #2790
(compiler bug: innerHTML inside nested component JSX is emitted as
HTML attribute rather than routed through __html()). Production
rendering is unaffected; Phase 1-3 integration tests cover the path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Apr 18, 2026
* feat(ui): add TrustedHTML type + trusted() helper [#2761]

Adds an opaque branded type `TrustedHTML` and a `trusted()` helper as
scaffolding for the upcoming `innerHTML` JSX prop. The brand is purely
type-level (zero runtime cost) and prepares the ground for a future
`no-untrusted-innerHTML` lint rule.

Also lands the design doc and phase-plan files for #2761.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(ui): add __html() runtime helper for reactive innerHTML [#2761]

The compiler target for the forthcoming `innerHTML` JSX prop. Uses
deferredDomEffect so the first assignment is deferred until hydration
finishes, preserving hydration-claimed child nodes during the cursor
walk. Nullish values render as empty string.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(ui): support innerHTML prop in jsx-runtime [#2761]

Extend HTMLAttributes with innerHTML?: string | TrustedHTML | null
and add VoidHTMLAttributes type that rejects innerHTML/children for
void elements (img, br, hr, input, etc.). jsxImpl() destructures
innerHTML out of attrs, throws if both innerHTML and non-empty
children are provided, and sets element.innerHTML directly (never
via setAttribute).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(ui): pin mutual-exclusion behavior and cross-link trusted() [#2761]

Address Phase 1 adversarial review should-fix items:
- Pin {0} and {''} as children to throw alongside innerHTML (surprising-
  but-correct behavior; now regression-protected).
- Pin void-element imperative runtime path (no throw when called
  directly; type-level guarantees cover the real callsite).
- Cross-link trusted() from the __html() security docstring.
- Add IntrinsicElements['img']/['br'] type-level regression checks so
  the index signature can't silently re-open innerHTML on void entries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: add Phase 1 adversarial review [#2761]

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(compiler): emit __html() for innerHTML JSX attribute [#2761]

`<pre innerHTML={value} />` now compiles to `__html(_el, () => value)`
instead of the legacy `_el.setAttribute("innerHTML", value)` path (which
silently failed — the browser's Element.innerHTML has no HTML attribute
counterpart). Static literals and expressions both route through the
same helper so hydration claim order is preserved via deferredDomEffect.

Adds __html to the runtime-helpers auto-import list and exposes it from
@vertz/ui/internals so generated code resolves the call. Mutual-
exclusion with children and the three other diagnostics come in the
next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(compiler): add innerHTML diagnostics E0761/E0762/E0764/W0763 [#2761]

Wire the innerHTML analyzer into the compile pipeline. The visitor walks
JSXElement nodes inside component bodies and emits:

- E0761 when `innerHTML` and JSX children coexist on the same element.
- E0762 when `dangerouslySetInnerHTML` is used (React-only; rejected).
- E0764 when `innerHTML` is set on an SVG element.
- W0763 when a `ref` callback's first statement is `el.innerHTML = …`
  (SSR-silent pattern — guide authors to `innerHTML={…}` instead).

Fourteen unit tests in the module cover positive and negative cases for
each code, plus `ref` callback body variants (block vs expression body,
first-statement-only matching).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(compiler): address Phase 2 review findings [#2761]

- Pin diagnostic message text to design-doc spec; add exact-message tests
  for E0761, E0762, E0764, W0763 so future drift fails a test.
- Run innerHTML diagnostics once at program level (not per component) so
  module-level inline route components are validated too. Adds three
  regression tests covering defineRoutes() arrow components.
- Assert emission order in `inner_html_coexists_with_class_and_events`:
  class setAttribute and onClick listener must both precede __html(…).
- Pin the BooleanShorthand `<pre innerHTML />` drop behavior with a test
  so a future refactor can't silently flip it to an emit.
- Record the Phase 2 review with resolutions for all four should-fix
  items; nits 1, 3, 5 deferred; nit 4 resolved by the program-level move.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(ui-server): add SSR + hydration integration tests for innerHTML [#2761]

Phase 3. Adds end-to-end coverage proving innerHTML survives SSR
serialization, hydration adoption, and reactive updates. Uncovered a
runtime bug where __html() skipped the hydration claim-tracking walk
because children are replaced post-hydration via deferredDomEffect;
fixed by exporting markSubtreeClaimed() and calling it from __html()
so the unclaimed-node diagnostic stays quiet when SSR markup matches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(ui-server): address Phase 3 review findings [#2761]

- Reactive-update step now mutates the hydrated <pre> through the
  deferred effect bound at endHydration (Should-fix 1).
- Dropped trivial innerHTML=undefined client-path assertion; unit test
  in html.test.ts already covers that branch (Should-fix 2).
- Added compile() scenario that asserts __html() emission and className-
  before-__html attribute ordering, guarded with skipIf(!hasNativeCompiler)
  (Should-fix 3).
- Added dangerous-markup scenario pinning the "we do not sanitize"
  contract across SSR and hydration (Nit 1).
- Filed #2781 for the pre-existing toVNode barrel gap; linked in review.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(ui): migrate in-repo callers to innerHTML + mint-docs page [#2761]

Phase 4 of raw HTML injection:

- @vertz/ui-auth: brandedIcon factory → <BrandedIcon> JSX component
  using <span innerHTML={getProviderIcon(...)} />.
- component-docs CodeBlock: <Foreign onReady={(el) => el.innerHTML=x}>
  → <div innerHTML={highlighted} />. The inline pre background
  override is removed; the same rule is already covered by a CSS
  !important in globals.ts.
- @vertz/icons render-icon and @vertz/ui Foreign: kept imperative
  with rationale comments — icons has no compiler plugin and
  Foreign is a hand-written framework primitive.
- mint-docs: new guides/ui/raw-html.mdx covering usage, XSS,
  DOMPurify, trusted(), mutual exclusion, void/SVG restrictions,
  and React migration. Added to docs.json nav.

Follow-ups filed:
- #2788 compiler: ref + innerHTML on host element emits invalid
  code (worked around by dropping ref in code-block).
- #2789 oxlint: no-untrusted-innerHTML rule.

Closes #2761.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(ui): address Phase 4 review findings [#2761]

- Correct error codes in docs/changeset: E0761 (not E0763) for
  innerHTML+children, E0764 is SVG-only (no void-element diagnostic
  exists at the compiler level)
- Split "void and SVG" section into separate SVG (E0764) and
  Void elements (runtime no-op, no compile error) sections
- Fix changeset grammar: "helper exports from" -> "helper is exported from"
- Add review Resolution section

BrandedIcon coverage gap (review should-fix #2) deferred to #2790
(compiler bug: innerHTML inside nested component JSX is emitted as
HTML attribute rather than routed through __html()). Production
rendering is unaffected; Phase 1-3 integration tests cover the path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Apr 18, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Apr 18, 2026
)

* feat(schema): add public ArraySchema.element getter [#2771]

Phase 1 of #2771 — exposes the existing private `_element` field on
`ArraySchema` so external consumers can introspect the element schema
without reaching into private state.

Used by the upcoming `coerce.ts` utility (Phase 2) to walk a schema
tree and coerce FormData values to schema-declared types in `form()`.

Additive only; no behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(schema): type ArraySchema.element as Schema<T> [#2771]

Drops the gratuitous `as Schema<unknown>` cast flagged by Phase 1 review;
preserves element-type information for downstream coerce.ts callers.
Matches the pattern used by ObjectSchema.shape (no widening).

Adds Phase 1 adversarial review at reviews/2771-form-coerce-field-types/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(ui): add schema-driven coerce.ts utility for form() [#2771]

Adds two pure helpers under packages/ui/src/form/coerce.ts:

- coerceLeaf(value, leafSchema): walks Optional/Default/Nullable/Refined
  wrappers via _schemaType()+unwrap, then coerces by inner type:
  Boolean → strict on/off/0/1/true/false set; Number/BigInt → numeric
  string parsing with empty-string drop; Date → parseable string → Date.
  String/Enum/Literal/Lazy/custom-adapter → passed through.
- coerceFormDataToSchema(formData, schema): dispatches on schema._schemaType()
  to assemble a coerced object. Object → walk shape; Array of primitives →
  formData.getAll().map(coerceLeaf); Array of objects → fall back to
  formDataToObject({nested:true}) so dotted-index data is preserved.
  Non-Vertz adapters and non-object schemas fall back to formDataToObject.

Utility stays internal (no public re-export). 60 BDD-style tests cover
every coerceLeaf table row plus the coerceFormDataToSchema scenarios from
the design doc; coverage 96.4% line / 95.4% branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ui): skip File entries when coercing form leaves [#2771]

readLeafFromFormData now returns undefined for any non-string FormData
value, mirroring the File-skip behavior of formDataToObject. Without
this, a File entry on a Boolean-typed field would coerce to true via
Boolean(file). File-typed schemas remain out of scope for this utility.

Adds two tests (File on Boolean → false, File on Number → drop) and
the Phase 2 adversarial review.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(ui): wire schema-driven coercion into form() submit and blur revalidation [#2771]

Submit pipeline now uses coerceFormDataToSchema when the body schema is
available, so checkbox booleans, numeric inputs, dates and bigints reach
the SDK as their proper types. Blur revalidation calls coerceLeaf on the
field value before validateField, so the revalidate-on-blur path agrees
with the submit path.

resolveFieldSchema is exported so form.ts can resolve a leaf schema once
per blur without duplicating traversal logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(ui): document FormData coercion in form() + changeset [#2771]

Adds a "FormData coercion" section to the Forms guide with a per-leaf
coercion table (boolean/number/bigint/date/string and arrays of
primitives), plus a brief mirror in the form() API reference linking
back to the guide. Records the user-facing change in a patch changeset
covering @vertz/ui and @vertz/schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(schema,ui): unwrap refine/superRefine so form() coerces refined object schemas [#2771]

Phase 3 review found that a top-level .refine() / .superRefine() on the
body schema silently disabled FormData coercion: neither wrapper exposed
.unwrap(), so coerceFormDataToSchema fell through to the non-coerced
fallback. The user pattern that breaks is the canonical cross-field
validator, e.g. s.object({ password, confirm }).refine(...).

This adds .unwrap() to RefinedSchema and SuperRefinedSchema, matching
the existing pattern on Optional/Default/Nullable. coerceFormDataToSchema
now reaches the inner ObjectSchema and walks its .shape as expected.

Also addresses Phase 3 review:
- Adds two coerce.test.ts cases (top-level refined / superRefined object).
- Adds two form-coercion.test.ts E2E cases (refined object + custom
  no-_schemaType adapter regression guard).
- Reuses the existing createMockFormElement helper pattern in the blur
  test instead of duplicating it inline.
- Tightens docs: empty number strings drop the field but do not echo
  default() back into the SDK body.
- Updates the changeset to mention the new unwrap() accessors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(reviews): record Phase 3 review resolution [#2771]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ui): unwrap top-level wrappers in resolveFieldSchema for blur revalidation [#2771]

Re-review caught a residual gap from the previous refine/superRefine fix:
submitPipeline now coerces refined object schemas correctly, but blur
revalidation went through resolveFieldSchema which read schema.shape
directly without unwrapping the top-level wrapper. With s.object({...}).refine(),
resolveFieldSchema returned undefined → the blur path validated the raw
string and produced a stale "Expected number, received string" error
that contradicted the post-coercion submit result.

Extracts the existing intermediate-segment unwrap loop into a reusable
unwrapToShape helper and applies it to the top-level schema as well as
each intermediate path segment. Adds a TDD test in form-coercion.test.ts
that proves blur revalidation on a refined number field clears the error
after the user fixes the value.

Also adds a docs <Note> calling out the remaining wrappers
(.transform / .pipe / .catch / .brand / .readonly) that still disable
top-level coercion, so users with those patterns aren't surprised.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(reviews): record Phase 3 re-review #2 approval [#2771]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove local phase reviews before merging to main [#2771]

Per .claude/rules/local-phase-workflow.md, reviews/ are working artifacts
and should not be committed to main. The PR description summarizes them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: apply oxfmt formatting to coercion files [#2771]

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Apr 19, 2026
Phase 2 of durable tool execution. Wires appendMessagesAtomic into the
loop so tool-calling steps commit durably in two atomic writes per step
(assistant-with-toolCalls, then tool_results), and deletes the obsolete
checkpointInterval + onCheckpoint pair.

reactLoop:
- Remove checkpointInterval + onCheckpoint from ReactLoopOptions (pre-v1,
  no shim).
- Add persistStep callback: fires with phase='assistant-with-tool-calls'
  after the assistant message is pushed, then with phase='tool-results'
  after all result messages for the step are pushed. If persistStep
  rejects, the error propagates out.

run.ts:
- Detect durable mode: store + sessionId + !isMemoryStore.
- When durable, provide persistStep that calls appendMessagesAtomic
  with a fresh session snapshot each write. The new user message is
  bundled into the FIRST persistStep call (so a crash before any step
  leaves no partial state — nothing persists until work begins).
- Skip the end-of-run saveSession + appendMessages pair when durable;
  flush any trailing messages (e.g. text-only final assistant) via one
  last atomic call.
- Non-durable path (stateless or no sessionId) unchanged.

Config:
- Delete checkpointInterval from AgentLoopConfig + agent() defaults +
  all tests that referenced the obsolete callback. The types.test-d.ts
  regression guard now asserts that checkpointInterval is rejected.

E2E test update:
- durable-resume.test.ts is still RED on the single "ToolDurabilityError
  surfaces in history" assertion; Phase 3 adds the resume logic that
  writes the synthetic error tool_result. Everything else in the file
  passes — handler runs once pre-crash, crash trips correctly on write
  #2, second run() completes cleanly (scripted LLM says "Done" rather
  than re-requesting the tool).

232 pass, 1 fail (the planned Phase-3-target assertion), 0 lint/type
regressions. The 8 pre-existing no-throw-plain-error warnings in
react-loop.ts / react-loop.test.ts / agent.ts are unchanged by this
commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Apr 19, 2026
Phase 3 of durable tool execution — the MVP completes here. When run()
resumes a session whose last assistant message has unmatched tool_call
ids (the crash signature for a side-effecting tool that ran but whose
tool_result was lost before write #2 committed), the framework now:

1. Constructs a ToolDurabilityError per missing tool_call.
2. Serializes each as a `tool` role message (same JSON shape the loop
   already uses for handler errors, plus a `kind: 'tool-durability-error'`
   discriminator so callers + LLMs can pattern-match).
3. Commits the synthetic tool_results atomically via
   appendMessagesAtomic before the loop's first LLM call.
4. Extends previousMessages with those synthetic rows so the LLM sees
   the error in-band and decides recovery (check external state, ask
   user, abort — its call).

Also adds `findOrphanAssistantWithToolCalls()` — a pure message-history
scan, no new schema column required. The sentinel is "assistant with
toolCalls + no matching tool_result" per the design's crash taxonomy.

Export:
- ToolDurabilityError class from @vertz/agents barrel (so callers
  inspecting resumed history can pattern-match).

Tests:
- errors.test.ts: class shape + serialized encoding.
- durable-resume.test.ts: the main E2E is now fully GREEN. Handler runs
  exactly once across crash + resume; ToolDurabilityError surfaces in
  history with the correct toolName/toolCallId.

Total: 236/236 tests pass. Typecheck clean. Lint clean on Phase 3
additions.

Phases 1–3 = MVP. Phase 4 adds the safeToRetry opt-in; Phase 5 ships
docs + changeset + PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Apr 19, 2026
Phase 2 of durable tool execution. Wires appendMessagesAtomic into the
loop so tool-calling steps commit durably in two atomic writes per step
(assistant-with-toolCalls, then tool_results), and deletes the obsolete
checkpointInterval + onCheckpoint pair.

reactLoop:
- Remove checkpointInterval + onCheckpoint from ReactLoopOptions (pre-v1,
  no shim).
- Add persistStep callback: fires with phase='assistant-with-tool-calls'
  after the assistant message is pushed, then with phase='tool-results'
  after all result messages for the step are pushed. If persistStep
  rejects, the error propagates out.

run.ts:
- Detect durable mode: store + sessionId + !isMemoryStore.
- When durable, provide persistStep that calls appendMessagesAtomic
  with a fresh session snapshot each write. The new user message is
  bundled into the FIRST persistStep call (so a crash before any step
  leaves no partial state — nothing persists until work begins).
- Skip the end-of-run saveSession + appendMessages pair when durable;
  flush any trailing messages (e.g. text-only final assistant) via one
  last atomic call.
- Non-durable path (stateless or no sessionId) unchanged.

Config:
- Delete checkpointInterval from AgentLoopConfig + agent() defaults +
  all tests that referenced the obsolete callback. The types.test-d.ts
  regression guard now asserts that checkpointInterval is rejected.

E2E test update:
- durable-resume.test.ts is still RED on the single "ToolDurabilityError
  surfaces in history" assertion; Phase 3 adds the resume logic that
  writes the synthetic error tool_result. Everything else in the file
  passes — handler runs once pre-crash, crash trips correctly on write
  #2, second run() completes cleanly (scripted LLM says "Done" rather
  than re-requesting the tool).

232 pass, 1 fail (the planned Phase-3-target assertion), 0 lint/type
regressions. The 8 pre-existing no-throw-plain-error warnings in
react-loop.ts / react-loop.test.ts / agent.ts are unchanged by this
commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Apr 19, 2026
Phase 3 of durable tool execution — the MVP completes here. When run()
resumes a session whose last assistant message has unmatched tool_call
ids (the crash signature for a side-effecting tool that ran but whose
tool_result was lost before write #2 committed), the framework now:

1. Constructs a ToolDurabilityError per missing tool_call.
2. Serializes each as a `tool` role message (same JSON shape the loop
   already uses for handler errors, plus a `kind: 'tool-durability-error'`
   discriminator so callers + LLMs can pattern-match).
3. Commits the synthetic tool_results atomically via
   appendMessagesAtomic before the loop's first LLM call.
4. Extends previousMessages with those synthetic rows so the LLM sees
   the error in-band and decides recovery (check external state, ask
   user, abort — its call).

Also adds `findOrphanAssistantWithToolCalls()` — a pure message-history
scan, no new schema column required. The sentinel is "assistant with
toolCalls + no matching tool_result" per the design's crash taxonomy.

Export:
- ToolDurabilityError class from @vertz/agents barrel (so callers
  inspecting resumed history can pattern-match).

Tests:
- errors.test.ts: class shape + serialized encoding.
- durable-resume.test.ts: the main E2E is now fully GREEN. Handler runs
  exactly once across crash + resume; ToolDurabilityError surfaces in
  history with the correct toolName/toolCallId.

Total: 236/236 tests pass. Typecheck clean. Lint clean on Phase 3
additions.

Phases 1–3 = MVP. Phase 4 adds the safeToRetry opt-in; Phase 5 ships
docs + changeset + PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Apr 19, 2026
…2841)

* docs(agents): design doc + phase plans for durable tool execution [#2835]

Rev 2 of the design for @vertz/agents durable tool execution and
transactional resume. Approved via three agent reviews (DX / Product /
Technical) + human sign-off on 2026-04-19. Implementation broken into
five phase files; Phases 1-3 are the shippable MVP that closes the P0
correctness hole for side-effecting tool calls (no double-fire on
resume after a crash between write phases).

Key design decisions locked:
- Activation is implicit: store + sessionId + non-memory store → durable.
  No flag.
- Tool opt-in named `safeToRetry` (not `idempotent`) to avoid the
  Stripe-idempotency-key semantic collision; default is side-effecting.
- Durability primitive is a single `AgentStore.appendMessagesAtomic()`
  method; two atomic writes per step (pre-dispatch / post-dispatch).
  No `toolCallStatus` field — orphan sentinel is message history alone.
- Memory store under durable execution throws `MemoryStoreNotDurableError`
  at run() entry, not lazily.
- Deletes `checkpointInterval` + `onCheckpoint` pre-v1, no shim.

Related: #2834 (Anthropic adapter — merged).

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

* feat(agents): AgentStore.appendMessagesAtomic interface + memory-store guard [#2835]

Phase 1 Task 1 of the durable resume feature. Adds the durability primitive
to the AgentStore contract so Phase 2 can rely on per-step atomic writes,
and introduces MemoryStoreNotDurableError plus an isMemoryStore() brand
so run() can fail fast when memoryStore + sessionId are combined.

- Extend AgentStore with appendMessagesAtomic(sessionId, messages, session).
  Implementations must run as one driver-level transaction over
  already-resolved data (no awaits between statements).
- memoryStore().appendMessagesAtomic() always throws
  MemoryStoreNotDurableError — memory is in-process, cannot provide the
  guarantee.
- sqlite-store + d1-store get stubbed throws; real implementations land in
  Phase 1 Tasks 2 & 3. The two no-throw-plain-error lint warnings are
  transient and disappear in the follow-up commits.
- Export MemoryStoreNotDurableError from @vertz/agents public barrel.
- Export MEMORY_STORE_KIND + isMemoryStore() as module-level helpers for
  run.ts to consume in Phase 1 Task 4.

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

* feat(agents): sqliteStore appendMessagesAtomic [#2835]

Phase 1 Task 2. Real implementation of the durability primitive for the
SQLite store. Session upsert + all message inserts run inside a single
db.transaction(() => { ... }) callable, over already-resolved data — no
await inside the transaction (the @vertz/sqlite driver is sync). If any
statement throws, the whole transaction rolls back, so readers never see
partial state.

Covered by three tests:
- happy path: session + messages visible after one call
- rollback: a circular-reference toolCalls payload fails JSON.stringify
  mid-batch; no messages land and session.updatedAt is unchanged
- monotonic seq: successive calls continue the sequence numbering

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

* feat(agents): d1Store appendMessagesAtomic [#2835]

Phase 1 Task 3. Real implementation of the durability primitive for the
Cloudflare D1 store. A single db.batch([...]) wraps the session upsert +
every message INSERT in one implicit transaction. Each INSERT derives its
seq from a subquery (COALESCE(MAX(seq), 0) + 1) so no pre-batch SELECT is
required — D1 statements in the same batch see each other's writes, which
gives the INSERTs monotonically increasing seq values.

Because D1 batch() is documented as implicitly transactional
(https://developers.cloudflare.com/d1/worker-api/prepared-statements/#batch-statements),
the whole batch commits atomically or rolls back on any statement failure.

Covered by four tests:
- happy path: session + messages visible after one call
- monotonic seq across two successive atomic appends
- rollback: a failing batch() rejects, no partial state visible
- toolCall metadata (toolCallId, toolName, toolCalls) round-trips

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

* feat(agents): run() entry check for memoryStore + sessionId [#2835]

Phase 1 Task 4. Fail fast at run() entry — before any LLM call or store
access — whenever a sessionId is paired with memoryStore(). The memory
store cannot provide the durable per-step writes that resume requires,
so silently allowing it would lose data on restart (especially for
chat-only agents that never call a tool and never exercise
appendMessagesAtomic otherwise).

- run() at the top of the hasStore branch checks isMemoryStore(store)
  when sessionId is present and throws MemoryStoreNotDurableError
  synchronously.
- Added three tests covering: tool-calling agent throws before LLM; chat-
  only agent throws (no silent loss); memoryStore without sessionId still
  works normally.
- Migrated 12 existing run.test.ts uses of memoryStore() + sessionId to
  sqliteStore({ path: ':memory:' }). Equivalent in-process behavior,
  transactional by construction.
- Same migration in create-agent-runner.test.ts (3 uses).
- types.test-d.ts left untouched — its memoryStore() call doesn't pass a
  sessionId and is a pure type check.

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

* feat(agents): @vertz/agents/testing subpath with crashAfterToolResults [#2835]

Phase 1 Task 5. Adds the test-only subpath export '@vertz/agents/testing'
and its first helper, crashAfterToolResults(store, failOnCallNumber = 2),
used by durable-resume.test.ts to simulate a crash between the pre-
dispatch write and the post-dispatch write. The helper wraps any
AgentStore, counts appendMessagesAtomic calls, and throws a sentinel
Error on the Nth call; all other methods pass through unchanged.

- src/testing/crash-harness.ts: the factory.
- src/testing/index.ts: barrel for the subpath.
- src/testing/crash-harness.test.ts: 4 tests covering pass-through
  behavior, Nth-call fail, and delegation of non-atomic methods.
- package.json exports: ./testing entry with dist/testing/index.{js,d.ts}.
- build.config.ts: add src/testing/index.ts as an entry so dts runs.

Verified: `dist/testing/index.js` and `dist/testing/index.d.ts` produced
by the build. Lint clean (the sentinel plain-Error throw has an inline
disable comment with rationale). 227/227 agent tests green.

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

* test(agents): durable-resume E2E test (TDD RED) + perf gate [#2835]

Phase 1 Task 6. Lands the feature-level E2E test that drives Phases 2
and 3 to GREEN, plus a perf regression gate for the durable-resume
write pattern.

__tests__/durable-resume.test.ts — the MVP contract. Three cases:

1. "Does not re-invoke handler" (THE key assertion). Scripted LLM asks
   for postSlack; crash harness throws on the 2nd appendMessagesAtomic
   call (simulating a crash AFTER the handler dispatched). Resume with
   the same sessionId must NOT re-invoke the handler; the stored
   message history must include a ToolDurabilityError tool_result.
   --- Currently RED: Phase 1 doesn't call appendMessagesAtomic in the
   loop so the crash never fires. Phase 2 wires the atomic writes
   (still RED because no resume logic). Phase 3 surfaces the error →
   GREEN. The feature branch carries the intermediate RED commits by
   design; the final PR to main is green. .claude/rules/tdd.md forbids
   .skip, so the test stays un-skipped.

2. "memoryStore + sessionId throws at entry" — GREEN already (Task 4
   landed that path). Doubles as an integration assertion that the
   guard fires before any LLM call.

3. Type-level @ts-expect-error on `safeToRetry: true` — GREEN until
   Phase 4 adds the field. If Phase 4's type wiring is missed, this
   directive fires "unused" and alerts.

__tests__/durable-resume.perf.test.ts — gates a 10-step scripted loop
on sqliteStore(:memory:) under 200ms. Current measurement: ~4ms. If
Phase 2's per-step atomic writes regress this badly, CI alarms.

Scope boundary: the current run() requires the session row to exist
before run() with a fixed sessionId — the test pre-seeds it via
saveSession(). This matches the DO walkthrough pattern and is not a
framework change in scope for Phase 1.

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

* feat(agents): per-step atomic writes in the ReAct loop [#2835]

Phase 2 of durable tool execution. Wires appendMessagesAtomic into the
loop so tool-calling steps commit durably in two atomic writes per step
(assistant-with-toolCalls, then tool_results), and deletes the obsolete
checkpointInterval + onCheckpoint pair.

reactLoop:
- Remove checkpointInterval + onCheckpoint from ReactLoopOptions (pre-v1,
  no shim).
- Add persistStep callback: fires with phase='assistant-with-tool-calls'
  after the assistant message is pushed, then with phase='tool-results'
  after all result messages for the step are pushed. If persistStep
  rejects, the error propagates out.

run.ts:
- Detect durable mode: store + sessionId + !isMemoryStore.
- When durable, provide persistStep that calls appendMessagesAtomic
  with a fresh session snapshot each write. The new user message is
  bundled into the FIRST persistStep call (so a crash before any step
  leaves no partial state — nothing persists until work begins).
- Skip the end-of-run saveSession + appendMessages pair when durable;
  flush any trailing messages (e.g. text-only final assistant) via one
  last atomic call.
- Non-durable path (stateless or no sessionId) unchanged.

Config:
- Delete checkpointInterval from AgentLoopConfig + agent() defaults +
  all tests that referenced the obsolete callback. The types.test-d.ts
  regression guard now asserts that checkpointInterval is rejected.

E2E test update:
- durable-resume.test.ts is still RED on the single "ToolDurabilityError
  surfaces in history" assertion; Phase 3 adds the resume logic that
  writes the synthetic error tool_result. Everything else in the file
  passes — handler runs once pre-crash, crash trips correctly on write
  #2, second run() completes cleanly (scripted LLM says "Done" rather
  than re-requesting the tool).

232 pass, 1 fail (the planned Phase-3-target assertion), 0 lint/type
regressions. The 8 pre-existing no-throw-plain-error warnings in
react-loop.ts / react-loop.test.ts / agent.ts are unchanged by this
commit.

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

* feat(agents): resume detection + ToolDurabilityError [#2835]

Phase 3 of durable tool execution — the MVP completes here. When run()
resumes a session whose last assistant message has unmatched tool_call
ids (the crash signature for a side-effecting tool that ran but whose
tool_result was lost before write #2 committed), the framework now:

1. Constructs a ToolDurabilityError per missing tool_call.
2. Serializes each as a `tool` role message (same JSON shape the loop
   already uses for handler errors, plus a `kind: 'tool-durability-error'`
   discriminator so callers + LLMs can pattern-match).
3. Commits the synthetic tool_results atomically via
   appendMessagesAtomic before the loop's first LLM call.
4. Extends previousMessages with those synthetic rows so the LLM sees
   the error in-band and decides recovery (check external state, ask
   user, abort — its call).

Also adds `findOrphanAssistantWithToolCalls()` — a pure message-history
scan, no new schema column required. The sentinel is "assistant with
toolCalls + no matching tool_result" per the design's crash taxonomy.

Export:
- ToolDurabilityError class from @vertz/agents barrel (so callers
  inspecting resumed history can pattern-match).

Tests:
- errors.test.ts: class shape + serialized encoding.
- durable-resume.test.ts: the main E2E is now fully GREEN. Handler runs
  exactly once across crash + resume; ToolDurabilityError surfaces in
  history with the correct toolName/toolCallId.

Total: 236/236 tests pass. Typecheck clean. Lint clean on Phase 3
additions.

Phases 1–3 = MVP. Phase 4 adds the safeToRetry opt-in; Phase 5 ships
docs + changeset + PR.

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

* feat(agents): tool({ safeToRetry }) opt-in for resume replay [#2835]

Phase 4 of durable tool execution. Pure-read tools or handlers that
are safe to run twice can now declare `safeToRetry: true` to opt out of
the conservative ToolDurabilityError path. On resume with a missing
tool_result, the framework re-invokes those handlers and persists the
real result instead of asking the LLM to decide recovery.

Types:
- ToolConfig.safeToRetry?: boolean — public, documented with explicit
  callout that this is about resume replay, NOT HTTP retry.
- ToolDefinition.safeToRetry?: boolean — forwarded by tool().

run.ts (resume dispatch):
- Orphan handling moves from the session-load branch to after
  ctx/agents/resolvedTools are built, so executeToolCall can run with
  a real ToolContext.
- Per missing tool_call: if resolvedTools[name]?.safeToRetry, call
  executeToolCall — which handles input validation + output validation +
  handler errors identically to the loop. Otherwise surface the
  ToolDurabilityError as before.
- Re-invocations + durability-error messages persist atomically in a
  single appendMessagesAtomic call (batch per resume, not per tool).

react-loop.ts:
- Export executeToolCall + ToolCallResult so run.ts can reuse the same
  code path (tool-not-found / no-handler / input/output validation /
  handler errors all encoded the same way).

Tests:
- tool.test.ts: safeToRetry forwards correctly (true → true, omitted →
  undefined).
- durable-resume.test.ts: new E2E scenario — a safeToRetry tool crashes
  mid-step, handler re-invokes on resume, real result lands, NO
  ToolDurabilityError appears in history. Handler count goes 1 → 2 —
  exactly the point.
- Updated the type-level test: safeToRetry: true compiles (negative
  was removed), safeToRetry: 'yes' is rejected.

240/240 tests pass. 2 pre-existing no-throw-plain-error warnings
unchanged by this commit.

Phases 1–4 deliver the full feature. Phase 5 wraps docs + changeset +
retrospective + PR.

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

* docs(agents): durable-resume guide + changeset + retrospective [#2835]

Phase 5 — the release wrap for durable tool execution.

- packages/mint-docs/guides/agents/durable-resume.mdx: a full
  user-facing guide covering activation (store + sessionId on durable
  store), the safeToRetry flag with an explicit "this is NOT network
  retry" callout, the crash-window taxonomy table in user terms, cost
  guidance (~2 writes/step on D1), and the ToolDurabilityError
  inspection pattern for resumed history.
- docs.json nav updated to include the new page.
- .changeset/agents-durable-resume.md: patch bump with the full
  public-surface diff (new + removed).
- plans/post-implementation-reviews/agents-durable-resume.md: retro
  covering what shipped, what worked, what didn't, manual-verification
  checklist for the CF DO staging run, and open follow-ups.

The manual CF DO verification is release-gating, not merge-gating —
the framework tests all pass; the retro captures the checklist to run
post-merge against triagebot staging.

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

* style(mint-docs): oxfmt durable-resume guide [#2835]

Pure oxfmt pass on the new durable-resume guide to unblock CI. No
content change.

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

* style(mint-docs): oxfmt index + installation (drive-by, pre-existing drift)

These two files are identical to origin/main but fail `oxfmt --check`,
blocking CI on this PR. Pure whitespace/wrap normalization — no
content change.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
matheuspoleza added a commit that referenced this pull request Apr 19, 2026
Applies feedback from the three adversarial reviews of the Phase 1
design (CONDITIONAL APPROVAL / CONDITIONAL APPROVAL / BUILDABLE
WITH CAVEATS). All 8 blockers addressed in-doc; no structural
redesign needed.

DX blockers:
 - selector: string → target: string | {text} | {name} | {label}
   (match existing vertz_browser_click locator union)
 - Added AUTH_REQUIRED, PAGE_HTTP_ERROR, PAGE_JS_ERROR,
   SELECTOR_AMBIGUOUS, URL_INVALID, ARTIFACT_WRITE_FAILED codes
 - Tool description rewritten: same-origin only, no session sharing
   with vertz_browser_*, never silently returns login-screen PNG
 - Added waitFor param (defaults to networkidle — catches query())

Product blockers:
 - Dogfooding criterion: owner (Matheus), target (linear-clone
   kanban), pass/fail (3 consecutive .tsx PRs include screenshot)
 - Added measurable P1-P7 success criteria with who-measures-what
 - Moved binary-size decision to Task 2 (before chromiumoxide lands
   in Cargo.toml — cheap rollback path)
 - Dropped --no-screenshot and --screenshot-pool flags
   (violated #2 "one way to do things"; per-call errors suffice)

Technical blockers:
 - BrowserSpawner / BrowserHandle trait signatures now specified
 - Pool state machine: explicit Launching state with OnceCell
   serialization for concurrent calls during cold start
 - BDD scenarios explicitly map to Rust #[tokio::test] (not vitest)
 - Path sanitization moved from Task 1 (writer) to Task 5
   (HTTP route — user input lives there)
 - .local.rs convention replaced with #[ignore] (doesn't exist on
   the Rust side)
 - .dockerignore claim corrected: scaffold doesn't write one today,
   new acceptance line in Task 6
 - Pool integrates with existing with_graceful_shutdown watch
   channel (no standalone ctrl_c handler)
 - Read-only $HOME fallback ($XDG_CACHE_HOME → $TMPDIR) for CI
 - Added AUTH_REQUIRED BDD scenarios; removed --no-screenshot
   scenario; added E2E for external URL rejection

Added non-goal: Windows support (documented; WSL workaround).

New "Changelog" section at doc tail tracks the revision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
matheuspoleza added a commit that referenced this pull request Apr 20, 2026
* chore(poc): chromium client validation for agent visual handoff [#2865]

Validates chromiumoxide v0.9.1 as the Rust Chromium/CDP client for
the headless screenshot work in Issue #2865. Throwaway crate at
native/chromium-poc/ exercises Browser::launch, browser.set_cookies
(impersonation path), viewport/fullPage/selector-crop screenshots,
warm capture, and graceful shutdown against a tiny in-process HTTP
target.

Release-build numbers (Apple Silicon, system Chrome):
  cold start:       836 ms  (target: <2000 ms)
  viewport capture:  37 ms  (target: <200 ms)
  full-page:         69 ms
  selector crop:     21 ms
  warm capture:      22 ms

Release binary size with full networking stack: 4.8 MB; estimated
delta for vtz (which already has tokio/hyper) is 3-4 MB, under the
20 MB budget from the design doc.

Cargo configuration pin: default-features = false, NO extra features.
The `rustls`/`native-tls`/`fetcher`/`zip0`/`zip8` features all
transitively enable chromiumoxide_fetcher, which we do not need
(vtz will download Chrome for Testing itself).

Full results: plans/2865-chromium-poc-results.md
Research:     plans/2865-chromium-poc-research.md

Decision: GO for Phase 1 implementation.

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

* chore(poc): isolate chromium-poc into its own workspace [#2865]

The POC's dep tree (chromiumoxide + reqwest + hyper + aws-lc-sys +
tls stack) is heavy enough that pulling it into the main native/
workspace blows up `cargo test --all` — both build time and disk
footprint — for a throwaway research crate.

Move native/chromium-poc/ out of the parent workspace:
  - Parent Cargo.toml: exclude = ["chromium-poc"]
  - POC Cargo.toml: own [workspace] + [profile.release]
  - POC has its own Cargo.lock
  - Parent Cargo.lock reverts to pre-POC state

Run with:
  cargo run --release --manifest-path native/chromium-poc/Cargo.toml

Also captures the cargo fmt pass on main.rs (signature collapse).

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

* docs(design): phase 1 design doc — headless screenshot MCP tool [#2865]

Focused design doc for Phase 1 of Issue #2865: just the headless
screenshot MCP tool, built on the POC findings in this branch. No
impersonation, no overlay, no compiler changes — all deferred until
their dependencies mature.

Addresses every blocker from the three adversarial reviews on PR
#2866 by scoping out the subsystems they flagged:
 - No @vertz/auth path reference (auth is out of Phase 1 scope)
 - No StoredSession.source field (no impersonation)
 - No compiler dev/prod mode (no data-vertz-source stamp)
 - Single unambiguous tool name and param shape
 - Measurable success criteria + real POC numbers
 - Phase 2–5 left as roadmap in the vision doc

Includes: API surface, error contract, architecture, task breakdown
(8 tasks, ≤5 files each per phase-implementation-plans rule), BDD
acceptance tests, security review, type flow map. POC Results
section cites plans/2865-chromium-poc-results.md.

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

* docs(design): address 8 review blockers on phase 1 design [#2865]

Applies feedback from the three adversarial reviews of the Phase 1
design (CONDITIONAL APPROVAL / CONDITIONAL APPROVAL / BUILDABLE
WITH CAVEATS). All 8 blockers addressed in-doc; no structural
redesign needed.

DX blockers:
 - selector: string → target: string | {text} | {name} | {label}
   (match existing vertz_browser_click locator union)
 - Added AUTH_REQUIRED, PAGE_HTTP_ERROR, PAGE_JS_ERROR,
   SELECTOR_AMBIGUOUS, URL_INVALID, ARTIFACT_WRITE_FAILED codes
 - Tool description rewritten: same-origin only, no session sharing
   with vertz_browser_*, never silently returns login-screen PNG
 - Added waitFor param (defaults to networkidle — catches query())

Product blockers:
 - Dogfooding criterion: owner (Matheus), target (linear-clone
   kanban), pass/fail (3 consecutive .tsx PRs include screenshot)
 - Added measurable P1-P7 success criteria with who-measures-what
 - Moved binary-size decision to Task 2 (before chromiumoxide lands
   in Cargo.toml — cheap rollback path)
 - Dropped --no-screenshot and --screenshot-pool flags
   (violated #2 "one way to do things"; per-call errors suffice)

Technical blockers:
 - BrowserSpawner / BrowserHandle trait signatures now specified
 - Pool state machine: explicit Launching state with OnceCell
   serialization for concurrent calls during cold start
 - BDD scenarios explicitly map to Rust #[tokio::test] (not vitest)
 - Path sanitization moved from Task 1 (writer) to Task 5
   (HTTP route — user input lives there)
 - .local.rs convention replaced with #[ignore] (doesn't exist on
   the Rust side)
 - .dockerignore claim corrected: scaffold doesn't write one today,
   new acceptance line in Task 6
 - Pool integrates with existing with_graceful_shutdown watch
   channel (no standalone ctrl_c handler)
 - Read-only $HOME fallback ($XDG_CACHE_HOME → $TMPDIR) for CI
 - Added AUTH_REQUIRED BDD scenarios; removed --no-screenshot
   scenario; added E2E for external URL rejection

Added non-goal: Windows support (documented; WSL workaround).

New "Changelog" section at doc tail tracks the revision.

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

* docs(design): v3 — fix factual errors + 3 new technical blockers [#2865]

Second review round caught a Product regression (CONDITIONAL → BLOCKED)
and 3 new Technical blockers. v3 addresses all of them:

Factual corrections (Product):
 - apps/linear-clone/ → examples/linear/ (wrong path; showcase is in
   examples/, 28 tsx files confirmed)
 - Dropped linear-clone-kanban-board as named target (already shipped
   in #1283/#1294); P5 now "active linear plan during Phase 1 window"
   with fallback to any .tsx PR work
 - P5 evaluation window decoupled from Phase 1 branch merge (Phase 1
   is pure Rust, produces zero .tsx changes) — now a 2-week post-merge
   retro, not a pre-merge gate
 - Shutdown description: "watch channel" → axum's .with_graceful_
   shutdown(shutdown_future) + shutdown_signal() (verified at
   http.rs ~line 2015/2028)
 - Removed allowAuthPage references; it was a v2 flag that leaked
   into the tool description, violating #3 (LLM-first) via
   non-existent param mention

Design fixes (DX + Technical):
 - Param `target` → `crop` (avoids semantic collision with
   vertz_browser_click.target which accepts element refs; same name,
   different semantics is the #3 LLM-first trap)
 - AUTH_REQUIRED heuristic: dropped /auth/.*/ regex (false positive on
   /auth/callback, /dashboard/auth-overview); path check is now exact
   match on /login, /signin, /signup; DOM check requires "form is
   primary element" to avoid flagging landing pages with inline
   signup widgets
 - BrowserHandle::capture(&mut self) → capture(&self) with interior
   mutability; Box<dyn BrowserHandle> → Arc<dyn BrowserHandle>. Pool
   clones the handle to spawn concurrent warm captures without
   holding a mutex (addresses "warm captures serialize" critique)
 - crate::screenshot::Error type now fully defined inline
   (thiserror-derived enum unifying all variants)
 - Task 2 stat -f%z (macOS-only) → wc -c < binary (portable)
 - Task 5 grows 4 → 5 files to include lib.rs pub use so
   native/vtz/tests/ can reach the module

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

---------

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.

2 participants