simfinity.js / rules
Cursor rule11 starsChanged 17 days agoApache-2.0View on GitHub
Simfinity.js project architecture overview -- always loaded for context
We split this simfinity-architecture.mdc into the rules it gives an agent
2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 115
Check cancellation again after awaiting an in-process context factory, before calling GraphQL.
- Distinctive: line 147
Do not describe module-scope state as instance-isolated.
≈ “Common” is an estimate: at least 1 in 5 of the 6,350 Cursor rule files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
---
description: Simfinity.js project architecture overview -- always loaded for context
alwaysApply: true
---
# Simfinity.js Architecture
Simfinity generates GraphQL schemas, CRUD queries and mutations from `GraphQLObjectType` definitions. MongoDB and PostgreSQL adapters share the GraphQL runtime and provide their own storage implementations. Backend selection is fixed for each runtime instance.
## Tech Stack
- **Language**: JavaScript (ES modules, `"type": "module"`)
- **Runtime**: Node.js >= 18.18.0 for consumers; Node.js 24 for development/CI
- **GraphQL**: `graphql` ^16 (peer dependency)
- **Databases**: MongoDB via `mongoose` ^8; PostgreSQL >=15 via `pg` ^8
- **Testing**: Vitest
- **Linting**: ESLint 9 (flat config)
- **Server integration**: GraphQL Yoga with Envelop plugins
## Workspace boundaries
The root `simfinity-workspace` package is private and contains development commands only. Every publishable library has its own `package.json`, `src/`, `types/`, `README.md` and `LICENSE` under `packages/`. Shared tests remain at the root. The independent VitePress project in `docs/` reads the root version without installing a runtime package.
`examples/barber/mongodb`, `examples/barber/postgres`, and `examples/barber/frontend` are private applications with independent npm manifests and lockfiles outside the library workspaces. The backends consume released Simfinity runtime packages at exactly 3.7.0; no file dependencies, vendor archives, or npm publication are involved. The two Compose projects select separate backends at startup and share frontend source, while native Mongoose and PostgreSQL code remains in its owning backend. See `simfinity-barber-examples.mdc` for their operational and test boundaries.
All five package versions, exact internal dependencies and the private root version stay aligned through `scripts/release-packages.js`. Publication order is core, SQL, MCP, PostgreSQL, MongoDB. Pushing a `v*` release tag or publishing a GitHub release runs `release.yml`, which validates that the existing tag matches aligned versions and source merged into `master`, validates packages and real databases, calls reusable `publish.yml`, verifies npm integrity/visibility, creates or updates the GitHub release and deploys stable documentation. Version changes and merges do not publish; manual dispatch is preview-only. Restrict the `npm-release` environment to `v*` tags and allow Pages from release tags plus `master` for documentation-only deployments. npm uses OIDC trusted publishing for caller `release.yml` and environment `npm-release`; keep local credentials and `NPMJS_TOKEN` out of this flow. Preserve superseded-run guards before registry/latest/Pages mutations and retry-safe artifacts; freshness checks read npm uncached and fail closed when a release package returns 404. `publish.yml` packs in a first `pack` job before any install or test; its manifest SHA-256 output is enforced by publish-npm, publish-gpr and github-release, while build only uses it to test the same archives. Release source validation lives in `release-state.js` (`selectRelease`, `verify-source`); workflows only check `GITHUB_REF`. Keep the Pages forward-only gate (`verify-docs`) in a job without the `github-pages` environment and with `deployments: read`; never let the environment job succeed without deploying. GitHub release creation uses `github-release.js find`, never `gh release view`, and stable releases clear pre-release in a call before `--latest`. Pin every workflow action to a full commit SHA with a version comment, and keep Node.js and npm exact in the pack and publish jobs. Keep local archives outside the repository; `docs/public/` contains intentional downloadable artifacts.
- `packages/core` (`@simtlix/simfinity-core`) owns driver-free `createRuntime`, model metadata, query plans, scalar factory, errors, filter types and the once-only introspection patch. Registries/middleware are per runtime instance. Its public subpaths `./auth`, `./auth/errors`, `./auth/expressions`, `./auth/rules`, `./plugins`, `./scalars` and `./validators` are typed (`types/auth/*.d.ts`, `types/plugins.d.ts`, `types/scalars.d.ts`, `types/validators.d.ts`, each deriving its members from `types/index.d.ts` via `typeof`; `typesVersions` for node10); `./internal/*` stays undeclared. The root also exports `UnauthenticatedError` and `ForbiddenError`, the same classes as the `auth` ones, declared with an explicit `export class`; `types/auth/errors.d.ts` re-exports those root classes.
- `packages/mongodb` publishes as `@simtlix/simfinity-js`, preserving the public name, entry points and existing `src/` deep imports. Its `src/index.js` binds core to the MongoDB adapter. Model generation, pipelines and transactions live under `packages/mongodb/src/mongo/`; helper shims preserve core identities. The `src/` shims of public API have sibling `.d.ts` files: the core-subpath shims' siblings (`src/auth/{index,errors,expressions,rules}`, `src/{plugins,scalars,validators}`) are the shim text without the `register-mongo-object-id` import; `src/index`, `src/mcp`, `src/const/*` and `src/errors/*` are root-derived one-liners. `src/mongo/*` and `register-mongo-object-id.js` stay undeclared.
- Mongo adapters support fixed `referentialIntegrity: 'off' | 'transactional'`, default off. Strict adapters bind to one runtime, require awaited `adapter.initialize()` after schema creation/connection, and audit existing declared references. `mongo/integrity.js` uses core model descriptions, reserved target-document ObjectId lock writes, snapshot/majority transactions and incoming native queries. Save/update checks read the persisted post-image's `_id` and reference paths (the `projections` the startup audit shares) and lock targets with one write per target collection, in chunks of `AUDIT_BATCH_SIZE` (1,000): a chunk of several ids is first read with `_id: { $in }` in the transaction, so a missing target is a violation before that chunk's `updateMany`, never a retried conflict within the chunk (a conflict on a target of an earlier collection or chunk still comes first); a single id keeps one `updateOne`. Deletion locks the target before checking incoming references, with one query per referencing collection (`incoming` maps target to source to paths; several paths are an `$or`, and each self-reference branch keeps its own `_id: { $ne }`). Supplied transactions abort on integrity violations or ambiguous update results. Raw model/driver calls and middleware escaping the session bypass the guarantee. See `docs/guide/mongodb-integrity.md`.
- Mongo `batchedCollections` (default false) and `collectionBatchSize` (1-100, default 5) are read once in `createMongoAdapter` (`INVALID_MONGO_ADAPTER_OPTIONS`; `integrity.js` accepts the two keys and still rejects unknown ones). Only when true does the adapter define `findChildrenBatch` and `batchesChildren`, sharing `declineReason()`: `this.findChildren` not the adapter's own, `batchesAggregate()` false (aggregate hooks, an own `aggregate` static, a discriminator child, an own `collection.aggregate`), or a session or `transactionAsyncLocalStorage` store (`$unionWith` aborts transactions). `findChildrenBatch` builds the pipeline once with `buildQuery`, casts each parent with `castPathValue` (`NOT_VALID_ID` gives an empty list and no sub-pipeline), wraps rows with `$replaceWith: { i, d: '$$ROOT' }`, sends commands of `collectionBatchSize` sub-pipelines (the first as the pipeline, the rest as `$unionWith`, fewer under an 8 MiB budget) concurrently with `batchSize` = limit × parents + 1, and fails as a whole after `Promise.allSettled`. Declines throw before any command.
- `packages/mcp` (`@simtlix/simfinity-mcp`) owns database-independent tool generation and transports. Its SDK peer is optional. MongoDB retains compatibility re-exports; core and PostgreSQL do not depend on MCP.
- `packages/sql` (`@simtlix/simfinity-sql`) owns `createSQL`, the pure relational planner, records and session orchestration. It imports core/GraphQL only. The version-1 plugin contract binds one plugin/configuration per instance, rejects missing guarantees before physical compilation/I/O, and never silently drops FKs or constraints. See `docs/guide/sql-plugins.md`.
- `packages/postgres` (`@simtlix/simfinity-postgres`) exposes `postgresPlugin(options)`, `createPostgres({pool,schema})`, the default configure/connect/createSchema facade and schema generation/initialization. It depends on SQL, core and `pg`, never Mongoose/MongoDB/MCP. Backend configuration is bound once and PostgreSQL types must be registered before createSchema.
- PostgreSQL 3.3.0 lowering preserves the 3.2.0 physical description/DDL, protected by golden fixtures, except that `planRelationalSchema` no longer indexes `[ID]` list columns (3.5.6). The default options keep that output; the opt-in `refreshTriggers: 'perRoot'` and `ownerIndexes: 'minimal'` change only `packages/postgres/src/schema/describe.js` output (never `plan.js`), validated there (`INVALID_DATABASE_SCHEMA`), and have their own golden, `tests/fixtures/postgres-opt-in-schema.json`, which also covers a maintained root with hashed long names. Changes to generated DDL stay opt-in in 3.x; initialization keeps such legacy indexes and warns once per call with exact-name `DROP INDEX CONCURRENTLY` statements. Plugin capabilities cover transactions, FKs/deferral, embedded/owned records, scalar lists and NULL-equal uniqueness, plus the optional `singleStatementReads`, which planning never requires and only PostgreSQL advertises. Only PostgreSQL is a supported SQL plugin.
- PostgreSQL executes the shared GraphQL lifecycle: scopes, middleware, validators, hooks, nested CRUD and state transitions. Native model/session APIs differ from Mongoose. Supported parity is tested against real databases; do not claim arbitrary Mongo pipeline/model compatibility.
- GraphQL metadata generates real FKs, including inverse collections, link entities and references in privately owned embedded tables. JSONB is used only for value-only embedded subtrees. See `docs/postgresql.md` for rejected shapes and current constraints.
- Generated PostgreSQL functions/triggers are explicit serializable metadata. Initializers verify their exact identities, bodies/configuration, trigger events and `WHEN` conditions, and enabled/deferrable enforcement, and never replace drift. Create mode reads the catalog in a fixed number of batched statements without table locks and plans first; with nothing missing it takes no table lock. Otherwise it locks, in one `LOCK TABLE … IN SHARE ROW EXCLUSIVE MODE`, only the existing tables of roots whose maintenance objects are missing (`staleRoots`), lets other DDL take its own locks, and rebuilds only those roots' keys; it does not re-scan checks. Private guard rows serialize owned mutations; typed key tables enforce owner-aware multikey uniqueness. Deferred triggers validate owned shape and refresh keys, per changed row by default (`perRow`) or, with `refreshTriggers: 'perRoot'`, once per root and transaction through a guard-table constraint trigger `AFTER INSERT OR UPDATE … WHEN (new.version < 0)` while it stays deferred (set `IMMEDIATE`, it fires inside each touch trigger's guard upsert, so once per changed row); initialization keeps each root's catalog layout (`resolveLayouts`), and objects of both layouts in one root are `SCHEMA_MISMATCH`. Immutable validators enforce value-only JSONB shapes.
- Embedded queries use JSONB projections for ragged lists and catalog-validated immutable ordering helpers: MIN/MAX compares whole arrays; sorting selects immediate extrema. Owned scalar/reference/JSONB and root JSONB presenceColumn markers distinguish SQL NULL absence from explicit null. Normalize descendant list defaults/minimized inline objects before encoding and deriving unique keys; preserve required constraints. Date parameters use per-query UTC encoding, never global pg defaults.
- Initialization borrows one pool client, owns its transaction, awaits DDL/catalog validation and never destructively synchronizes existing storage. `validate` mode is read-only. PostgreSQL >=15 is required.
- Await initialization before serving. Requests rejected before the plugin keep readiness: `DATABASE_CREATION_DISABLED` (409) is checked on the requested mode (any truthy mode except `validate` on a validation-only runtime, where a falsy mode validates), and `INVALID_INITIALIZATION_MODE` (400) on the effective mode (anything but `create`, `validate` or undefined, so it fires only when creation is allowed); a ready runtime keeps serving while it re-initializes; the most recently started initialization decides readiness and any failure of it marks the instance unavailable. `preventCreatingCollection(true)` before createSchema forces validation only. Text columns and typed query expressions use binary C collation.
- Enum query filters resolve names before strict internal values, then encode the selected value for storage; query normalization must never be applied to writes or state guards. LIKE is string-only.
- PostgreSQL writes use repeatable-read transactions; supplied sessions are joined, owned sessions retry confirmed serialization/deadlock aborts after a full-jitter backoff that starts once the client is released. Per-attempt typed input copies isolate attempts. Sessionless reads that one statement answers (no owned tables, no lock) run without a transaction through `singleStatementReads`. Owned rows are inserted with multi-row statements (`multiRowInserts: false` opts out), and sorts on NOT NULL root columns omit the NULLS clause (`explicitNullsOrder: true` restores it). SQL adapters define `findChildrenBatch` and `batchesChildren` in `prepare()` only when `configured?.batchedCollections === true` (opt-in, default false; any other value keeps 3.6.0's statements) and the bound plugin has a `compileChildrenQuery` paired with its `compileQuery` (`bindPlugin` binds it only when its `compileQuery` property is the bound `compileQuery`); `batchesChildren` is `this?.findChildren === findChildren`. PostgreSQL compiles, per 100 parents (`CHILDREN_BATCH_SIZE`), a `UNION ALL` of the per-parent `compileQuery` statement (`compileSelect(…, batched)` with a NUL placeholder that must occur once) with one `$n::uuid` parameter per parent and the other parameters shared, so PostgreSQL plans each unnamed statement's branches for their own parent IDs; each branch is tagged with its position and `row_number() OVER ()` (`freeColumn` names), and an outer `ORDER BY` on both keeps per-parent order. The statement carries `setup: [{ text: 'SET LOCAL jit = off', values: [] }]` (`CHILDREN_SETUP`, a fresh list per call), because a plan of up to 100 branches can cross `jit_above_cost` when no per-parent plan does. The adapter validates `setup` with `setupOf()` before taking a connection (a plain `TypeError` for anything but a list of `{ text }`; `null`/`undefined` is none), runs the chunks one after another, each right after its setup, and hydrates all children once; when any chunk has setup, the whole batch runs in `transactions.withTransaction(null, body)`, otherwise it is routed like `findChildren` (`read()`). `compileQuery` output is unchanged. Adapter stateValue supplies the canonical persisted representation for state writes/guards (names on Mongo, enum internal values on PostgreSQL).
- Standalone saveObject owns a full workflow transaction including hooks and independent nested entities; active supplied sessions remain caller-owned. Owner+embedded reads use one snapshot. Only ownership FKs cascade.
## Core Flow
```
Define GraphQLObjectType
|
v
simfinity.connect(null, Type, 'singular', 'plural', controller, onModelCreated, stateMachine)
-- or --
simfinity.addNoEndpointType(Type) // supporting types without endpoints
|
v
simfinity.createSchema()
|
+-- assertCollectionConnections() // INVALID_MODEL for collections without connectionField
+-- storedIdentity flags // entities: connect() types and relation targets
+-- adapter.prepare() // prepare storage metadata
+-- adapter.createModel() // Mongoose models or PostgreSQL handles
+-- installIdResolver() // entity `id` -> `_id ?? id`
+-- autoGenerateResolvers() // wire relationship resolvers
+-- installEmbeddedValueResolvers() // only when adapter.readEmbeddedValue exists; readEmbeddedField also nulls data-less objects that miss a required member (readEmbeddedObject, which reads stored members through adapter.rawEmbeddedValue, never getters; MongoDB returns hydrated values whose schema declares getters, virtuals, aliases or subdocument methods inside them unchanged, so they are kept)
+-- buildRootQuery() // list, single-entity, aggregate queries
+-- buildMutation() // resolve pending inputs and build mutations
|
v
GraphQLSchema
|
+-- PostgreSQL: await initializeDatabase({ mode: 'create' | 'validate' })
|
v
Serve with Yoga/Apollo after database startup
```
## Module Map
| File | Responsibility |
|------|---------------|
| `packages/mongodb/src/index.js` | MongoDB facade and legacy helper exports |
| `packages/core/src/runtime.js` | Shared registration, input/schema generation, lifecycle, middleware, scopes and resolvers |
| `packages/core/src/relation-owners.js` | Internal `relationFieldOwners` `WeakMap` shared by `runtime.js` and the auth plugin; not exported |
| `packages/core/src/relation-storage.js` | Internal connection-storage helpers (`getListShape`, `getFieldStorageName`, `normalizeConnectionField`) shared by `runtime.js` and the MongoDB adapter, models and query builder. Exported only as the `@simtlix/simfinity-core/internal/relation-storage` subpath, which is not public API and has no type declarations; Mongo code imports it by package name, never by a relative path |
| `packages/core/src/query-plan.js` | Driver-free typed query paths and predicate/sort/aggregation plans |
| `packages/core/src/query-limits.js`, `packages/core/src/mutation-limits.js` | Process-wide `configureQueryLimits` (page sizes) and `configureMutationLimits` (`maxNestedOperations`) settings |
| `packages/mongodb/src/mongo/` | Mongoose models, pipelines, transactions and record adapter |
| `packages/mongodb/src/mongo/ids.js` | Identifier casts shared by the Mongo adapter, query builder and integrity layer: `invalidId()` (`NOT_VALID_ID`, 400, `Invalid identifier`), `castObjectId` (ObjectId or 24-hex string only, never mints; the adapter's `castId`), `castPathValue`/`castModelId` (the model's own schema type without setters, so String/Number `_id` keys work; used where Mongoose does not cast, such as `find`'s `requiredId` `$match`, and by protected integrity writes), `isQueryKeyCastFailure` (default-mode `getById`, `update` and `delete` pass the raw ID so Mongoose applies `_id` setters and casts once; a `CastError` on `_id` that is the query's own error flag becomes `NOT_VALID_ID` when the query runs, while the Query stays chainable) and `mapIdCastError` (ObjectId cast failures from writes become `NOT_VALID_ID`) |
| `packages/sql/src/` | Driver-free relational planner, record store, model/session orchestration and plugin contract; `errors.js` (not exported) classifies driver-call failures by origin. The `./internal/*` package subpaths are deprecated and unused, kept until 4.0 |
| `packages/postgres/src/` | PostgreSQL plugin/facade, codecs, record/query compiler and physical schema lifecycle |
| `packages/core/src/auth/index.js` | Shared auth plugin (`createAuthPlugin`), permission resolution, deprecated middleware; `packages/mongodb/src/auth/` remains a compatibility facade |
| `packages/core/src/auth/rules.js` | Shared rule helpers: `requireAuth`, `requireRole`, `composeRules`, `isOwner`, `allow`, `deny` |
| `packages/core/src/auth/expressions.js` | Shared policy expression DSL evaluator |
| `packages/core/src/auth/errors.js` | `UnauthenticatedError`, `ForbiddenError` (extend the shared `SimfinityError`; also exported from the core root); `runtime.js` also throws `ForbiddenError` for nested collection ownership denials |
| `packages/core/src/auth/thenable.js` | Internal `isThenable`/`discardThenable`; not exported |
| `packages/core/src/validators.js` | Shared declarative field validators: `stringLength`, `pattern`, `email`, `numberRange`, etc. |
| `packages/core/src/scalars.js` | Shared pre-built scalars (`EmailScalar`, `URLScalar`) and scalar factories |
| `packages/core/src/scalars/factory.js`, `packages/core/src/scalars/pattern.js` | `createValidatedScalar`; internal RegExp copy/reset helper shared by `validators.pattern` and `createPatternStringScalar` (not a published subpath) |
| `packages/core/src/plugins.js` | Shared plugin exports: `createAuthPlugin`, `apolloCountPlugin`, `envelopCountPlugin` |
| `packages/mcp/src/index.js` | Optional MCP generation and transports; `packages/mongodb/src/mcp.js` remains the Mongo facade shim. Uses optional `@modelcontextprotocol/sdk` |
| `packages/core/src/const/` | Shared filter operators, values and sort input types; MongoDB retains compatibility shims |
| `packages/core/src/errors/` | Shared `SimfinityError` and `InternalServerError` identities |
| `packages/core/src/introspection.js` | Once-only extension of the shared GraphQL introspection types; exports `fieldMetadataTypes`, `fieldMetadataNamesFixed`, `fixFieldMetadataNames` and `renameFieldMetadataTypes` for the collision-only fallback names |
| `packages/postgres/src/schema/` | Database description, DDL compilation and catalog validation |
### MCP contracts
- Generated input schemas preserve nullability independently from requiredness, including list items and recursive `$defs`. Defaults use GraphQL AST serialization; enum literals use member names. Opaque defaults without a literal representation use the original field default when omitted. The top-level arguments object is closed (`additionalProperties: false`); `$defs` are not.
- The terminal rejects with `MCP_UNKNOWN_ARGUMENT` (an isError result, before default-page injection, execution or `fetch`) only the undeclared keys the caller sent: the `callTool` wrapper records them, with defined values, in a `WeakMap` keyed by the `call` object before middleware runs, so `composeToolMiddleware` must hand the terminal that same object. Keys middleware injects pass; caller keys middleware deletes pass (the terminal checks `Object.hasOwn`, so a deleted `constructor` or `__proto__` is not mistaken for the inherited member). `suggestArgument` hints a case-insensitive match, else distance <= 1 when either name has <= 4 characters and <= 2 otherwise, never `id` except case-insensitively.
- In-process calls follow `graphql()`'s steps: `validateSchema(schema)` on every call, then the tool's operation, parsed and validated once by `prepareOperation()` and cached on its toolIndex entry (`entry.prepared`, shared by the HTTP handler's per-request `callTool`s, filled on the first call, after `schemaPlugins`), then `execute`. Results and errors must equal `graphql()`'s.
- `maxResultBytes` caps the text content block on success and on errors, including partial data and `totalCount`; `textFormat` (`normalizeTextFormat`: `'pretty'` by default or `'compact'`) yields the `stringify` that `createCallTool`, `limitErrorResult` and `oversizedMutationResult` use, so the cap measures the text sent. Limit diagnostics (`MCP_RESULT_TOO_LARGE`, `MCP_PAGE_SIZE_EXCEEDED`, `MCP_UNKNOWN_ARGUMENT`) are exempt; envelope overhead, duplicate structured content, remote download size and middleware-created results are outside the cap. An oversized mutation result (`entry.kind === 'mutation'`) never reads as a failure that invites a retry: `oversizedMutationResult` returns `isError: false`, `_meta.truncated` and `{ [field]: {id} | [{id}], [truncationKey]: { applied, message, resultBytes, maxResultBytes } }` on success (the ids only when they fit within `Math.floor(maxResultBytes)`, the integer the notice reports because `outputSchema` declares one; caps themselves may be fractional), and keeps `MCP_RESULT_TOO_LARGE` with `extensions.applied` (`true` or `'unknown'`, never `false`), `firstError` (300 characters) and id `data` with GraphQL errors. Its wording follows `entry.op` and `entry.idempotent` (the merged `idempotentHint`). Queries keep the plain error, plus `extensions.totalCount` on counted lists.
- Result keys MCP adds next to the root field use `resultKey` (`__`-prefixed on collision): `totalCount` for counted calls of list tools with a `pagination` argument (declared in `outputSchema`; a remote `extensions.count` only when it is a non-negative safe integer), and `truncated` (declared only when `maxResultBytes` is set at setup). Both are declared only without a `selection` override; `outputSchema` never gets `additionalProperties: false`. `_meta.count` stays.
- Generated find fields carry `extensions.simfinityQuery.countSink: true`; core's `canReportCount(parent, info, context)` runs before the queries, and without a sink and with a context that is not an object or function the count query is skipped. `reportCount(parent, info, context, count)` hands the count to a function under `Symbol.for('simfinity.countSink')` on `parent` (the root value), then `info.rootValue`, else stores it with `Reflect.set(context, 'count', count)`, and returns whether it was stored; a refused write logs one `warnOnce(runtimeIdentity, 'count-context', …)`. MCP passes a null-prototype root value with that sink on every in-process call of a flagged list tool and the caller's context unchanged, reading the count only when `pagination.count === true`. Unflagged list tools keep the `Object.create(context)` layer on counted calls. `buildToolDefinitions` reads the flag from the field at setup, before `schemaPlugins`, not from the resolver that runs: a replaced or wrapped resolver on a flagged field must forward `parent` or `info`, or report through the sink, unless the application clears the flag before setup. Neither package exports the symbol.
- Check cancellation again after awaiting an in-process context factory, before calling GraphQL. Do not promise to undo database work already started.
- Remote retry k waits `backoffMs × k`. Opt-ins, each only for exactly `true`: `retry.retryAfter` makes the next wait at least the previous attempt's `Retry-After` (`retryAfterMs`: delta-seconds or an HTTP-date that `parseHTTPDate` accepts, IMF-fixdate, RFC 850 or asctime, all GMT; any other text keeps the backoff) and returns that failure at once when it exceeds `maxRetryAfterMs` (default 10000); `retry.jitter` adds `Math.floor(Math.random() × backoffMs × k / 2)` to the longer of the backoff and the requested wait. Each wait uses the previous attempt's value, which is then reset. Without the opt-ins the schedule and `Math.random` use are unchanged. Remote attempt timeouts include reading the body. Queries alone may retry transport failures; client cancellation rejects during fetch, body reading or a retry backoff (`sleep(ms, signal)` rejects with `signal.reason` and removes its listener when the timer fires). Validate the remote GraphQL envelope before creating a tool result. `combineAbortSignals` returns `{ signal, release }`: it uses `AbortSignal.any` when the runtime has it, and on Node.js 19 and 20.0–20.2, inside the engines range, abort listeners that remove themselves on abort and through `release()`, which the retry loop calls in `finally`, so a long-lived caller signal keeps none. Over `createHTTPMCPHandler`, a client's `notifications/cancelled` reaches a fresh per-request server that does not know the request id, so only `res.on('close')` (`server.close()`) aborts an in-flight call's `extra.signal`; do not document MCP cancellation as reaching calls there.
- Remote `endpoint`, `headers` and `timeoutMs` are read on every call. `buildRemoteHeaders` accepts plain objects (`Object.entries`), Headers, Maps and pairs, sets Content-Type once case-insensitively, returns lowercase names, and reports invalid entries without values (`MCP_REMOTE_REQUEST_FAILED`). `normalizeTimeoutMs` treats falsy and 0 as no timeout, rounds up and caps at 2147483647. `resolveExecution` throws `MCP_INVALID_EXECUTION_CONFIG` at setup only for an `endpoint` that `parseEndpoint` reads as an absolute URL with a username or password (fetch rejects it on every call; the message never quotes the URL): relative, unparsable and non-http(s) endpoints, and `Request` instances, pass setup because an application-provided fetch may serve them. It also throws for invalid `timeoutMs` and for headers containers that are truthy non-objects, promises, functions or one-shot iterators, reading no header value. `readForSetupCheck` skips `headers` and `timeoutMs` when they are own getters (`hasOwnGetter`) or when reading them throws (an inherited getter or a Proxy whose token is not loaded yet), leaving the check to `executeRemote` per call; a non-throwing inherited getter is read and checked at setup. `executeRemote` reports a fetch `TypeError` for an unusable endpoint through `remoteEndpointProblem` (a `Request` uses its own URL), never with the undici message that quotes the URL.
- `createHTTPMCPHandler` always creates a fresh stateless server/transport per POST request; any other method gets 405 with `Allow: POST` and a JSON-RPC -32000 body inside the handler's `try`, before `context`, `callTool`, the server or the transport exist. Reject session generators, session callbacks and event stores at setup, after the SDK and transport load and before `resolveExecution`; applications needing sessions manage a transport with `createMCPServer`.
- `composeToolMiddleware` turns a synchronous middleware throw into a rejection and marks every dispatched promise handled (`pending.catch(() => {})`) while returning it.
- `classifyOperation`: queries use `extensions.simfinityQuery`, then the `aggregation` argument; a list needs `pagination` and a single record `id`, otherwise `customQuery` (read-only). Mutations use core's `extensions.simfinityMutation` (`save`/`update`/`delete`/`state_changed` → `transition`/`custom_mutation`), else the placeholder description plus name prefix; never the name alone. `buildAnnotations` makes updates `idempotentHint: !acceptsAddedItems(field)` (a list input field named `added` at any depth) and keeps deletes destructive and idempotent (data-level; a repeat calls `onDelete(null)`).
- Generated selections and output schemas skip nested collections (`isNestedCollection`: an object list field with a `pagination` argument) when `nestedCollections` is `'none'`, globally or per tool (`toolOverrides[tool].nestedCollections` wins; an explicit `selection` ignores both); the default `'all'` output must stay byte-identical. `buildToolDefinitions` validates the global value and every `toolOverrides` entry, published or not, with `MCP_INVALID_OPTIONS` (500), as `normalizeTextFormat` does for `textFormat`; there is no bounded mode in 3.x. One `$defs` registry (`createDefRegistry`) serves every tool of a `buildToolDefinitions` run, so tools share def objects; the exported `graphqlArgsToJSONSchema` builds a fresh registry per call.
- `prepareMCPTools()` and `createHTTPMCPHandler` validate execution, `textFormat`, `nestedCollections`, `toolMiddleware` (`normalizeToolMiddleware`: a single function is a one-element stack, copied at setup; an index loop rejects non-functions and sparse-array holes with `MCP_INVALID_MIDDLEWARE`), `limits` (`normalizeLimits`: falsy means unset, caps go through `normalizeCap` and must be at least 1, one NaN warning per call; then `validateDefaultPagination` runs `coerceInputValue` against each distinct `paginationType`; `MCP_INVALID_LIMITS`) and tool definitions before `applySchemaPlugins()`. That validates every entry (`normalizeSchemaPlugins`, `MCP_INVALID_SCHEMA_PLUGIN`) before any hook runs, inspecting own and inherited property descriptors (`pluginDescriptors`) so no getter other than `onSchemaChange` runs (`then` is checked through its descriptor; a `then` method or getter is rejected as a promise), rejects a misspelling of `onSchemaChange` only on entries without a function `onSchemaChange` (`isMisspelledSchemaHook`: a method or getter key that starts with `onschema` ignoring case or is within two optimal-string-alignment edits of `onschemachange`), warns once per plugin object about ignored hooks, rejects a different schema in `replaceSchema` (`MCP_UNSUPPORTED_SCHEMA_REPLACEMENT`) and returns a promise for asynchronous hooks: the server and HTTP factories await it, and `generateMCPTools` calls wait for it. `callTool` keeps the caller's `limits` object and runs `readLimits` on every call (the HTTP handler re-reads `options.limits` per request), so later changes apply. `createMCPServer`, `startStdioMCPServer` (also `loadStdioTransport`) and `createHTTPMCPHandler` (also its transport export, then its `transportOptions` check) load the SDK before any of this, so SDK errors precede every option error and plugins.
- `scalarToJSONSchema` follows the whole `baseScalarType` chain (cycle-safe) for the input JSON type and `format`, and publishes a custom scalar's own description. With `{ output: true }` it publishes a primitive type only for spec scalars reached through `createValidatedScalar` links (`serializedSpecScalar`, which follows core's non-exported `Symbol.for('simfinity.validatedScalarBase')` marker); every other scalar is `{}`, with a curated description for description-less `Date`/`DateTime`/`Time` roots. `includeId` is accepted and ignored.
- TypeScript (`packages/mcp/types/index.d.ts`): read-side option interfaces (`GenerateMCPToolsOptions`, `MCPServerOptions`, `HTTPMCPHandlerOptions`, `MCPLimits`, `MCPRemoteExecutionOptions`, `MCPRetryOptions`, `MCPToolOverride`) may gain optional members (3.6.0: `nestedCollections`, `textFormat`, `retry.retryAfter`/`maxRetryAfterMs`/`jitter`, `MCPToolOverride.nestedCollections`); never narrow or retype existing members, and widen the accepted input forms of existing members only in the input-only parameter types (`MCPOptionsInit`, `MCPLimitsInit`, `MCPRemoteExecutionInit`, `GenerateMCPToolsInit`, `MCPServerInit`, `HTTPMCPHandlerInit`), so code that reads options back keeps compiling. The `mcpTypes` consumer in `scripts/test-packages.js` pins both directions. `EnvelopSchemaPlugin` is core's declaration; MCP imports and re-exports it and must not declare it again, or `export *` of MCP with core reports TS2308. `createAuthPlugin` returns core's `AuthSchemaPlugin`.
## Instance State (inside `createRuntime` in `packages/core/src/runtime.js`)
- `typesDict` -- primary registry: `{ types: { [typeName]: { model, gqltype, simpleEntityEndpointName, listEntitiesEndpointName, endpoint, controller, stateMachine, inputType, storedIdentity } } }`. `storedIdentity` is set on every `createSchema()` before `adapter.prepare()`: true for `connect()` types and targets of non-embedded relations (entities), false for embedded-only types; it is mirrored on `typesDictForUpdate`, and entity type objects are also marked with `markStoredIdentity()`, which the Mongo query builder reads (`hasStoredIdentity()`) to map embedded `.id` paths, so the rule follows the shared type object
- `typesDictForUpdate` -- parallel registry with update-specific input types
- `waitingInputType` -- registered types whose inputs are not built yet; a type waits only for the input types of its embedded fields. Types with unsupported writable lists (lists of lists) or embedded types that contain each other never resolve (`INPUT_TYPE_UNRESOLVED`, naming every unbuildable field of the listed types, with the causes apart from the fields that only wait for another listed type). Embedded cycles reach this with adapters that do not validate models, and in default MongoDB mode when only supplied models reach the cycle (a `readOnly` link then builds). PostgreSQL and transactional MongoDB (`describeModels`, every entity) and generated MongoDB models (`generateSchemaDefinition`, every mode, only the models it generates) reject them first with `INVALID_MODEL` `Embedded cycle at …`; do not add a prepare-time walk in 3.x, since supplied-model cycles with a `readOnly` link start today. Referenced collections resolve item inputs lazily, in fields thunks, so they never wait
- `excludedFieldInputTypes` -- `Map` from generated `added` item input name to `{ itemType, fieldToExclude, optionalFields, type }`, so collections that reach one child through one `connectionField` share that input
- `selfCollectionNameHolders` and `qualifiedSelfCollections` -- `Map`s from a self-referencing collection field name to the types whose inputs were built with `OneToManyA|U<field>`, and from a type to its self collections named `OneToMany<Type>A|U<field>` (`claimSelfCollectionNames()`). A holder qualifies another type only while it is still the registered type for its name and the same build reaches it; otherwise the type being built becomes another holder
- `registeredMutations` -- custom mutations registered via `registerMutation()`
- `middlewares` -- array of middleware functions registered on this runtime via `use()`; `use()` rejects non-functions with `INVALID_MIDDLEWARE`
- `runtimeIdentity` -- private object identifying this runtime in `runtimeBoundTypes`
- `reportedIssues` -- `WeakMap` from type to the `Configuration issue` warning keys already logged (`warnOnce`), so each warning is logged once per runtime
These registries are per runtime. Module-scope state shared by every runtime:
- the introspection extension;
- `maxPageSize` in `packages/core/src/query-limits.js` (`configureQueryLimits()`);
- `maxNestedOperations` in `packages/core/src/mutation-limits.js` (`configureMutationLimits()`), null (unlimited) by default;
- `runtimeBoundTypes` in `runtime.js`, a `WeakMap` from each GraphQL object type to the identity of the runtime that generated its relation resolvers (bound) or whose schema reached it with an unresolved non-embedded relation field (reserved, such as an unregistered custom mutation result). `connect()`, `addNoEndpointType()` and `createSchema()` throw `TYPE_BOUND_TO_OTHER_RUNTIME` (409) for a type bound or reserved elsewhere;
- `relationFieldOwners` in `packages/core/src/relation-owners.js` (internal, imported by `runtime.js` and `auth/index.js`), a `WeakMap` from each generated relation resolver, the fresh extensions object of its field, and the auth plugin's wrapper of such a resolver (which inherits the owner) to the runtime that generated it. `toConfig()` copies and field spreads keep the resolver and the extensions object, so `createSchema()` and `autoGenerateResolvers()` reject a copied field from another runtime (`assertFieldsNotBoundElsewhere`): fields without a resolver are skipped and get this runtime's own; a foreign resolver tag is rejected; the extensions tag counts only on fields whose resolver is untagged; interface fields are not checked, because graphql-js never runs their resolvers (the types they reference still are). A third-party in-place wrapper copied with a rebuilt extensions object is not detected. Type objects are not shared between runtimes that generate or reserve their relation resolvers; types without unresolved non-embedded relation fields may be, and a `toConfig()` copy made before the first schema may be used by another runtime.
Do not describe module-scope state as instance-isolated.
## Introspection Extension (Critical)
Simfinity adds `extensions` to the field map returned by `__Field.getFields()`. This works before or after GraphQL materializes introspection fields and reuses an existing extension field on repeated module evaluation, preserving metadata type identity. Do not capture or replace the private `_fields` thunk.
The extension is still global to the shared GraphQL peer, and Simfinity's registries belong to each runtime instance. Schemas constructed after import collect `FieldExtensionsType` and `RelationType` (or their fallback names `SimfinityFieldExtensionsType` and `SimfinityRelationType`, which a clash in the process's first Simfinity schema sets for every later schema) in their type maps. Pre-existing schemas retain their original type maps: ordinary operations and direct metadata introspection work, but named fragments on these new types require a schema constructed after import.
Tools that clone types (`graphql-middleware`'s `applyMiddleware` / `mapSchema`) remain unsupported. `buildClientSchema()` on a post-import schema's introspection can also produce duplicate metadata types when run in the same process as Simfinity. Use Envelop plugins and in-place resolver wrapping; initialization safety does not provide schema isolation or schema-cloning compatibility.
Where files are headingNew Cursor rule files have more rules about planning and tracking tasks, and fewer about code style.
Of the Cursor rule files we read, the 3,978 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository9 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .cursor/rules/simfinity-auth-module.mdcCursor rule
- .cursor/rules/simfinity-barber-examples.mdcCursor rule
- .cursor/rules/simfinity-coding-standards.mdcCursor rule
- .cursor/rules/simfinity-core-functions.mdcCursor rule
- .cursor/rules/simfinity-development-workflow.mdcCursor rule
- .cursor/rules/simfinity-documentation.mdcCursor rule
- .cursor/rules/simfinity-extensions.mdcCursor rule
- .cursor/rules/simfinity-testing.mdcCursor rule
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

