Skip to content

v2: mongospecs-web 0.1.0, REST resources with five plugs - #7

Merged
jaykv merged 31 commits into
v2/foundationfrom
v2/web
Oct 13, 2026
Merged

jaykv merged 31 commits into
v2/foundationfrom
v2/web

Conversation

@jaykv

@jaykv jaykv commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #6. Adds the web layer from the plan (§4.8, §4.9): a model exposed as REST operations that are pure data, mounted on any framework by a thin plug.

What it adds

  • Neutral core: Resource, Operation, Ctx, Request, Response with no framework imports. Api.handle(...) runs a request without a server.
  • Field policy drives the surface: Policy on a field decides whether it can be created, patched, read, filtered or sorted. No view classes are generated: input is filtered by policy and validated against the real model, and each view's JSON Schema is a transform of the model's own.
  • Queries from the URL: ?price[gte]=2&name[prefix]=an&sort=-price&limit=20&after=.... Only allowlisted fields and a bounded operator set get through; anything else is a 400.
  • Authorization: operation guards, plus a row scope that is ANDed into list, get, patch and delete, so ownership is enforced in the query.
  • HTTP details: ETag and If-Match from the revision field, RFC 9457 problem documents, OpenAPI 3.1.
  • Plugs: pure ASGI, Starlette, FastAPI, Litestar, Quart.
  • mongospecs_web.testing: an in-memory backend and a contract suite. A plug is correct when it reproduces the reference transcript (77 requests covering CRUD, validation, policy, filters, pagination, auth, scope, conditional updates, custom operations and error documents).

Verification

  • Every plug × model backend (5 × 3) reproduces the reference transcript exactly.
  • Database-backed tests cover what the in-memory contract cannot: plugins, unique-index conflicts, soft delete, includes, cursor pagination, row scope, and that rejected input never reaches MongoDB.
  • Generated OpenAPI validates with openapi-spec-validator for all three backends. With FastAPI, the operations and schemas appear in the host app's own /openapi.json and /docs for msgspec and attrs models as well as pydantic.
  • 464 tests in the workspace; ruff and mypy --strict clean.

Things to know when reviewing

  • Behaviour change in core: Policy(private=True) now also means "not writable" unless insert/update are set explicitly. Policy(private=True, insert=True) is a write-only field such as a password.
  • Routes a framework rejects by itself (unknown path, unsupported method) get that framework's native 404/405 body. The contract compares only the status for those.
  • Litestar handlers are excluded from Litestar's own schema generation; the API's OpenAPI document is served at <prefix>/openapi.json instead.
  • sku[exists]=true matches documents where the field is stored as null, as in MongoDB.
  • The first PR's lint failure (an unformatted code block in the package README) is fixed on its branch.

🤖 Generated with Claude Code

Summary by Sourcery

Introduce a framework-neutral REST layer for mongospecs models with policy-driven APIs, multiple web-framework plugs, and comprehensive cross-backend contract coverage.

New Features:

  • Add framework-neutral REST resources with CRUD and custom operations for mongospecs models.
  • Support policy-driven field views, validated filtering and sorting, pagination, row scopes, authorization guards, conditional requests, problem documents, and OpenAPI 3.1.
  • Provide ASGI, Starlette, FastAPI, Litestar, and Quart adapters plus reusable contract-testing utilities.

Bug Fixes:

  • Prevent private fields from being writable by default while preserving explicit write-only field support.
  • Improve query-string type coercion and required-field validation across model backends.
  • Avoid leaking unexpected internal errors through HTTP responses.

Enhancements:

  • Expose a MongoDB resource backend and in-memory backend for consistent API behavior across storage implementations.
  • Add ETag and revision handling, relation includes, computed fields, soft-delete integration, and lifecycle support.
  • Validate generated OpenAPI documents and integrate API schemas with host FastAPI applications.

Build:

  • Add openapi-spec-validator to development dependencies and update the lockfile.

Documentation:

  • Document the framework-neutral API, resource model, policies, query syntax, and framework plugs.

Tests:

  • Add a cross-framework contract suite covering five plugs and three model backends.
  • Add database-backed coverage for CRUD, validation, filtering, pagination, authorization, scopes, indexes, includes, plugins, and conditional updates.
  • Add OpenAPI, schema transformation, serialization, lifespan, body limits, and error-safety tests.

A Resource exposes a model as REST operations described as pure data;
plugs translate requests and responses for a framework and share
everything else.

- Resource, Operation, Ctx, Request, Response with no framework
  imports; custom operations, before/after hooks
- field Policy drives Create, Patch, Read and Filter views; input is
  filtered by policy and validated against the real model, and JSON
  Schema per view is a transform of the model's own schema
- allowlisted query-string filters with a bounded operator set, sort,
  cursor and offset pagination, includes
- operation guards plus a row scope that is ANDed into every query
- ETag / If-Match from the revision field, RFC 9457 problem responses,
  OpenAPI 3.1 generation
- plugs: pure ASGI, Starlette, FastAPI (schemas merged into the host
  app's OpenAPI for any model backend), Litestar, Quart
- mongospecs_web.testing: an in-memory backend and a contract suite
  whose transcript every plug must reproduce

Core changes this needed: Policy(private=True) is no longer writable
unless insert/update are set explicitly; lax casting for values that
arrive as text; msgspec missing-field errors name the field.

49 web tests; every plug x model backend matches the reference
transcript. 464 tests in the workspace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Sorry @jaykv, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 6 days and 13 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: f0b9d996-16ad-4620-9ced-964ab79e4cb2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds mongospecs-web 0.1.0 as a framework-neutral, policy-driven REST layer with MongoDB and in-memory backends, five framework plugs, authorization and conditional HTTP semantics, OpenAPI 3.1 generation, and a cross-plug contract test suite.

Sequence diagram for policy-driven REST request handling

sequenceDiagram
    participant Client
    participant Plug
    participant Api
    participant Resource
    participant Backend

    Client->>Plug: HTTP request
    Plug->>Api: handle(method, path, query, headers, body)
    Api->>Api: match()
    Api->>Api: authenticate(request)
    Api->>Resource: dispatch(operation, request)
    Resource->>Resource: guard(ctx)
    Resource->>Resource: parse_list_query() / check_input()
    Resource->>Resource: scope()
    Resource->>Backend: list() / get() / create() / patch() / delete()
    Backend-->>Resource: document or page
    Resource-->>Plug: Response
    Plug-->>Client: HTTP response
Loading

File-Level Changes

Change Details Files
Introduces a framework-neutral REST resource execution layer driven by model policies and operation descriptors.
  • Adds pure-data request, response, context, resource, operation, guard, hook, and dispatch abstractions.
  • Implements CRUD and custom operations with validation, serialization, ETags, conditional requests, scopes, authorization, and problem responses.
  • Exports the public API and HTTP error types for package consumers.
packages/mongospecs-web/src/mongospecs_web/__init__.py
packages/mongospecs-web/src/mongospecs_web/resource.py
packages/mongospecs-web/src/mongospecs_web/errors.py
Adds policy-based model views and safe query/backend handling.
  • Derives readable, insertable, updatable, filterable, sortable, relation, revision, and JSON Schema views from model metadata.
  • Parses allowlisted filters, operators, sorting, pagination, totals, and includes while casting URL values.
  • Defines the backend protocol and MongoDB implementation with row scopes, includes, pagination, revision checks, and database error mapping.
  • Updates Policy semantics for private fields and adds lax casting needed for query strings.
packages/mongospecs-web/src/mongospecs_web/policy.py
packages/mongospecs-web/src/mongospecs_web/filters.py
packages/mongospecs-web/src/mongospecs_web/backend.py
packages/mongospecs/src/mongospecs/markers.py
packages/mongospecs/src/mongospecs/meta.py
packages/mongospecs/src/mongospecs/msgspec.py
Provides routing, framework adapters, lifecycle integration, and OpenAPI generation.
  • Adds API prefix routing, route matching, authentication, body limits, direct in-process handling, and OpenAPI serving.
  • Implements pure ASGI, Starlette, FastAPI, Litestar, and Quart plugs that translate framework requests to the neutral core.
  • Generates OpenAPI 3.1 schemas and operations from policy-transformed model schemas, including FastAPI host-app integration.
packages/mongospecs-web/src/mongospecs_web/api.py
packages/mongospecs-web/src/mongospecs_web/openapi.py
packages/mongospecs-web/src/mongospecs_web/plugs/asgi.py
packages/mongospecs-web/src/mongospecs_web/plugs/starlette.py
packages/mongospecs-web/src/mongospecs_web/plugs/fastapi.py
packages/mongospecs-web/src/mongospecs_web/plugs/litestar.py
packages/mongospecs-web/src/mongospecs_web/plugs/quart.py
Adds reusable contract-testing infrastructure and broad verification across adapters and model backends.
  • Adds an in-memory backend, contract API, fixed reference transcript, and plug comparison suite.
  • Tests CRUD, validation, policy enforcement, filtering, pagination, authorization, row scope, custom operations, conditional updates, error documents, includes, soft delete, indexes, and database behavior.
  • Validates generated OpenAPI for msgspec, pydantic, and attrs models and adjusts development dependencies and an existing Sentry test.
packages/mongospecs-web/src/mongospecs_web/testing.py
packages/mongospecs-web/tests/conftest.py
packages/mongospecs-web/tests/test_contract.py
packages/mongospecs-web/tests/test_mongo_resource.py
packages/mongospecs-web/tests/test_openapi_views.py
packages/mongospecs/tests/test_registry_errors.py
pyproject.toml
uv.lock

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

jaykv and others added 4 commits October 13, 2026 00:01
An unpublished workspace package answering performance questions with
data from real MongoDB (replica set and sharded cluster).

- mongospecs-bench run: workloads B1-B8 (materialise, find, include
  strategies, writes, pagination, plugin overhead, HTTP per plug,
  cost of change tracking)
- mongospecs-bench stress: scenarios X1-X7 (pool pressure, transaction
  contention, optimistic-lock storm, cancellation, 16MB results,
  change stream across a step-down, threads with own loops)
- mongospecs-bench trace: Sentry SDK through mongospecs-web into a
  local rustrak server, asserting one trace per request with
  Model.op database spans
- results are JSON under bench/results/<git sha>/ with server,
  topology and machine metadata; --quick only proves the harness runs
- smoke tests keep every workload and scenario runnable

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Changes driven by the first full benchmark run and by booting a real
application.

Performance
- tracked reads keep the raw BSON the driver received as the snapshot
  and build the comparable form only when a document is saved, instead
  of re-encoding every loaded document
- cursors are drained with to_list() batches, not one await per document
- remove the 'trusted' read flag: the pydantic fast path it enabled was
  slower than validating, and it did nothing on msgspec and attrs

Fixes found by the end-to-end tests
- aggregate, union, graph and backref-count paths sent max_time_ms in
  find() spelling and failed whenever a default time limit was set
- web: unexpected errors are now logged server-side (the client still
  gets a bare 500)
- ids given as strings to reference fields are coerced to the target's
  id type on backends that do not validate on construction
- stored documents are read leniently on strict backends (an integer
  stored as a double), input stays strict
- a document stored before a Revision field existed can be saved

Reference application and tests
- bench: 'storefront', a production-shaped app (auth, admin guards,
  row scope, transactional orders that cannot oversell, soft delete,
  ETags, reviews as a reverse relation, a resumable exactly-once
  change-stream projector, health check, graceful shutdown)
- end-to-end tests boot it under uvicorn and drive it over HTTP on
  every plug and model backend: customer journey, concurrent buyers,
  all-or-nothing multi-item orders, projector resume across restarts,
  in-flight requests on shutdown, consistency across a primary
  step-down
- HTTP load benchmark against the running server with a hand-written
  pymongo + Starlette baseline of the same endpoints
- Resource(server_fields=...) for values the server sets on create
- guide pages whose examples are executed by the test suite, migration
  guide, Mongoose parity page, Mongoose benchmark comparator

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nerator

- Codec.to_json: msgspec.to_builtins / pydantic model_dump(mode='json')
  produce the Read view directly instead of encoding to BSON types and
  converting those; responses are dumped with msgspec's JSON encoder
  when available
- list and get requests read lean documents: nothing is saved, so
  nothing is tracked
- upserts give the inserted document the model's defaults, so it has
  the same shape as one created by insert()
- load benchmark: the httpx-based generator capped out below the
  server's capacity, so its throughput numbers measured the client.
  Replaced with a minimal keep-alive client; the primary metric is now
  server CPU time per request, with latency taken on an unloaded server
  and throughput at saturation
- CI job for the Sentry -> rustrak tracing check; plan amendments
  A14-A21 and nodes E4-E6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- results for all workloads, the HTTP load test, the stress scenarios
  and the Mongoose comparator at 6dc837f (earlier B1-B6 results kept
  as the before picture for the tracking optimisation)
- docs/benchmarks.md: what was measured, how to read it, and what
  follows from it
- decisions recorded in the plan: include() defaults to batched
  queries; no Rust in 0.2, with a BSON-to-model decoder for bulk reads
  as the one candidate; tracked stays the default read mode
- drop workload B7 in favour of LOAD: measuring plugs through an
  in-process client measured the client
- ignore stale class copies in subclass discovery (found by the
  free-threaded CI job) with a regression test
- CI builds the docs in strict mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jaykv and others added 21 commits October 13, 2026 01:06
A fixture file written from a user's point of view; mypy --strict must
reject exactly the lines marked expect-error (wrong model type, a
possibly-None result used as a document, a count used as a document,
a reference's target type) and accept the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A timestamp with a zero fractional second is printed without one by
some serialisers, which made a lexical comparison fail intermittently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A timestamp with a zero fractional second is printed without one by
some serialisers, which made a lexical comparison fail intermittently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 279bd6c)
Each has a regression test that failed before the fix.

- cursor pagination with select() repeated documents and did not
  terminate: sort keys were left out of the projection, so the next
  cursor was built from defaults. Sort keys are now always fetched
- bulk_save left memory out of sync after a partial failure. Writes in
  the batch are independent; the ones that succeeded are now brought in
  sync and the error lists the rest in .failed. Which updates applied
  is determined by reading the documents back, since the server only
  reports a total
- bulk_write hard deletes skipped OnDelete cascade rules
- a cycle of cascading references recursed forever; cascades now carry
  the set of documents already being deleted
- cascades resolved ids with distinct() (16MB limit) and one unbounded
  $in; ids are now read with find and applied in chunks
- saving an untracked instance that has an id (a copy, a deepcopy, a
  lean read) raised a bare duplicate-key error; it now says the
  instance is not tracked and what to do
- reload() loaded Deferred fields
- Query.update() returns an int that also carries .matched
- the Location header of a create was a bare id; it is now a URL under
  the API prefix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review of the web layer.

- request bodies are read under the size limit on every plug (declared
  length checked first, then chunk by chunk); previously only the pure
  ASGI plug stopped at the limit and the others buffered the whole body
- DELETE applies the row scope in the delete's own filter, not only in
  the lookup before it, and honours If-Match
- deleting a loaded document checks its revision on hard deletes too
- offset pagination refuses pages deeper than Pagination.max_offset
  (10,000 skipped documents by default)
- totals are opt-in per resource: Pagination.total now defaults to False
- reads made for a request carry a 10s time limit when neither the
  registry nor the model sets one
- Api(openapi=False) keeps the API description private on every plug
- HEAD and OPTIONS are answered by Api.handle and the pure ASGI plug
- Resource(explicit_writes=True): a field is writable only when its
  Policy says so
- mongospecs_web.testing.mount_on(plug, api) builds an app on any
  built-in plug

BREAKING: ?total=true needs page=Cursor(total=True) / Offset(total=True).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- parity page: 28 of 35 Mongoose behaviours now confirmed against its
  documentation; three more deliberate differences recorded
- plan amendments A25-A29: the computed decorator, Atlas Search and
  vector indexes declared out of scope for 0.2 (not testable here),
  revision-checked hard deletes, the changed web defaults
- guides: limits and defaults of the web layer, untracked documents,
  partial bulk_save failures, registries and explicit managers
- CI: MongoDB 7.0 on Python 3.11, 3.13 and 3.15
- publish workflow runs lint, types and the test suite first

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each has a regression test that failed before the change
(tests/test_review_round2.py in both packages).

core
- a field whose default is None is not stored while it is None, and
  setting it to None unsets it; a unique sparse index no longer
  collides on stored nulls
- field names that shadow the Document API raise ConfigurationError
- registry.transaction() inside a running transaction raises unless
  independent=True (it would commit on its own)
- UpdateCount formats as its int value in str() and f-strings
- filter values are cast by one rule on every backend
- empty Or()/Nor(), non-text prefix()/contains(), sort keys that are
  not field paths and server type mismatches raise QueryError
- include() checks its path and strategy when the query is built
- find().on_invalid('raise' | 'skip' | 'raw')

web
- integers beyond 64 bits are a 422 on create and patch, and an
  EncodingError is no longer a 500
- Quart: init_app installs BodyLimit, which reads API bodies under the
  limit before Quart queues them
- a guard runs once per request however many places list it
- OpenAPI publishes only referenced definitions and no private field
- HEAD on the ASGI plug reports the length of the GET body

BREAKING: optional fields that are None are omitted from stored
documents, so an exists filter now means 'has a value'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Private fields of embedded (non-document) models were serialised,
writable and described in OpenAPI. They are now handled like private
fields of the document itself, at any depth and inside lists, dicts,
optionals and unions: never in a response (including included
documents), a 422 'unknown field' when a client supplies one on create
or patch, and absent from every published schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Leaving None-valued optional fields out of stored documents stays the
default. StoreNone on a field, or none='store' on a model, stores an
explicit null instead (OmitNone opts a field back out). Encoding,
change tracking, update(), upsert defaults and the bulk paths follow
the field's mode; loading never produces a spurious change in either
mode. A unique sparse index over a field that stores null is refused
with a pointer to a partial index.

Also applies the omit rule to embedded pydantic models, which were
encoded with their None values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- web GET list/get read stored documents with a projection of the
  readable fields and serialise them directly when that is certain to
  equal the model path; anything else falls back to the model
- DELETE is one conditional write unless the model needs the document
  (delete hooks, cascades)
- independent includes are loaded concurrently (not inside a session)
- per_parent_limit on a reverse relation is applied on the server
- tracked documents use one weak reference instead of weakref.finalize
- Query._clone copies slots instead of re-running __init__
- routes are looked up by resource before patterns are tried

fix(core): cursor pagination no longer skips or stops at documents
without a value for a sort key

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Requests beyond the limit are answered at once with 503 and
Retry-After instead of queueing for database connections.

test: correctness of the optimised paths (tracking, clone, includes,
per-parent limits, routing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om the stored document

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs/guide/production.md: pool and timeout settings, backpressure,
  read and pagination choices, relations, writes, what needs a replica
  set; each item marked measured, tested or guidance
- docs/benchmarks.md: hot paths before and after the review
- bench/results: B9 before (49a32a9) and after (fb543de)
- plan: decision per review finding, amendment A36

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An ObjectId, Binary, Timestamp, Regex or Int64 inside a field typed
Any (or dict[str, Any], list[Any]) made the JSON conversion fail, and
the web layer answered 500: on pydantic for any ObjectId, on msgspec
and attrs for the BSON types their serialiser does not know. All three
backends now give the same JSON for such values, at any depth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PATCH that gave an object for an embedded model replaced the stored
object, writing defaults over everything the client did not send,
including private, read-only and immutable values it is not allowed
to set. Objects are now merged key by key (RFC 7396) in one atomic
update; lists and dicts are still replaced whole.

- nested keys are held to the embedded model's own Policy
- an embedded object holding protected values cannot be set to null,
  and a list or dict of such objects cannot be replaced through PATCH
- Policy(insert=False) is enforced on nested keys at create
- the OpenAPI Patch schema describes embedded objects as partial

BREAKING: clients that relied on PATCH replacing an embedded object
must now send null for the keys they want cleared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PATCH merging into embedded objects (A37) had no way out for resources
that need the embedded model validated whole, need to write over a
stored null, or use a backend without supports_merge.

- Resource(merge_patch=False): PATCH replaces an embedded object with
  the one sent. Values the client cannot send go back to their
  defaults; nested keys the policy closes are still rejected.
- Put: PUT /{id} replaces every field the client may update (the sent
  value, or the default when left out) and leaves the rest as stored,
  in one atomic update with the If-Match check. An embedded object
  holding protected values is replaced key by key; one without is
  replaced whole. It never creates, and is not in the default ops.
- OpenAPI gains <Model>Put schemas where a resource has the operation.

Recorded as A39 in the plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each was reproduced against the replica set before it was fixed; the
tests are in test_review_round3.py of both packages.

Core
- A single-field update on a pydantic model failed a model-level
  validator that only saw the other fields' defaults. The field's own
  validators still run; the cross-field verdict is no longer taken.
- insert_many and bulk_save run document hooks, as insert and save do.
- page() starts its count after its arguments are validated, so a bad
  cursor no longer leaves an un-awaited coroutine.

Web
- Request bodies are strict JSON: NaN, Infinity and over-deep nesting
  are a 400 (NaN was stored; deep nesting was a logged 500).
- If-Match on a model without a Revision is a 412, not an
  unconditional write. If-None-Match accepts lists, weak tags and *.
- The ASGI plug drops a request whose client disconnected mid-body.
- Api(openapi=False) is honoured by the FastAPI plug.
- Guards and hooks must name an operation the resource has.
- The concurrency limit is applied before authentication.
- A filter or sort key given twice is a 400.

Recorded as A40 in the plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Branch pushes and their pull requests both triggered CI, so every job
ran twice. mongospecs[all] now includes the otel and sentry extras, and
mongospecs-web declares msgspec as the optional encoder it already uses
when present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new user had reference-quality guides and no way into them.

- examples/: a quickstart script, a to-do REST API with its tests, and
  a compose file for a single-node replica set. They are run by the
  suite, and the tutorial page and the README show them as they are.
- docs/tutorial.md: from MongoDB to a model, an API and its tests.
- New guides: choosing a backend, errors (what each means and the HTTP
  status it becomes), authentication and authorization, testing an API.
  The last two are executed like the other guide pages.
- The replica-set requirement is stated in the README, the landing page
  and the tutorial instead of only in the production notes.
- The package README is checked to be the root README.
- The reference section is removed: it was generated from docstrings,
  and the source is the better reference. mkdocstrings goes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jaykv and others added 5 commits October 13, 2026 10:33
- CHANGELOG.md, CONTRIBUTING.md and SECURITY.md.
- A workflow deploys the documentation to GitHub Pages from main.
- The publish workflow refuses a release tag that does not match the
  version in the package it names.
- Package metadata links to the documentation and the changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
msgspec and attrs do not check types when a model is constructed or
assigned to, and pydantic does not check assignments. A wrongly typed
value was therefore stored as it was, and the document could not be
loaded afterwards.

insert, save, insert_many and bulk_save now decode the document's own
encoding with the backend's strict rules before sending anything, and
raise ValidationError if it does not fit. save checks only when there
is a change, and only the changed fields of a document loaded with a
projection.

The check can be skipped with validate=False on a call or
SpecConfig(validate_writes=False) on a model. It cost about 3 µs
(msgspec) and 5 µs (pydantic) per seven-field document when measured.

Recorded as A41 in the plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the site config to zensical.toml on the new default theme, swap the
docs dependency, and deploy through the GitHub Pages actions since
zensical has no gh-deploy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
v2: fixes and hardening from review of #6–#8
v2: evidence track — benchmarks, stress, end-to-end storefront, docs
@jaykv
jaykv merged commit 011f3e5 into v2/foundation Oct 13, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant