docs: initial vertz core API design plan#2
Merged
viniciusdacal merged 4 commits intomainfrom Feb 4, 2026
Merged
Conversation
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>
- 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>
4 tasks
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>
7 tasks
10 tasks
11 tasks
5 tasks
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
This was referenced Feb 17, 2026
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 ✅
This was referenced Mar 3, 2026
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>
This was referenced Apr 18, 2026
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>
This was referenced Apr 18, 2026
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>
7 tasks
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>
6 tasks
4 tasks
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>
7 tasks
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>
Closed
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Defines the functional API design for: