diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..771ce3e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI +# Every push runs CI, on any branch, so a unit needs no pull request to be checked. +on: + push: + workflow_dispatch: +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: npm + - uses: actions/setup-python@v7 + with: + python-version: "3.x" + - run: npm ci + - run: python3 design/tools/check.py + - run: npm run typecheck + - run: npm run lint + - run: npm run format:check + - run: npm test + native: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: npm + - uses: actions/cache@v6 + with: + path: .cache + key: convex-local-backend-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('harness/backend-release.json') }} + - run: npm ci + - run: npm run codegen + - run: git diff --exit-code -- fixture/convex + - run: npm run test:native + - uses: actions/upload-artifact@v7 + if: always() + with: + name: native-evidence + path: evidence/runs/*.json diff --git a/.gitignore b/.gitignore index 646ac51..a4189ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ -.DS_Store node_modules/ +.DS_Store +generated/ +.cache/ +evidence/runs/ +.env.local diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6a5a479 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +design/ +docs/ +vendor/ +generated/ +**/_generated/ +evidence/**/*.json +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..e4774d7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{ "singleQuote": false, "trailingComma": "all" } diff --git a/AGENTS.md b/AGENTS.md index 0f5219e..6390f0d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # libar-platform -The Convex transactional domain platform. `docs/` holds the inputs. `design/` holds the design as a Software Delivery Protocol corpus of Specs. The code built from the design sits beside them; there is none yet. +The Convex transactional domain platform. `docs/` holds the inputs. `design/` holds the design as a Software Delivery Protocol corpus of Specs. The code built from the design sits beside them: `harness/`, `fixture/` and `tests/`. ## Which reader you are @@ -11,7 +11,7 @@ The Convex transactional domain platform. `docs/` holds the inputs. `design/` ho - `docs/convex-transactional-domain-platform-decisions.md`, "the doc", says what the platform is. Its decisions are proposals until the owner rules. Only the owner states `ready` on a Spec, settles an open question or edits the doc. - A Spec is intended truth. Report code that disagrees with a Spec as a gap, and the main thread rules which side changes. -- Read the corpus through its graph. Run `npm ci` once, then `npx sdp q '' --root design --json` with a body from `node_modules/@libar-dev/software-delivery-protocol/docs/agent-surface/recipes.md`. Always `npx sdp`: a bare `sdp` on macOS is an unrelated tool. +- Read the corpus through its graph. Run `npm ci` once, then, from the repository root, `npx sdp q '' --json` with a body from `node_modules/@libar-dev/software-delivery-protocol/docs/agent-surface/recipes.md`. Always `npx sdp`: a bare `sdp` on macOS is an unrelated tool. - Before you write or edit a Spec, read `design/PLAN.md` section 6 and `node_modules/@libar-dev/software-delivery-protocol/.agents/skills/sdp-authoring/SKILL.md`. - `python3 design/tools/check.py` prints `OK` when the Specs, the README and the ledger agree. Run it before you say they do. - Name the tier behind a claim about behavior: compiled, pure test, `convex-test`, or native backend. diff --git a/README.md b/README.md index 181d083..481d7a1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A transactional domain platform for Convex. Bounded contexts own their state and their journal, one business operation is one mutation, and work leaves the transaction only when something has to wait, spread load or reach an external system. -This repository holds the design and, from the first build slice on, the code built from it. There is no code yet. +This repository holds the design and, from the first build slice on, the code built from it. The first slice built the test harness, the fixture app and the probes. ## Where to start @@ -14,8 +14,8 @@ This repository holds the design and, from the first build slice on, the code bu ```sh npm ci -npx sdp view design -open design/generated/design-review/index.md +npx sdp view +open generated/design-review/index.md ``` `python3 design/tools/check.py` validates the corpus and checks that the Specs, the index and the review ledger agree. @@ -23,3 +23,28 @@ open design/generated/design-review/index.md ## Status The design has been through three review rounds and carries open findings, all listed in `design/reviews/consensus-ledger.json`. It is being finished by building it, one slice at a time, starting with the repository layout and the probes. + +## Running the code + +Use Node 24 and run `npm ci` in the repository root. The repository has one `package.json`. The root `convex.json` names `fixture/convex` as the function directory of the fixture app. + +Each tier has its own command and its own directory: + +- Domain tier: `npm run test:pure` runs `tests/pure`. These tests need no backend and no simulator. Until slice S1 adds a domain, they test the pure parts of the harness. +- Simulator tier: `npm run test:simulator` runs `tests/simulator` on `convex-test`. Every test name starts with `convex-test` and the version it ran on. A simulator pass is not native proof. +- Native tier: `npm run test:native` runs `tests/native`. Every test owns its processes and temporary storage, stops its processes and removes its storage when it ends. Bound examples deploy the fixture app. +- Compile check: `npm run typecheck` compiles the fixture app, the harness and the tests. `npm run test:types` runs the type tests in `tests/types`. A compile check says nothing about a running backend. + +The claim tiers in `AGENTS.md` map to these names: compiled means compile check, pure test means domain tier, `convex-test` means simulator tier, and native backend means native tier. + +`npm test` runs the type tests, the domain tier and the simulator tier. None of them needs a backend. + +The native tier downloads the pinned backend release into `.cache/` on first use and checks the hash of the executable on every run. `harness/backend-release.json` pins the release for macOS on Apple silicon and for Linux on x86-64. On another platform, or to run another build, set `CONVEX_BACKEND_BINARY` to a `convex-local-backend` executable. The run's record then names the release only when the executable's hash is the pinned one. + +The harness gives every Convex CLI call the address and the admin key of the backend it started, and an environment of its own. A deploy key or a deployment named in your shell or in a `.env` file is never used. + +`npm run codegen` starts a disposable backend and rewrites `fixture/convex/_generated` and `fixture/convex/annex/_generated`. Run it after you add or rename a fixture function or change its arguments, and commit the result. CI fails when the committed files differ from what codegen writes. + +`npm run dev` starts one disposable backend and runs `convex dev` against it. Each time you save a file under `fixture/convex`, it rewrites both `_generated` directories. It runs until you press Ctrl-C, then stops the backend and removes it. Commit the generated files as with `npm run codegen`: CI still fails when they are stale. It refuses to start when a `.env.local` file exists in the repository root, because `convex dev` would write the backend's URL into it. + +Every native run writes a record to `evidence/runs/`. [`evidence/README.md`](evidence/README.md) says what a record holds and which records are kept. diff --git a/convex.json b/convex.json new file mode 100644 index 0000000..4d29aa8 --- /dev/null +++ b/convex.json @@ -0,0 +1,3 @@ +{ + "functions": "fixture/convex/" +} diff --git a/design/.gitignore b/design/.gitignore deleted file mode 100644 index 9ab870d..0000000 --- a/design/.gitignore +++ /dev/null @@ -1 +0,0 @@ -generated/ diff --git a/design/PLAN.md b/design/PLAN.md index 2c73547..020d593 100644 --- a/design/PLAN.md +++ b/design/PLAN.md @@ -10,17 +10,18 @@ The CLI is the `sdp` binary of the pinned `@libar-dev/software-delivery-protocol ```sh # from the repository root -npx sdp validate design +npx sdp validate ``` -Both forms were run against the scaffold in this directory on 2026-09-30 and printed `validate: 0 errors · 0 warnings`. No `--exclude` is needed; `design/` carries no fixtures. Output goes to `design/generated/` (graph, contracts, registrar manifest), which `design/.gitignore` ignores. +The Protocol's root is the repository root since slice S0, so that one graph holds the Specs under `design/specs/` and the test anchors that verify them, which live outside `design/`. No `--exclude` is needed. Output goes to `generated/` at the repository root (graph, contracts, registrar manifest), which `.gitignore` ignores. A test imports its example's step contract from `generated/contracts/`, so `npx sdp build` runs before the tests. Other verbs, same path and root rules: -- `npx sdp validate design --watch` is the authoring loop; it re-runs on carrier changes and stays alive. -- `npx sdp view design` writes the Design Review to `design/generated/design-review/` (an index plus one page per Spec and Pack). This is the human-readable rendering of the corpus. -- `npx sdp q '' --root design --json` evaluates a recipe body from the package's `docs/agent-surface/recipes.md` against this corpus. Recipes 3 (one Spec's guarantees), 5 (Pack review), 6 (concept search), 7 (readiness divergence), 8 (warnings as data), 9 (promotion preflight) and 11 (the lower ladder) are the useful ones here. -- `npx sdp new spec specs//.sdp.md --id ID --kind KIND --altitude ALT --title TITLE --outcome OUTCOME` scaffolds an idea-rung stub. PATH is relative to the current directory, so run it from `design/`. +- `npx sdp validate --watch` is the authoring loop; it re-runs on carrier changes and stays alive. +- `npx sdp view` writes the Design Review to `generated/design-review/` (an index plus one page per Spec and Pack). This is the human-readable rendering of the corpus. +- `npx sdp q '' --json` evaluates a recipe body from the package's `docs/agent-surface/recipes.md` against this corpus. Recipes 3 (one Spec's guarantees), 5 (Pack review), 6 (concept search), 7 (readiness divergence), 8 (orphans and gaps), 9 (promotion preflight) and 11 (the lower ladder) are the useful ones on paper. With code in the tree these join them: 2 (the drift alarm, which lists what is built and does not state `ready`), 4 (what a changed file reaches), 10 (declared against enabled verifiers), 20 (the open-question register), 21 (what a Spec rests on, with each target's rung and floor), 22 (the mention audit) and 23 (entry search by name). +- `npx sdp q '' --root node_modules/@libar-dev/software-delivery-protocol/specs --json` asks the same questions of the Protocol's own Specs, which the authoring skill cites. +- `npx sdp new spec design/specs//.sdp.md --id ID --kind KIND --altitude ALT --title TITLE --outcome OUTCOME` scaffolds an idea-rung stub. PATH is relative to the current directory, so from the repository root it starts with `design/specs/`. Exit code 1 means an error; warnings exit 0. The corpus must exit 0 with zero errors and zero warnings. @@ -158,10 +159,11 @@ Relations shown are the minimum. `epic` means `spec:platform.transactional-domai | `spec:platform.decision-method` | rule | feature | defined | refines epic | The seven rules of "How decisions are made" | | `spec:platform.vocabulary` | model | feature | defined | refines epic | CMS, journal, stream version, receipt, obligation, effect, attempt, generation, operation, baseline event, do-nothing option, plus: context, parent, use case, read model, projection, tenant scope, actor, request key, effect key, needs attention, write pause, layer, profile. OQ6 clashes (receipt, generation) as `[non-blocking]` open questions | | `spec:platform.acceptance-contract` | rule | feature | defined | refines epic | Scenarios test behavior; layer = first layer making it available; the four tiers; fixture app separate from example app; test-only functions never ship; disposable backend per test; the evidence record; readiness stated as specified, implemented, tested under named conditions, operationally accepted | +| `spec:platform.native-harness` | rule | feature | defined | refines acceptance-contract | E-13 and E-15, which the doc does not state: the fixture issuer and ordinary clients, the admin key for setup and reading only, two compositions, faults and assertions, the four controls, the run's record. Added by slice S0 with four examples | | `spec:platform.existing-systems` | rule | feature | defined | refines epic | No deletion or migration authorized; the five bullets | | `spec:laws.law01-sanctioned-writes-only` ... `spec:laws.law12-one-retry-owner` | rule | feature | defined | refines epic (Law 4 also decidedBy D6) | One law each, the sentence verbatim in `## Rule`, the reworded note in narrative | | `spec:decisions.d01-one-mutation-per-operation` ... `spec:decisions.d19-operations-travel-with-capability` | decision | feature | defined | refines epic; dependsOn per table below | Provenance line in narrative; `context` = the concern and the do-nothing option; first `alternative` = the do-nothing option; further `alternative`s = the doc's rejected options; `decision` = the ruling; `rationale`; `consequence` = costs, gaps, open items; the doc's "Probe" lines as `[non-blocking]` open questions naming the probe number | -| `spec:facts.fact-ledger` | rule | feature | defined | refines epic | Statuses; nothing probed yet; sources support facts not the design; each fact names its decisions | +| `spec:facts.fact-ledger` | rule | feature | defined | refines epic | Statuses; the probes that have run; sources support facts not the design; each fact names its decisions | | `spec:facts.probe-plan` | workflow | feature | defined | refines fact-ledger; dependsOn F14 to F17 | Seven flows, one per probe, each ending with the decisions it serves | | `spec:facts.f01-serializable-mutations-under-occ` ... `spec:facts.f17-migrations-fits-generation-backfill` | constraint | story | defined | refines fact-ledger | statement = the fact; `flavor: convex-fact`; `target` per 3.2; `measurableBy` = source and probe | @@ -273,7 +275,7 @@ Fact slugs: `f01-serializable-mutations-under-occ`, `f02-component-calls-commit- ## 4. Scenarios to examples -Every row of the doc's acceptance table becomes at least one `example` Spec. When a row enumerates cases (three injection points, three order sizes, two broken subsystems), each case is one sibling example binding a different point, per SDP's point-per-example law. Titles are the doc's scenario text. The tier comes from the acceptance contract: domain (pure), simulator, native, or end to end. +Every row of the doc's acceptance table becomes at least one `example` Spec. When a row enumerates cases (three injection points, three order sizes, two broken subsystems), each case is one sibling example binding a different point, per SDP's point-per-example law. An "or" in the scenario's text enumerates cases the same way (a worker or agent namespace, rate or capacity refusal). An "or" in the pass condition does not: the example binds the alternative its design produces. A verification bullet adds assertions to a case and never stands in for one. Titles are the doc's scenario text. The tier comes from the acceptance contract: domain (pure), simulator, native, or end to end. | Sc | Scenario (doc text) | Parent Spec | Example ID | Tier | Pkg | |---|---|---|---|---|---| @@ -329,7 +331,7 @@ Five packages, disjoint by directory. Sequence: A first, because every other pac Definition of done, every package: -1. `npx sdp validate design` exits 0 on the merged corpus. While a foreign ID from the package's cross-package list is not yet authored, the only tolerated error is `conformance/referential-integrity` naming that ID. +1. `npx sdp validate` exits 0 on the merged corpus. While a foreign ID from the package's cross-package list is not yet authored, the only tolerated error is `conformance/referential-integrity` naming that ID. 2. No warning. An example with no test bound is data below `ready`, not a warning. 3. Every Spec in the package's inventory exists with the stated kind, altitude, readiness and minimum relations; every scenario assigned to the package has its example(s). 4. The pack manifest lists every Spec of the package in inventory order. @@ -601,7 +603,7 @@ Completeness: 18. All 43 scenario rows of the doc have examples per section 4, with the enumerated cases split into siblings. 19. Every component in section 2 has its Specs; every Spec in 3.4 exists. 20. Every open question of the doc (OQ1 to OQ6) is recorded on the Spec section 3.4 assigns it to. -21. Every assumed fact (F14, F15, F17) states `scoped` behind a `[blocking]` question that names its probe, is named as an `assumption` or open question on each Spec that relies on it, and has its probe in the probe plan. F16 was found documented and states `defined`. +21. Every assumed fact (F17; F14 and F15 until their probes ran in slice S0) states `scoped` behind a `[blocking]` question that names its probe, is named as an `assumption` or open question on each Spec that relies on it, and has its probe in the probe plan. F16 was found documented and states `defined`. Convex correctness, checked against the sources the doc lists (S1 to S15) and the recheck in section 9, never from memory: @@ -616,8 +618,8 @@ Convex correctness, checked against the sources the doc lists (S1 to S15) and th 30. Scheduling: `ctx.scheduler` calls from a mutation commit with it (F8); at most 1000 per mutation; a scheduled action is never retried by Convex (F9), so external calls are claim, call, settle with retries owned by the obligation module; scheduled functions carry no auth, so the obligation record carries authority (D13). 31. `_scheduled_functions`: the design uses only the documented states (Pending, InProgress, Success, Failed, Canceled) and the 7-day retention, and never assumes `cancel` stops an in-flight action. 32. Restore: backups exclude pending scheduled functions (F12), so restore rebuilds dispatch from obligations and starts with dispatch off (D19). -33. `ConvexError`: rejection data uses values the `v` validators accept; the design states that a throw in a mutation prevents the commit and that data intact across a component boundary is F14, assumed, with Probe 2 named. -34. Reactivity of a parent query over a component query is stated as F15, assumed, with Probe 5 named. +33. `ConvexError`: rejection data uses values the `v` validators accept; the design states that a throw in a mutation prevents the commit and that data intact across a component boundary is F14, probed by Probe 2. +34. Reactivity of a parent query over a component query is stated as F15, documented and probed by Probe 5. 35. Workflow component: only waits and external effects are steps; per-step retries are off or ownership is passed deliberately; the definition version is saved at start; deploy behavior is stated as trigger and promise (D16). `@convex-dev/migrations`: a batch is a checkpointed mutation, so backfill versus live commands is an OCC race per batch (D9, Probe 6). 36. Timeouts: queries and mutations have 1 second; the max-lines limit and every per-command bound respect it; actions have 30 minutes (Convex runtime) or 10 minutes (Node). diff --git a/design/README.md b/design/README.md index cc2e354..99e02e9 100644 --- a/design/README.md +++ b/design/README.md @@ -14,20 +14,20 @@ Run the review and open its index: ```sh npm ci # once, from the repository root: installs the pinned Protocol CLI -npx sdp view design -open design/generated/design-review/index.md +npx sdp view +open generated/design-review/index.md ``` Always `npx sdp` or an `npm run` script. A bare `sdp` on macOS resolves to an unrelated Xcode tool. The Protocol version is the one `package.json` pins; `python3 design/tools/check.py` prints it. -The index lists every Spec with its kind, altitude, stated readiness and the floor its structure reaches. Each Spec has one page under `generated/design-review/spec/`, each pack under `generated/design-review/pack/`. The directory is gitignored; regenerate it after any edit. +The index lists every Spec with its kind, altitude, stated readiness and the floor its structure reaches. Each Spec has one page under `generated/design-review/spec/`, each pack under `generated/design-review/pack/`. The Protocol's root is the repository root, because the graph binds the Specs under `design/specs/` to the tests that verify them, which live outside `design/`. `generated/` at the repository root is gitignored; regenerate it after any edit. -A reading order that works. Start with the epic ([`transactional-domain-platform`](specs/platform/transactional-domain-platform.sdp.md)) and three of its five platform children ([`layers-and-profiles`](specs/platform/layers-and-profiles.sdp.md), [`decision-method`](specs/platform/decision-method.sdp.md), [`vocabulary`](specs/platform/vocabulary.sdp.md)); the other two, [`acceptance-contract`](specs/platform/acceptance-contract.sdp.md) and [`existing-systems`](specs/platform/existing-systems.sdp.md), are best read before the scenario table below. Then the twelve laws in [`specs/laws/`](specs/laws/) and decisions D1 to D12 in [`specs/decisions/`](specs/decisions/). Then go up the layers: the kernel ([`outcome-model`](specs/kernel/outcome-model.sdp.md) first, then [`decider-contract`](specs/kernel/decider-contract.sdp.md) and [`domain-kernel`](specs/kernel/domain-kernel.sdp.md)), the context component and its children in [`specs/context/`](specs/context/), the command pipeline in [`specs/command/`](specs/command/) (read [`actor-and-scope`](specs/command/actor-and-scope.sdp.md) before [`command-pipeline`](specs/command/command-pipeline.sdp.md)), and the composed application in [`specs/application/`](specs/application/). Read the fact ledger and the probe plan in [`specs/facts/`](specs/facts/) before the Layer 3 material, because the obligation module's justification is a probe that has not run. Layers 4 to 6 are trigger, promise and rules only. +A reading order that works. Start with the epic ([`transactional-domain-platform`](specs/platform/transactional-domain-platform.sdp.md)) and three of its five platform children ([`layers-and-profiles`](specs/platform/layers-and-profiles.sdp.md), [`decision-method`](specs/platform/decision-method.sdp.md), [`vocabulary`](specs/platform/vocabulary.sdp.md)); the other two, [`acceptance-contract`](specs/platform/acceptance-contract.sdp.md) and [`existing-systems`](specs/platform/existing-systems.sdp.md), are best read before the scenario table below, and [`native-harness`](specs/platform/native-harness.sdp.md), which refines the acceptance contract, before any native test. Then the twelve laws in [`specs/laws/`](specs/laws/) and decisions D1 to D12 in [`specs/decisions/`](specs/decisions/). Then go up the layers: the kernel ([`outcome-model`](specs/kernel/outcome-model.sdp.md) first, then [`decider-contract`](specs/kernel/decider-contract.sdp.md) and [`domain-kernel`](specs/kernel/domain-kernel.sdp.md)), the context component and its children in [`specs/context/`](specs/context/), the command pipeline in [`specs/command/`](specs/command/) (read [`actor-and-scope`](specs/command/actor-and-scope.sdp.md) before [`command-pipeline`](specs/command/command-pipeline.sdp.md)), and the composed application in [`specs/application/`](specs/application/). Read the fact ledger and the probe plan in [`specs/facts/`](specs/facts/) before the Layer 3 material, because the obligation module's justification is a probe that has not run. Layers 4 to 6 are trigger, promise and rules only. -Questions the graph answers better than the files. Pass a recipe body from the Protocol's catalog to `sdp q` with this corpus as the root. The catalog ships in the package, at `node_modules/@libar-dev/software-delivery-protocol/docs/agent-surface/recipes.md`: +Questions the graph answers better than the files. Pass a recipe body from the Protocol's catalog to `sdp q`, run from the repository root. The catalog ships in the package, at `node_modules/@libar-dev/software-delivery-protocol/docs/agent-surface/recipes.md`: ```sh -npx sdp q '' --root design --json +npx sdp q '' --json ``` Recipe 3 gives one Spec's sections, relations and verifiers. Recipe 5 reviews a pack. Recipe 6 finds a concept by phrase, so `D7` or `staleVersion` lands on every Spec that carries it. Recipe 7 lists Specs whose stated readiness their structure does not earn; it returns `[]` on this corpus. Recipe 8 returns the warnings as data. Recipe 9 is the promotion preflight for one Spec. Recipe 11 lists every non-ready Spec by family with its floor. Recipe 20 is the open-question register, blocking questions first. Recipe 21 lists what one Spec rests on, with the rung each target states. Recipe 22 audits Spec ids written in prose against declared relations. Recipe 23 searches keyed Design entries. @@ -36,7 +36,7 @@ Recipe 3 gives one Spec's sections, relations and verifiers. Recipe 5 reviews a ```sh # from the repository root -npx sdp validate design +npx sdp validate # the session check: validate, readiness divergence, and this project's own policy python3 design/tools/check.py @@ -45,11 +45,11 @@ python3 design/tools/check.py Expected result on the integrated corpus: ``` -168 specs · 5 packs · 0 anchors → 173 nodes · 881 edges (0 errors, 0 warnings) +187 specs · 5 packs · 22 anchors → 214 nodes · 959 edges (0 errors, 0 warnings) validate: 0 errors · 0 warnings (conformance + honesty over the one graph) ``` -Any warning and any error is a defect. Every example declares `verifies` toward its parent for fidelity to the acceptance contract, and no test anchor binds it yet. The Protocol reads an unbound example below `ready` as data and does not warn; recipe 10 lists each one as a declared verifier that is not enabled. `npx sdp validate design --watch` is the authoring loop. +Any warning and any error is a defect. Every example declares `verifies` toward its parent for fidelity to the acceptance contract, and no test anchor binds it yet. The Protocol reads an unbound example below `ready` as data and does not warn; recipe 10 lists each one as a declared verifier that is not enabled. `npx sdp validate --watch` is the authoring loop. ## The component map @@ -82,7 +82,7 @@ Detail follows the build (decision method, rule 4). Full means interfaces, TypeS | Layer | Families | Pack | Specs | |---|---|---|---| -| all | `platform`, `laws`, `decisions`, `facts` | [`foundation`](specs/foundation.pack.sdp.md) | 56 | +| all | `platform`, `laws`, `decisions`, `facts` | [`foundation`](specs/foundation.pack.sdp.md) | 75 | | 0 | `kernel` | [`kernel-and-context`](specs/kernel-and-context.pack.sdp.md) | 7 | | 1 | `context`, `command` | [`kernel-and-context`](specs/kernel-and-context.pack.sdp.md) (18 with the kernel), [`command-pipeline`](specs/command-pipeline.pack.sdp.md) | 11 + 20 | | 2 | `application`, `operations`, `constraints` | [`application`](specs/application.pack.sdp.md) | 31 | @@ -91,7 +91,7 @@ Detail follows the build (decision method, rule 4). Full means interfaces, TypeS | 5 | `agents` | [`durable-and-later`](specs/durable-and-later.pack.sdp.md) | 7 | | 6 | `advanced` | [`durable-and-later`](specs/durable-and-later.pack.sdp.md) | 8 | -Counts by kind and by stated readiness are derived, not kept here: `npx sdp census design` writes the census, and the session check prints the readiness tally. Three groups state `scoped`: every Layer 4 to 6 non-example Spec, held by its deferral, and the three assumed facts F14, F15 and F17, each held by its probe. Nothing states `ready`. Every pack carries `modelRefs: [spec:platform.vocabulary]`; the application pack also refs the example domain's model. +Counts by kind and by stated readiness are derived, not kept here: `npx sdp census` writes the census, and the session check prints the readiness tally. Two groups state `scoped`: every Layer 4 to 6 non-example Spec, held by its deferral, and the assumed fact F17, held by its probe. Nothing states `ready`. Every pack carries `modelRefs: [spec:platform.vocabulary]`; the application pack also refs the example domain's model. What each layer fixes, in one paragraph each. @@ -101,7 +101,7 @@ Layer 1, context side. A bounded context is one Convex component with `streams` Layer 1, parent side. Every command runs through one top-level mutation with eleven steps: parse, authenticate, establish scope, authorize, look up the receipt, admit, read the maintenance gate and mint the whole `OperationRef`, execute the contexts through `ctx.runMutation` on their operations with the facts the executor captured, update read models from the `streams` the executor returned, insert the receipt and the audit record, emit a diagnostic and return. The only catch is an outermost rethrow that gives a context's bare `Rejection` and the pipeline's own rejections one wire shape. Each command has one declaration and two static exports: `publicCommand` (a `mutation`, namespace fixed to `public`, reads `ctx.auth`) and `internalCommand` (an `internalMutation` that trusted server code calls with `actor`, `namespace` and a required `requestKey`); the doubled function count and the single-export alternative it beat are recorded under E-7. Receipts are thin, keyed by four indexed fields, guarded by a SHA-256 fingerprint that excludes captured facts, read and inserted in the same transaction; a rejection or transient refusal writes none, and the second UI submit of a create is answered with `entityExists`, not an idempotent success (E-39). The declaration's `bounds` is the one input bound, checked at step 1, and its `writes` names the stream types the executor may write. The internal entry takes an optional `causedBy`, which is how a reaction's derived command names its publication event (E-31), and a derived command carries the `worker` namespace (E-6). The actor, tenant scope, caller namespace, the worker's `Authority` and the grants table are defined once in [`actor-and-scope`](specs/command/actor-and-scope.sdp.md) and reused by the envelope, the receipts table and the obligation record. -Layer 2. A use case calls each context once with a list, applies the cross-context rule to the DTOs it gets back, and returns the union of stream versions with every context call's `streams` entries; the pipeline's step 9 writes the read models the declaration lists from those entries through one `applyProjection` shared with rebuild, and a rejection or throw from the second context rolls back the first. Rebuild is a new numbered generation: live commands write it too, backfill batches of at most 100 entities, capped by the context's byte-derived page bound, write only where the target is missing or older, verification precedes a one-write switch, and the old generation stays for 7 days. Cross-stream history views rebuild under the write pause, one gate document the pipeline reads for every command, with the generation's fence for background writers. Such a view is a `HistoryProjection` that folds events into its row; its paused rebuild closes the view's source stream types, so every writer of those streams is held whether or not it maintains the view, and its backfill pages each source's history. Restore closes writers through `MAINTENANCE_MODE`, restores the snapshot, runs four checkpointed checks (journal, domain, read model, receipt), reopens only on zero findings, and only then hands over to the durable profile's schedule rebuild. Audit is written inside the transaction and fails closed; diagnostics go through a sink that never throws. The first experiment is Orders and Inventory with `PlaceOrder`, `CancelOrder`, the order summary and the order allocation history view that Sc L2-6 builds; its six cost targets are constraint Specs, and its native runs authenticate through the local backend's admin key acting as the production issuer's identity, the one named difference from production (E-13). +Layer 2. A use case calls each context once with a list, applies the cross-context rule to the DTOs it gets back, and returns the union of stream versions with every context call's `streams` entries; the pipeline's step 9 writes the read models the declaration lists from those entries through one `applyProjection` shared with rebuild, and a rejection or throw from the second context rolls back the first. Rebuild is a new numbered generation: live commands write it too, backfill batches of at most 100 entities, capped by the context's byte-derived page bound, write only where the target is missing or older, verification precedes a one-write switch, and the old generation stays for 7 days. Cross-stream history views rebuild under the write pause, one gate document the pipeline reads for every command, with the generation's fence for background writers. Such a view is a `HistoryProjection` that folds events into its row; its paused rebuild closes the view's source stream types, so every writer of those streams is held whether or not it maintains the view, and its backfill pages each source's history. Restore closes writers through `MAINTENANCE_MODE`, restores the snapshot, runs four checkpointed checks (journal, domain, read model, receipt), reopens only on zero findings, and only then hands over to the durable profile's schedule rebuild. Audit is written inside the transaction and fails closed; diagnostics go through a sink that never throws. The first experiment is Orders and Inventory with `PlaceOrder`, `CancelOrder`, the order summary and the order allocation history view that Sc L2-6 builds; its six cost targets are constraint Specs, and its native runs authenticate ordinary clients with tokens signed by a fixture issuer that the deployment's environment variables name, the one named difference from production (E-13). Layer 3. An obligation is one recorded promise that commits with the business change and whose first dispatch is scheduled in the same transaction. The module lives in the parent as a library plus the `obligations` and `obligationRepairs` tables. A command's static reactions are the `createObligation` calls of its executor body, not a field of its declaration, and the record stores the one `Authority` shape of the actor and scope contract. A local reaction is a scheduled wrapper that runs the body as a nested mutation, so the effect and its completion commit together or the wrapper records the attempt after the body failed; a body refused by the write pause or a capacity policy is deferred without counting an attempt. A cron sweeper reads `_scheduled_functions`, whose `state.kind` is a lowercase literal, to tell a queued dispatch from a dead one, rearms at most five times, and escalates. External effects claim in a mutation, call in an action that makes exactly one provider call, and settle in a second mutation under one of four repetition policies; a stale worker's late evidence is kept and reconciled. Retention never erases unresolved work; restore starts with `OBLIGATIONS_DISPATCH` off and rebuilds schedules from the obligations after the transactional restore has reopened writers. All of it is gated on Probe 7 through the [`do-nothing-check`](specs/obligations/do-nothing-check.sdp.md) decision, made once for the durable profile and not per reaction, and every numeric default in the layer is provisional until the first experiment, which each Layer 3 Spec records against decision method rule 4. @@ -178,7 +178,7 @@ Every row of the doc's acceptance table is an example Spec that `refines` and `v ## The extension register -An extension is a design claim the doc does not make. Each one is three things together: a bullet that starts with `[extension]` and cites its E-number, and a `[non-blocking] Extension E-n` open question in the Intent of the Spec that owns it. The register lists every E-number once. "Owned by" names the Specs that carry the open question and the ruling; other Specs cite the number as a cross-reference without repeating the question. Every extension is a provisional ruling the owner confirms, changes or rejects; none is blocking, because each has a working default. E-15 to E-19, E-27 to E-29, E-48, E-49 and E-59 are unused; E-13 and E-39 were assigned by the integration round from the unassigned and the Package C range, and E-14 by review round 2. +An extension is a design claim the doc does not make. Each one is three things together: a bullet that starts with `[extension]` and cites its E-number, and a `[non-blocking] Extension E-n` open question in the Intent of the Spec that owns it. The register lists every E-number once. "Owned by" names the Specs that carry the open question and the ruling; other Specs cite the number as a cross-reference without repeating the question. Every extension is a provisional ruling the owner confirms, changes or rejects; none is blocking, because each has a working default. E-17 to E-19, E-27 to E-29, E-48, E-49 and E-59 are unused; E-13 and E-39 were assigned by the integration round from the unassigned and the Package C range, E-14 by review round 2, and E-15 and E-16 by slice S0. | E | Owned by | What the doc leaves open, and the option taken | |---|---|---| @@ -194,8 +194,10 @@ An extension is a design claim the doc does not make. Each one is three things t | E-10 | [`obligations.lifecycle-transitions`](specs/obligations/lifecycle-transitions.sdp.md), [`obligations.record-contract`](specs/obligations/record-contract.sdp.md) | The allowed transitions between the six states and who causes each; the `obligations` and `obligationRepairs` tables with seven and two indexes; a 64 KiB inline payload bound; ten late-evidence entries; two attention reasons beyond D13's four, `recoveryFailing` and `chainBound` | | E-11 | [`effects.claim-call-settle`](specs/effects/claim-call-settle.sdp.md) | The signatures of `claimAttempt`, `readForCall`, `callProvider`, `reconcileAttempt`, `settleAttempt`, `expireLease`; the `RepetitionPolicy` union with a fourth value `none` | | E-12 | [`constraints.events-stay-small`](specs/constraints/events-stay-small.sdp.md) | A 16 KiB payload bound measured by the journal's `append`, so a fold of 600 events since the latest baseline stays near 10 MiB under the 16 MiB read ceiling; the reserved `baseline` event is exempt and bounded by its stream type's `budgetBytes`; the 1 second query timeout binds first and the first experiment measures it | -| E-13 | [`platform.acceptance-contract`](specs/platform/acceptance-contract.sdp.md) | The native tier's identity comes from the local backend's admin key acting as an identity with the production issuer's name and the test subject, the mechanism S13's guide uses; parity for Sc L2-9 is code path, schema, concurrency and the authority model, and the identity source is the one named difference on every evidence record | +| E-13 | [`platform.native-harness`](specs/platform/native-harness.sdp.md) | The native tier's callers are ordinary clients carrying tokens the harness signs for a fixture issuer; a composition that runs natively reads its issuer, application ID and a data-URI key set from the deployment's environment variables, which reaches into the production `auth.config.ts`; parity for Sc L2-9 is code path, schema, concurrency, the authority model and Convex's visibility check; the issuer is the identity source and the one named difference on every evidence record. The admin key acting as an identity was dropped on 2026-10-01: it passes the visibility check for internal functions | | E-14 | [`platform.acceptance-contract`](specs/platform/acceptance-contract.sdp.md) | A fifth tier, the build tier: a compile of the fixture app with the change applied that must fail at `tsc` or at the registration check, run before the native suite with no backend; Sc L2-4 is its one scenario | +| E-15 | [`platform.native-harness`](specs/platform/native-harness.sdp.md) | The native harness: the admin key is for setup and reading, never for a caller under test; two compositions, fixture and production; faults only through what the fixture composition supplies, and no test hook in the shared libraries; assertions only on what a client or admin access can read, and a refusal shown with its own error, nothing stored and the refused thing existing; four controls (the backend process, the clients, the environment variables, fixture functions) and no clock or scheduler switch; the production-configuration run of Sc L2-9 is the scenarios routed to the production composition; a pass claim cites a record kept in the repository | +| E-16 | [`facts.probe-plan`](specs/facts/probe-plan.sdp.md) | How a probe is written and recorded: example Specs that verify the fact, bound values as the expectation written before the first run, a different first result recorded on the fact and the example then bound to what the release does, a run that must reach its boundary, sizes recorded and not asserted, and what a local backend cannot answer left open on the fact | | E-20 | [`kernel.decider-contract`](specs/kernel/decider-contract.sdp.md), [`kernel.domain-kernel`](specs/kernel/domain-kernel.sdp.md) | The concrete generics, `DomainEvent`, `DecisionActor` as the kernel's structural view of the actor, `DecisionContext { now, actor, facts }`, `DecideResult`, `Invariant`, `Transitions` and `transition`; the kernel imports nothing from Convex and nothing from the command family | | E-21 | [`kernel.outcome-model`](specs/kernel/outcome-model.sdp.md) | `Outcome`, `Rejection`, `CommittedOutcome`, `AffectedRef`; a committed outcome carries at least one event; the reserved codes `staleVersion`, `operationTooLarge` and `entityExists`, the last raised by the adapter for a create planned at expected version 0 whose stream exists | | E-22 | [`context.context-component`](specs/context/context-component.sdp.md) | One registered mutation per sanctioned operation via `defineOperation` with args `{ tenantId, actor, operation, input, facts }`; `contextId` as a code constant equal to the mount name; 256 streams, 800 written documents and 8 MiB of stream documents read or written per call, derived from each stream type's byte budget; `convex-helpers` as the component's dependency; the query and maintenance surface | @@ -233,7 +235,7 @@ An extension is a design claim the doc does not make. Each one is three things t ## Open questions for the owner -A `[blocking]` question in this corpus is one of two things. A deferral names the trigger that brings a Layer 4 to 6 Spec back. An assumed fact (F14, F15, F17) names the probe that would settle it. Every other open question is `[non-blocking]`, which means the design took a provisional reading and says so where it did. The rulings that matter most, grouped by where they come from. +A `[blocking]` question in this corpus is one of two things. A deferral names the trigger that brings a Layer 4 to 6 Spec back. An assumed fact (F17) names the probe that would settle it. Every other open question is `[non-blocking]`, which means the design took a provisional reading and says so where it did. The rulings that matter most, grouped by where they come from. The doc's own open questions: @@ -267,12 +269,12 @@ Beyond the plan's list, three product decisions the packages surfaced: - The late-payment policy of `StartCheckout`: reallocate, ask a person, or refund as a separate operation ([`start-checkout-example`](specs/processes/start-checkout-example.sdp.md)). - Whether an old process run is kept, migrated or blocked on a given deploy ([`workflow-processes`](specs/processes/workflow-processes.sdp.md)). -The seven probes are pending, not rulings. Each is recorded on the Specs that rest on it and on [`probe-plan`](specs/facts/probe-plan.sdp.md): Probe 1 (where the client guarantee ends, D6), Probe 2 (`ConvexError` data across a component boundary, F14, D7), Probe 3 (cost of a component call, F4, D2, D8), Probe 4 (whether transaction limits add up across nested calls, F13, D10), Probe 5 (reactivity and pagination of a parent query over a component query, F15, D8), Probe 6 (a backfill batch racing a live command; the migrations component, F17, D9), Probe 7 (what a restore leaves of scheduler, Workpool and Workflow state; the system table's states and retention, F12, F16, D13, D19). +Probes 1 to 5 ran on a native backend in slice S0 on 2026-10-01. Each result is recorded on its fact and on the Specs that rest on it, and [`probe-plan`](specs/facts/probe-plan.sdp.md) says what stays open: Probe 1 (the client guarantee holds across a restart, a closed client can lose a command, and the HTTP client does not retry, D6), Probe 2 (`ConvexError` data survives a nested mutation and a component boundary, F14, D7), Probe 3 (a component call costs about 2 ms more than a helper call on a local backend, with hosted cost and quota open, F4, D2, D8), Probe 4 (transaction limits are one budget across nested calls and components, F13, D10), Probe 5 (a parent query over a component query stays reactive and pinned pages stay contiguous, and the client hook loses rows after a capped split, F15, D8). Two are pending: Probe 6 (a backfill batch racing a live command; the migrations component, F17, D9) and Probe 7 (what a restore leaves of scheduler, Workpool and Workflow state; the system table's states and retention, F12, F16, D13, D19). Every open question in the corpus, by Spec, is recipe 20 of the Protocol's catalog, the open-question register. It lists the Specs that hold a blocking question first and reports the totals, so no table is kept here: ```sh -npx sdp q '' --root design --json +npx sdp q '' --json ``` ## ID changes @@ -301,7 +303,7 @@ Cross-package fixes made at the source Specs: - The captured facts D3 makes inputs had no path: `DecisionContext.facts` existed but no validator, context call or executor carried them. The operation's `args` take `facts`, the executor captures them and passes them on every context call, and the fingerprint excludes them. - The gate, the audit record and the diagnostic were required of every use case by Package D and absent from the pipeline's eleven steps; they now sit at steps 7, 10 and 11, each a no-op in a deployment without read models, closed gates or audited declarations. - The `entityExists` code had three owners. It is now the kernel's third reserved code, raised by the adapter's step 3 for a create planned at expected version 0 whose stream exists; the initial-state decision, the receipts rules and the L1-4 example say the same, and E-39 registers that the second submit is a rejection rather than an idempotent success. -- Pagination inside a component. The queries contract used the built-in `.paginate()`, which the components page says does not work in a component; `list` and `history` now use `paginator` from `convex-helpers`, the parent relays its page, the client hook is the one from `convex-helpers/react`, and Probe 5 is narrowed to contiguity across the boundary. +- Pagination inside a component. The queries contract used the built-in `.paginate()`, which the components page says does not work in a component; `list` and `history` now use `paginator` from `convex-helpers`, the parent relays its page, the client hook is the one from `convex-helpers/react`, and Probe 5 showed contiguity across the boundary and the hook losing rows after a capped split. - Byte budgets. Every bound on streams per call, list page, backfill batch, written documents and fold length is now derived from a stream type's byte budget or the 16 KiB payload bound so that the 16 MiB ceilings bind in bytes, not only in documents; the fold bound is 600 events with headroom, and a longer tail is refused rather than read. - A write pause or a restore no longer exhausts local reactions: the wrapper defers an attempt refused with `writePaused` or `capacity` without counting it, and `rebuildSchedules` runs only after the transactional restore has cleared `MAINTENANCE_MODE`. - The operation query path is `queries.operations.byOperation` everywhere (restore's receipt check and read-models' surface said `queries.order.byOperation`). @@ -345,7 +347,7 @@ Adopted from the Protocol on 2026-10-01, at the commit `package.json` pins: - Generics are written with real angle brackets inside inline code. The Unicode brackets the carrier once forced, 248 pairs on 114 lines of 29 Specs, are gone, so every pinned declaration can be pasted into source. - An unbound example below `ready` no longer warns. The expected validate verdict is 0 errors and 0 warnings. -- The `ready` floor reads `constrainedBy` and `decidedBy` as well as `refines` and `dependsOn`. The assumed facts F14, F15 and F17 state `scoped` behind a blocking question that names their probe, so no Spec resting on one can state `ready` until the probe has run. F16 stays `defined`: its states and retention are documented. +- The `ready` floor reads `constrainedBy` and `decidedBy` as well as `refines` and `dependsOn`. The assumed fact F17 states `scoped` behind a blocking question that names its probe, so no Spec resting on it can state `ready` until the probe has run; F14 and F15 did the same until their probes ran in slice S0. F16 stays `defined`: its states and retention are documented. - A deferral is a blocking open question that names its trigger, on the nine Layer 4 to 6 Specs. - The census sentence and the open-question table are no longer kept by hand. `sdp census` and recipe 20 derive them. - The extension register stays in this file. It is this project's policy, and `design/tools/check.py` keeps it honest. diff --git a/design/SESSIONS.md b/design/SESSIONS.md index c035d74..0f21d6f 100644 --- a/design/SESSIONS.md +++ b/design/SESSIONS.md @@ -24,19 +24,20 @@ The owner ruled on 2026-10-01 that the session takes tactical decisions on its o | Work | Who | Effort | |---|---|---| -| Orchestration, rulings, every Spec sentence, the check, commits | Fable 5.1, the main thread | high | -| Read-only scout before a unit; reviews that run things | `gpt-6-astra` | high | -| Bounded building from a Spec; mechanical edits from an exact text; the mechanical review lane | `gpt-6.1-sol` | medium | +| Orchestration, rulings, every Spec sentence, the check, commits, merges | Fable 5.1, the main thread | high | +| The design of a solution's key abstractions before an implementer writes it: interfaces as stubs, names, shapes, and exact text only where a reader sees it | Claude agent, Fable 5.1 | default | +| Building from a Spec, and folding in ruled findings | Claude agent, Opus 5.5 | default | +| Read-only scout before a unit; the behavior review lane, which runs things | `gpt-6-astra` | high | +| The mechanical review lane: every changed line, test and pinned value | `gpt-6.1-sol` | medium | +| Whole-branch review before a hand-over or a close | `gpt-6-astra` | xhigh | | Review of balance and of how a reader will take the text | Claude agent, Opus 5.5 | default | -| A design pass over interfaces that several Specs share | Claude agent, Fable 5.1 | default | -| Whole-branch review before a hand-over | `gpt-6-astra` | xhigh | -GPT capacity is the abundant side, so reading, checking and building go there. Claude agents are the scarce side: start one only for a role in the table. Design judgment and the project's vocabulary stay with Claude. The mechanics of reaching GPT models, briefing each family and calibrating a reviewer are in the orchestration guide, and this project's commands, brief fragments and job log are in its notes file. `AGENTS.md` names both. +Claude models write and GPT models review. The owner ruled it on 2026-10-01, after slice S0 had tried both: Opus 5.5 is the primary coder, and GPT reviews run on frozen copies beside the write lanes, where they cost no time. Design judgment and the project's vocabulary stay with Claude. Three earlier rulings of the same day stand. A Fable agent, not the main thread inside a brief, designs a solution before any implementer writes it. Taste review goes to a Claude model only, and solution design to Fable only. And a job is sized to its model: sol takes what an Opus agent would take, astra what a Fable agent would, in width and in depth. A GPT job is started with the launcher the orchestration guide describes, not through a forwarding agent, and its session id is kept so that a job that dies can be resumed. The mechanics, the briefing of each family and the calibration of a reviewer are in the orchestration guide, and this project's commands, brief fragments and job log are in its notes file. `AGENTS.md` names both. Four rules from the stop of 2026-09-30, when ten parallel Fable agents hit the usage limit and one left 109 unreported edits: - One unit per session. -- One write lane. A second writer shares the tree only when the two briefs split the files and each names the other's files as forbidden. +- One write lane per tree. A second writer gets its own git worktree and files the first does not touch, and the main thread merges its diff. Two writers share one tree only when the two briefs split the files and each names the other's files as forbidden, and then nothing is committed or checked until both are done. - The ledger is written ahead: each finding and each status before the next begins. - Every delegated job's report is saved to a file the moment it arrives. @@ -52,13 +53,14 @@ Four rules from the stop of 2026-09-30, when ten parallel Fable agents hit the u One slice of the build, as `STATE.md` cuts them. `python3 design/tools/check.py --slice S1` lists the findings it takes. -1. **Scout.** One read-only astra job reports, per finding of the slice: the Spec that owns the concept, every other place that restates it, found by searching the whole corpus for each name and signature, and each conflict with the doc. Its report is a list of leads. -2. **Settle.** For each ledger finding and each scout lead, look for what would refute it, then trace it to the scenario it breaks. Rule one of three: fix on paper now, let the build decide and say what the build must show, or the owner. A `fix` field in the ledger is a lead; five were found wrong on 2026-10-01. +1. **Scout.** One read-only astra job reports, per finding of the slice: the Spec that owns the concept, every other place that restates it, found by searching the whole corpus for each name and signature (recipe 23 finds keyed entries by name), and each conflict with the doc. Its report is a list of leads. +2. **Settle.** For each ledger finding and each scout lead, look for what would refute it, then trace it to the scenario it breaks. Rule one of three: fix on paper now, let the build decide and say what the build must show, or the owner. A `fix` field in the ledger is a lead; five were found wrong on 2026-10-01. Recipe 21 lists what a Spec rests on, with each target's rung and floor. 3. **Author.** Change the Spec that owns the concept, then every Spec that restates it. A Spec that names another in prose declares the relation. Fix what the slice cannot be built without and leave the rest open with the note that the build decides. Set each finding's `status` and `note` before starting the next. -4. **Build.** One sol job per Spec, or one job for several when an exact design exists. The job writes code and tests, binds each example it realizes to its test, changes no Spec and no stated readiness, and lists everything the Spec did not say that it had to decide. -5. **Commit, then review a frozen copy.** Three lanes read the commit, each with one criterion. Sol reads every changed line, test and pinned value. Astra runs the code against the Spec and looks for consequences elsewhere. An Opus agent asks whether the change added mechanism the scenario does not need and how a reader will misread it. A number in a Claude finding is re-run before anyone acts on it. -6. **Rule on each gap.** Code wrong becomes one numbered fold-in job. Spec wrong becomes a Spec edit by the main thread and a ledger entry. A platform decision goes to the owner queue with its provisional reading. -7. **Review the fixes.** One more read-only pass over the last fold-in. A fix is new work. +4. **Design.** A Fable agent reads the Specs the slice builds and designs their key abstractions, not every line: the interfaces later work builds on, as signatures without bodies, the data shapes, the names, and each decision with its reason. It proposes them as `## Design` entries and rules for those Specs, in one short design file that also holds what is not a Spec entry: toolchain answers, work packages, spike facts and gaps it found in the Specs. The brief gives it a time bound, about half an hour of work, and says that a spike stays small. It changes no file in the repository. The main thread rules on each proposed entry and writes it into the Spec, so the Spec is what bounds the design and what the implementer builds from. Anything the designer built while designing is kept and handed to the implementer as a patch, each file marked proven with its tier, drafted or not started. +5. **Build.** One Opus agent per Spec, or one for several, building from the Spec's Design entries and the design file. The agent writes code and tests, binds each example it realizes to its test, gives code built from a Spec a `codeAnchor` that `satisfies` it once the code has bodies, changes no Spec and no stated readiness, and lists everything the design did not say that it had to decide. It runs codegen and every tier itself, the native one included, and reports the output. A second builder works in its own worktree. +6. **Commit, then review a frozen copy.** The reviews run while the next thing is built. Three lanes read the commit, each with one criterion. Sol reads every changed line, test and pinned value. Astra runs the code against the Spec and looks for consequences elsewhere. An Opus agent asks whether the change added mechanism the scenario does not need and how a reader will misread it. A number in a Claude finding is re-run before anyone acts on it. A reviewer proposes mutations the builder did not try, and the main thread runs the native ones. +7. **Rule on each gap.** Code wrong becomes one numbered fold-in list for an Opus agent, the one that built the code when it can still be continued, designed first when it reshapes an interface. Spec wrong becomes a Spec edit by the main thread and a ledger entry. A platform decision goes to the owner queue with its provisional reading. +8. **Review the fixes.** One more read-only pass over the last fold-in. A fix is new work. Done when the slice's tests pass at the tier each claims, the check prints `OK`, every finding the slice took has a status other than `open` or a note saying what the build still has to show, and every gap is ruled. @@ -104,12 +106,23 @@ The owner rules. The session applies each ruling at the Spec that owns the quest 1. `python3 design/tools/check.py` prints `OK`. 2. Rewrite `STATE.md` under its existing headings, with the check's summary lines copied in. It describes the present. Git holds the history. 3. Add each delegated job to the run log in the project notes: model, effort, scope, time, outcome. -4. Commit as `design: ` or `build: `, on the unit's branch, push it and open a pull request. `STATE.md` says who merges. +4. Commit as `design: ` or `build: `, on the unit's branch, and push it. CI runs on every push. While the owner's mandate of 2026-10-01 holds, the session fast-forwards `main` to the branch once CI is green on the last commit, at the close and at any seam where the branch is green, and opens no pull request: the owner found on 2026-10-01 that a pull request per unit slows the design and build cycles at this stage. `STATE.md` says whether both still hold. ## Consensus Consensus holds for Layers 0 to 2 when the first experiment passes on a native backend and each of the five lenses has `approve` as its latest verdict covering `HEAD`. `STATE.md` then names `owner` as the next unit. Layers 3 to 6 are written from what the experiment shows and reach consensus the same way afterwards. +## Handing over inside a unit + +A build slice is too long for one conversation. The main thread hands over to a fresh main thread at a seam, before its own context gets in the way: after a commit, with no delegated job running and every report saved. + +1. Reach the seam. Wait for or stop every delegated job. A Claude agent dies with the conversation and leaves no report. A GPT job started with the launcher survives it, and its report, events and session id are in the job directory the notes name. +2. Commit what is in the tree on the unit's branch, as work in progress if it is not reviewed, and say so in the message. +3. Write the handover note beside the unit's reports, in the folder the project notes name. It says what is committed, what each tier showed and at which commit, which findings are ruled and where the rulings are, what is drafted and not applied, the next steps in order, and what waits for the owner. It lists every instruction the owner gave during the conversation, in the owner's words. +4. Put a short "In progress" block at the top of `STATE.md` that names the unit, the seam and the note. The rest of `STATE.md` waits for the close. +5. Start the next main thread from this one, over cmux. Two panes of one workspace take turns: the thread that hands over goes idle, and the other pane carries the work until it hands back. `cmux identify --json` names this pane, and the owner or `cmux list-panes` names the other. Read the other pane with `cmux read-screen --surface ` and make sure of three things before prompting it. Claude Code is running there in the repository; if it is not, start it with `cmux send`. The conversation is clear, with an empty context in its status line; if it is not, send `/clear`. A cleared conversation is required: a main thread that inherits a summary of this one inherits its weight. The model is Fable 5.1 at high effort, as its banner and status line show. Then send the prompt as one paragraph with `cmux send --surface `, because a newline submits, submit it with `cmux send-key --surface enter`, and read the screen once more to see that it started. The prompt names the unit, the note, and the files to read first. +6. The next main thread reads `AGENTS.md`, `STATE.md`, this file and the note, runs the check, and confirms with `git status` and `git log` that the tree is what the note says before it does anything else. + ## When a session runs short -The ledger is at most one finding behind. Stop, write `STATE.md` with what is done and what is not, and let the next session open with `recover`. +The ledger is at most one finding behind. Stop, write `STATE.md` with what is done and what is not, and let the next session open with `recover`. Hand over as above when there is still room to write the note. diff --git a/design/STATE.md b/design/STATE.md index e5961b2..17e60ee 100644 --- a/design/STATE.md +++ b/design/STATE.md @@ -1,69 +1,90 @@ # State of the design corpus -Written on 2026-10-01 at the close of the `adopt` unit. `SESSIONS.md` says how to use this file. Every close rewrites it. +Written on 2026-10-01 at the close of slice S0. `SESSIONS.md` says how to use this file. Every close rewrites it. ## Measured at close ``` -168 specs · 5 packs · 0 anchors → 173 nodes · 881 edges (0 errors, 0 warnings) +187 specs · 5 packs · 22 anchors → 214 nodes · 959 edges (0 errors, 0 warnings) validate: 0 errors · 0 warnings; readiness divergence: [] -open questions: 67 Specs, 129 questions, 12 blocking; extensions registered: 48 -stated readiness: {'defined': 156, 'scoped': 12} -ledger: 231 findings, {'fixed': 111, 'open': 116, 'partially-fixed': 4} -open findings by slice: {'L3': 22, 'P': 18, 'S0': 6, 'S1': 17, 'S2': 7, 'S3': 1, 'S4': 30, 'S5': 15} -corpus digest: 6f0b395c30948893 +open questions: 67 Specs, 122 questions, 10 blocking; extensions registered: 50 +stated readiness: {'defined': 177, 'scoped': 10} +ledger: 245 findings, {'fixed': 125, 'open': 111, 'owner': 1, 'partially-fixed': 8} +open findings by slice: {'L3': 22, 'P': 18, 'S1': 17, 'S2': 8, 'S3': 1, 'S4': 30, 'S5': 15} +corpus digest: 4a84dfa83427d329 protocol: file:vendor/libar-dev-software-delivery-protocol-0.0.0-5993da7.tgz ``` -The default branch is `main`, published at `github.com/libar-dev/libar-platform`. The owner fast-forwarded it to the corpus on 2026-10-01. Each unit works on its own branch, named for the unit, and opens a pull request. The session commits and pushes its branch; merging to `main` stays with the owner. +The default branch is `main`, published at `github.com/libar-dev/libar-platform`. Each unit works on its own branch, named for the unit, so that `main` stays green. The owner gave the session a mandate on 2026-10-01 to merge at this early stage, and said the same day that a pull request per unit was not a good idea while the design is iterated quickly. So from slice S1 on there is no pull request: CI runs on every push, and the session fast-forwards `main` once CI is green on the last commit. Slice S0 was the one unit merged through a pull request. The mandate has no stated end: ask when a release or an outside user appears. ## Where the work stands -Review round 3 is complete for all five lenses at `f44cad4`. No lens approved. It left 116 open findings: 1 blocker, 36 majors, 79 minors. Three round-2 fixes are `partially-fixed`, each carried by a round-3 finding. +Slice S0 is built. The repository now holds code beside the design: a native test harness (`harness/`), a fixture Convex app with one component (`fixture/convex/`), four test tiers (`tests/`), a codegen script and a watch mode (`scripts/`), CI, and evidence records (`evidence/`). The root `README.md` says how to run each. -The owner changed the plan on 2026-10-01. A final design cannot be reached by design work alone, so the design is now finished by building it, one slice at a time. The numbers agree. Each fix round added names that the next round found defects in. A blind GPT read of one area found about 15 defects that three review rounds had not recorded. And of the 37 open blockers and majors, a read-only analysis predicts that code would catch 28: 6 at the compiler, 2 in a pure test, 16 in `convex-test`, 3 on a native backend, 1 in a measurement. The other 9, and 49 of the 79 minors, are judgment that no code catches, so review continues beside the build. +What passed, and at which tier, on 2026-10-01 with backend release `precompiled-2026-09-28-5c7cb5b`, `convex` 1.46.0, `convex-helpers` 0.1.124, `convex-test` 0.0.60: -Every open finding carries a `slice` and a `detector` field in the ledger. The detector is that analysis's prediction of the cheapest thing that would catch the finding. Nothing was compiled or run to produce it. +- Compiled, pure test and `convex-test` tiers at `61c3879`: 14 files, 80 tests. Typecheck, lint and format pass. +- Native tier at `61c3879`, on a clean tree, macOS on Apple silicon: 21 files, 31 tests. The record is `evidence/native-20261001T195108Z-61c3879-e2da54b9-e845-4fa6-bc1d-2449482970dc.json`. +- CI on Linux at `bf80ec4`, which is `61c3879` plus that record: both jobs pass, the native one included. CI also passed at each of the three fold-ins before it. +- Thirty-seven native mutations proposed by reviewers were run across three review rounds. Twelve left their test green when first run. The three of the first round were ruled and fixed in the first fold-in. The six of the second round and two of the third were rerun after their fixes and turn their tests red. The last one is no finding: a page size no example binds. + +What each probe showed on the native tier. The fact Specs hold the details and the measured sizes. + +- Probe 1 (F5). Across a backend kill and restart on the same storage every mutation's promise stayed pending through the kill, resolved after the restart and left one row, whether the kill came before the commit or after it. A closed client left its row once or not at all. `ConvexHttpClient` sent one request per call, and a caller's retry left two rows. +- Probe 2 (F14, now `probed`). `ConvexError` data reached the parent's catch and the client unchanged through a nested mutation and through a component boundary. The child's write rolled back and the parent committed. An ordinary `Error`'s added property did not arrive. +- Probe 3 (F4). Fifty reads took about 3 ms through a helper, about 50 ms nested and about 80 ms through a component, with a factor of two between runs. The backend's own `documentsRead` metric counted one document per read on all three paths, in a query and in a mutation. Local time only: the quota half needs a hosted deployment. +- Probe 4 (F13). 16 MiB read and 16 MiB written are each one budget across a nested call and a component call. A stack of 9 functions commits and 10 fails with "Cross component call depth limit exceeded". +- Probe 5 (F15, now `probed`). A parent query over a component query stays reactive. Pinned pages stay contiguous. A capped page comes back as `SplitRequired` and both halves hold every row. The `usePaginatedQuery` hook of `convex-helpers/react` shows 40 of 45 rows after a capped split. +- Not a probe: `ctx.auth.getUserIdentity()` inside a component returned the caller's identity, against the documentation. `convex-test` 0.0.60 returns null. + +What changed in the Specs during S0, and why: + +- `spec:platform.native-harness` is new. It took extensions E-13 and E-15 out of the acceptance contract, because the doc says what native tests prove and not how a test gets an identity, what the fixture holds, how a fault enters or what a test controls. Its Design holds the harness's interface as stubs. +- The first reading of E-13, the admin key acting as an identity, was dropped: such a caller passes Convex's visibility check for internal functions. A fixture issuer replaced it. One example shows why. +- `spec:facts.probe-plan` carries extension E-16: how a probe states its expectation, what a later different run is, and that a size is recorded and not asserted. +- Eighteen examples are new or rewritten, four for the harness and fourteen for probes 1 to 5, each bound to a native test. Review rounds reworded several of them, each time because a review or a mutation showed the test proved less than the text said. +- F14 and F15 state `defined` and are `probed`. F13, F5 and F4 carry what their probes showed. F11 carries an open question for the owner. +- Fourteen findings entered the ledger with `round: "S0"`. The slice has no `open` finding of its own. + +Review of S0. The foundation and the first fold-in each had three lanes (sol mechanical, astra behavior, Opus taste). The second fold-in was read by astra at xhigh over the whole branch, the watch mode by sol, and the third and fourth fold-ins by astra and sol again. Every review of a fold-in found defects, and each time some were ones the fold-in itself had introduced, in a cleanup or failure path the fix had added: eight findings on the second fold-in, eleven on the third, eight on the fourth. The probes were confirmed closed at the third. What the fourth review found was in the handling of failed cleanup, and the fifth fold-in closed it. The slice then stopped on tactical decision 19. A review of the fifth fold-in was started as the slice closed and gates nothing: its report is `jobs/review-ai-fold-in-5.report.md` in the S0 folder the project notes name, and the thread that opens S1 rules on it first. + +Fourteen findings of slice S0 are `fixed` and carry `unreviewed: true`: no lens has confirmed them in the Spec text. They stay flagged until a `review` unit reads them. + +Review round 3 stands as it was for the rest of the corpus: no lens approved, and its open findings are sorted by slice and by the cheapest thing that would catch each. A final design cannot be reached by design work alone, so the design is finished by building it, one slice at a time. | Slice | Builds | Open findings | Blocker, major, minor | |---|---|---|---| -| S0 | Repository layout, toolchain, fixture app, test tiers; probes 2 to 5 | 6 | 0, 3, 3 | | S1 | Layers 0 and 1, thin: deciders, kernel types, one context component, journal and adapter, command pipeline, receipts, actor and tenancy, the minimal grant bootstrap | 17 | 0, 4, 13 | -| S2 | Layer 2, thin: `PlaceOrder` over Orders and Inventory, a legal way to create stock, the summary written in the command with its first active generation, queries and lists | 7 | 1, 3, 3 | +| S2 | Layer 2, thin: `PlaceOrder` over Orders and Inventory, a legal way to create stock, the summary written in the command with its first active generation, queries and lists | 8 | 1, 4, 3 | | S3 | Measurement: 1, 10 and 100 lines, without and with contention. Repeated after S4 and S5 | 1 | 0, 0, 1 | | S4 | Generations and rebuild, the history view and the write pause, with the tenant list, operator entry points and gate audit the rebuild uses; Probe 6 | 30 | 0, 14, 16 | | S5 | Baseline migration, restore, the rest of operators and audit, maintenance jobs; the transactional part of Probe 7 | 15 | 0, 7, 8 | | L3 | Layers 3 to 6, written from what the experiment shows | 22 | 0, 5, 17 | | P | Polish with no effect on what is built: citations, markers, stale sentences, README | 18 | 0, 0, 18 | -Aggregate and cross-context projections sit in S4 and are not part of the pinned experiment. Remove an unsupported generic promise before designing a helper no view needs. - -What the `adopt` unit did, on the Protocol at `5993da7` (pull request 26): - -- The Protocol is a pinned dependency in `package.json`, from a tarball under `vendor/`, because the commit hash installs no CLI. `docs/sdp-feedback.md` has the reason and the rest of the feedback. -- Generics use real angle brackets: 248 pairs on 114 lines of 29 Specs. -- Validate expects 0 warnings. Five checks left `tools/check.py`. -- F14, F15 and F17 state `scoped` behind a blocking question naming the probe. Nine deferred Specs carry a blocking question naming the trigger. -- The README's census sentence and open-question table are gone. `sdp census` and recipe 20 derive them. -- OQ4 is ruled: the experiment lives in this repository. +Eight more findings are `partially-fixed`, each carried by a later finding or waiting for its slice. Aggregate and cross-context projections sit in S4 and are not part of the pinned experiment. Remove an unsupported generic promise before designing a helper no view needs. ## Next unit -`slice` S0. +`slice` S1. Before its scout, rule on the review of S0's fifth fold-in (see "Where the work stands") and fold in only what a healthy run or an ordinary failure would hit. + +Decisions S1 needs on paper before its code: -Four decisions come before any code, all from fix unit 7's findings. Settle them on paper first: +- Who creates the first grant and changes grants (`r3-completeness-grants-have-no-writer`). +- One source for tenant scope (`r3-sdp-tenant-scope-passed-two-ways`). +- Drop the standing command registry unless a reader needs it (`r3-fidelity-command-registry-standing-without-a-reader`). +- Whether a Spec may say `ctx.auth` is unavailable in a component, given what S0 observed (`s0-f11-documented-unavailable-observed-available`, status `owner`). +- Whether callers of `ConvexHttpClient` must supply a request key on the public entry, given Probe 1 (the question is on D6 and on `spec:command.idempotency-and-receipts`). -1. What time and interruption controls the native tier has, and how a run with adjusted time differs from production (`r3-architecture-native-tier-cannot-move-time-or-interrupt`). -2. The fixture composition, its fault seams, and the production acceptance path that stays unmodified (`r3-completeness-test-seams-and-fixture-app-undefined`). -3. Admin setup access against ordinary-client authority in visibility tests (`r3-convex-admin-acting-identity-bypasses-visibility`). The earlier platform's native tests sign ordinary JWTs from a fixture issuer with a data-URI key set, and that passed on its native backend on 2026-09-27. -4. Which acceptance alternatives the harness must cover, so it does not prove only the convenient cases (`r3-completeness-or-rows-bound-to-one-point`). +What S0 leaves for S1's opening: -Then S0 lays out the repository, pins the toolchain and runs probes 2 to 5. Each probe becomes an `example` Spec that `verifies` its fact, bound to its test, which is the Protocol's home for a checked statement. Probe 1 runs before S1 relies on receipt boundaries. +- Start `npm run dev` in a background shell before the first build job. It keeps `fixture/convex/_generated` current while agents write functions. +- `tsconfig.json` sets `moduleSuffixes: [".d", ""]` and `erasableSyntaxOnly` for the whole repository. The library S1 starts meets both. Decide then whether the library gets a `tsconfig` of its own. +- `r3-completeness-test-seams-and-fixture-app-undefined` and `r3-completeness-or-rows-bound-to-one-point` are `partially-fixed`: the harness side is done, and the part that needs a kernel waits for S1. Decisions each later slice needs on paper before its code: -- S1: who creates the first grant and changes grants (`r3-completeness-grants-have-no-writer`); one source for tenant scope (`r3-sdp-tenant-scope-passed-two-ways`); drop the standing command registry unless a reader needs it (`r3-fidelity-command-registry-standing-without-a-reader`). -- S2: stock created through a domain operation (`r3-architecture-example-domain-cannot-create-stock`); the transition that makes the first summary writable (`r3-completeness-first-generation-has-no-bootstrap`). +- S2: stock created through a domain operation (`r3-architecture-example-domain-cannot-create-stock`); the transition that makes the first summary writable (`r3-completeness-first-generation-has-no-bootstrap`); what a list uses in place of the helper hook that loses rows after a capped split (`s0-helper-hook-loses-rows-after-a-capped-split`). - S4: order-independent cross-stream folds and whether deletion keeps fold memory (`r3-architecture-history-fold-order-and-deletion`); who owns the tenant list (`r3-architecture-tenant-list-undefined`); operator authority and entry points (`r3-completeness-operator-entry-points-undefined`); refusal or a paused catch-up for history rollback (`r3-architecture-history-generation-rollback-unpaused`). - S5: history-reading migrations off the command path unless the owner changes D3 (`r3-fidelity-migrate-on-load-replays-history-against-d3`); whether paused migrations are supported (`r3-fidelity-baseline-sweep-and-the-pause-contradict`); reopening authority, failure scope and drill cadence as explicit extensions (`r3-fidelity-restore-policies-cited-to-the-doc`). @@ -71,34 +92,52 @@ Eighteen findings are listed in the sorting report as wasted effort on paper bef ## Tactical decisions taken on 2026-10-01 -The owner accepted these on the session's recommendation. Any of them can be reopened. +The owner accepted the first nine on the session's recommendation. The session took the rest during S0. Any of them can be reopened. 1. Slices replace the eight fix units of the previous plan. 2. A slice is built on Specs at `defined` with no open blocker inside the slice. The owner states `ready` after the slice passes. 3. Layer 3 and later wait for the experiment. Their 22 findings are not fixed on paper first. -4. ESM only, Node 22 or later, one supported compiler. Dual builds, compatibility matrices and publishing wait for a first release. +4. ESM only, Node 24, one supported compiler. Dual builds, compatibility matrices and publishing wait for a first release. 5. npm, with one `package.json` at the repository root, as the Convex component template has it. 6. The main thread settles a GPT finding before it enters the ledger. An `unreviewed` flag still waits for its lens. 7. A lens verdict for `sdp`, `convex` and buildability may rest on a GPT read once that reviewer has caught a planted defect in a Spec. -8. Sessions delegate. `SESSIONS.md` is rewritten for it. -9. Two Protocol idioms are adopted later, when their moment comes. An extension becomes a decision Spec when the owner takes it up, and until then the E-register stays. A probe becomes an example that verifies its fact when S0 builds it. +8. Sessions delegate. Since the owner's ruling during S0, Claude models write and GPT models review: `SESSIONS.md` has the role table. +9. Two Protocol idioms. An extension becomes a decision Spec when the owner takes it up, and until then the E-register stays. A probe is an example that verifies its fact, which S0 did for probes 1 to 5. +10. The fixture's component is mounted as `annex`. A fixture context gets its own name when a slice adds it. +11. `moduleSuffixes: [".d", ""]` and `erasableSyntaxOnly` hold for the whole repository, so that Node 24 can run the harness's TypeScript by stripping its types, and so that the compiler reads the declarations of `convex-helpers` and not the source it ships. +12. Evidence records are kept by hand: every native run writes one under `evidence/runs/`, which git ignores, and the record of a run a Spec or a commit cites is copied to `evidence/` in a commit of its own. +13. A design pass designs the key abstractions as proposed Spec entries, with a time bound, and anything it built is handed on as a patch. +14. A GPT job is started with the launcher beside the orchestration guide, on a frozen copy, and its session id is kept. Builders work in one worktree each. +15. `npm run dev` is the watch mode: one disposable backend with `convex dev` bound to it. +16. The day-one order "component codegen before the build" is met by two things together: the generated files are committed, so the `checks` job compiles what the repository holds, and the `native` job runs codegen and fails when the committed files differ. +17. Code built from a Spec carries a `codeAnchor` that `satisfies` it. The harness has one, so the Protocol's drift alarm (recipe 2) lists what is built and does not state `ready`. +18. No pull request per unit. A unit has its branch, CI runs on every push, and the session fast-forwards `main` at the close or at a green seam. +19. A slice stops reviewing its fixes when a review finds no defect on a path a healthy run or an ordinary failure takes. What is left, a hole in the handling of a failed failure path or a test that would not catch a removal, is tracked under Leads and goes to the next slice. ## Owner queue -Platform decisions. None blocks S0. - -1. The 129 open questions (recipe 20), the twelve ambiguities of `PLAN.md` 11 and three product decisions listed in `README.md`. -2. The seven probes. None has run. The earlier platform's evidence partly answers probes 1, 2, 3, 4 and 7 and does not touch 5 or 6. -3. Rulings the round-3 findings ask for: - - D6 and the local wrapper: whether a derived command issued inside the reaction wrapper needs a receipt (`r3-fidelity-derived-command-receipt-question-not-on-a-spec`). - - D3 and a history-reading migration (`r3-fidelity-migrate-on-load-replays-history-against-d3`). - - Restore policy: whether a failed check keeps every writer out with no override, and how often the drill runs (`r3-fidelity-restore-policies-cited-to-the-doc`). - - The `[extension]` marker: `PLAN.md` 6.5 asks for it on every extension bullet and 167 of 903 carry it (`r3-fidelity-extension-marker-applied-to-a-fifth-of-citations`). - - D2's premise: Convex documents a commit timestamp that increases in commit order (`r3-convex-commit-timestamp-not-weighed`). -4. What code cannot choose, each at the slice named above: the grant bootstrap trust model, deployment-wide operator authority, tenant creation during a scan, the acceptable length of a write pause, history-deletion semantics, the restore override policy. -5. The maximum order size and the latency and throughput targets, before S3 benchmarks. -6. Whether the doc's statuses follow what round 3 found documented: F15's reactivity and most of Probe 4. -7. A license. The repository is public and has no license file. +Platform decisions. None blocks S1's opening, and the first five bear on S1's paper decisions. + +1. `ready` on `spec:platform.native-harness`. It is built, its tests pass, and recipe 2 lists it as waiting. +2. E-13, both halves: the fixture issuer for tests, and what it means for production's `auth.config.ts`, which must then be a `customJwt` provider whose trust root the deployment's environment names. +3. E-15: the harness rules, the reading of Sc L2-9 as the scenarios routed to the production composition, and evidence records kept in the repository. +4. E-16: how a probe states its expectation and records a different result. +5. F11: documented as unavailable in a component, observed as available. And D6 after Probe 1: whether a caller of `ConvexHttpClient` must supply a request key. +6. The doc disagrees with what the probes showed in eleven places. The list is in the S0 reports the project notes name (the facts-pass scout, section 3). Only the owner edits the doc. +7. Probe 3's quota half needs a hosted deployment. OQ1 has its first number: a component call costs 1.5 to 2.4 ms more than a helper call on a local backend, across three runs on one machine. +8. The 122 open questions (recipe 20), the twelve ambiguities of `PLAN.md` 11 and three product decisions listed in `README.md`. +9. Probes 6 and 7 have not run. They sit in S4 and S5. +10. Rulings the round-3 findings ask for: + - D6 and the local wrapper: whether a derived command issued inside the reaction wrapper needs a receipt (`r3-fidelity-derived-command-receipt-question-not-on-a-spec`). + - D3 and a history-reading migration (`r3-fidelity-migrate-on-load-replays-history-against-d3`). + - Restore policy: whether a failed check keeps every writer out with no override, and how often the drill runs (`r3-fidelity-restore-policies-cited-to-the-doc`). + - The `[extension]` marker: `PLAN.md` 6.5 asks for it on every extension bullet and about a fifth carry it (`r3-fidelity-extension-marker-applied-to-a-fifth-of-citations`). + - D2's premise: Convex documents a commit timestamp that increases in commit order (`r3-convex-commit-timestamp-not-weighed`). +11. What code cannot choose, each at its slice: the grant bootstrap trust model, deployment-wide operator authority, tenant creation during a scan, the acceptable length of a write pause, history-deletion semantics, the restore override policy. +12. The maximum order size and the latency and throughput targets, before S3 benchmarks. +13. `AGENTS.md` names two files by their path on the owner's machine, in a public repository. A reviewer flagged it. The section is the owner's. +14. A license. The repository is public and has no license file. +15. Four proposals on how sessions are run, in the orchestration guide under "Proposals not yet ruled". ## Leads @@ -122,14 +161,17 @@ A session confirms or drops each. None is a finding yet. - A fence does not limit how many batches are scheduled. 2. Five `fix` fields in the ledger are wrong as written: a timestamp-only rebuild cursor skips ties; `floor(4 MiB / 64 KiB)` is 64 and payload size is not row cost; transaction metrics are `{ used, remaining }` behind an async call; Workpool 0.4.7 does not hand `onComplete` the final attempt count; a Node handler needs its own `"use node"` action file. 3. Two Convex facts no Spec covers. Nested calls draw on a system-operation time budget that byte and document bounds do not see: 605 calls used 14.8 s of a transaction that failed at 15.1 s on the earlier platform's native backend, in test mode, on 2026-09-26. And `convex-helpers` checks `maximumBytesRead` after it has read the row. -4. F11 as stated. One of the earlier platform's tests observes the parent's identity inside a component on its pinned native backend. Recheck before a Spec says `ctx.auth` is unavailable there. -5. Convex ships `getConvexSize` and `getDocumentSize`. They answer the two byte-bound findings of S1. +4. The local backend refuses writes past 4 MiB a second. That bounds what slice S3 can measure on it, and a throughput number from it states the limit. +5. Convex ships `getConvexSize` and `getDocumentSize`. They answer the two byte-bound findings of S1. And `ctx.meta.getTransactionMetrics()` counts documents read and written across nested and component calls, in queries and mutations: S0 used it in two probes, and it is a cheap way for S1 to measure what a command reads. 6. Fifteen places where the design or a proposed fix repeats a shape the earlier platform regretted, each with the Spec line: progress kept on a row every command reads, work repeated per entry or per boundary, a stored maximum version taken as proof of coverage, a closed state with no route out, bookkeeping on the healthy path with no bounded lifetime, nested command calls where a helper would do, a recovery scan blocked by healthy work, registries with no reader. -7. Nine type names used in pinned signatures and declared by no Spec: `Journal`, `GetArgs`, `ListArgs`, `HistoryArgs`, `WriteBaselineArgs`, `WriteBaselineResult`, `DiagnosticSink`, `AuditRecordInput`, `BatchCursor`. +7. Nine type names used in pinned signatures and declared by no Spec: `Journal`, `GetArgs`, `ListArgs`, `HistoryArgs`, `WriteBaselineArgs`, `WriteBaselineResult`, `DiagnosticSink`, `AuditRecordInput`, `BatchCursor`. Recipe 23 finds where each is used. 8. `dispatchId` on the obligation row is `v.id("_scheduled_functions")`. Nothing shows whether such an ID validates after a restore into another deployment. It belongs in Probe 7. 9. The mention audit (recipe 22) gives 78 unbacked pairs, 50 of them declared from neither side. Each slice checks the ones in its families. +10. Using the Protocol more, from a side thread's read on 2026-10-01. A local query for the build backlog at `defined` (recipe 1 reads stated `ready`, which no Spec here has before its slice passes; the same question at `defined` with a clear floor gave 80 Specs). A Pack per slice, so that "which Specs does S1 build" is a graph read. `check.py` counting open questions through recipe 20 in place of its own expression. The carrier grammar restated in `PLAN.md` 6.2, which can drift from the authoring skill's table. +11. Small things in the harness that no finding carries. The sweep signals by process id after reading the process table, and the Spec says the interval is not closed. A `convex dev` child killed with SIGKILL leaves a temporary directory behind, which a `TMPDIR` inside the watch mode's own home would remove. The scripts' hand-over of the interrupt to executable resolution is tested by reading their source. The admin read's page size of 10 in the shared-limits helper is not needed for the read to succeed. Inside a Codex sandbox `ps` cannot inspect other processes, so the sweep tests fail there and pass outside it. +12. Left by the last review of S0 on purpose. Redaction of a failed child's output scans a character at a time: 901 ms and about 725 MiB at peak for a 16 MiB output, where the version before the overlapping-secret fix took 3 ms. No pure test covers the accepting path of the download's hash check; it needs a real archive as a fixture, and the path runs for real whenever the cache is cold. Four mutations survive with no test: the interrupt not passed to `unzip`, the SQLite-path match dropped from the ownership check, SIGTERM mapped to SIGINT in `scripts/dev.mjs`, and a global teardown that does nothing. The test that both scripts hand their interrupt to executable resolution parses their source, and a shadowed variable or a later spread that overrides the step passes it. A failed amendment of a run record leaves a `.next` file beside the whole record. -The reports behind leads 1 to 6 are outside the repository, because they quote a private repository. The project notes file named in `AGENTS.md` lists them. +The reports behind leads 1 to 3 and 6 are outside the repository, because they quote a private repository. The project notes file named in `AGENTS.md` lists them, with the reports, briefs and rulings of S0. ## Inputs diff --git a/design/reviews/consensus-ledger.json b/design/reviews/consensus-ledger.json index 907a9b3..f81e326 100644 --- a/design/reviews/consensus-ledger.json +++ b/design/reviews/consensus-ledger.json @@ -2077,10 +2077,11 @@ ], "issue": "The native tier is 'a disposable local backend per test' and the contract says nothing about how a test controls time or interrupts work, yet the examples depend on both. Interruption: L2-5 'kills the backend's scheduler after the third batch', L2-8 'stops the scheduler after its first batch', L2-6 'interrupts after the second history batch', L1-3 has 'the harness drops the response'. A local backend offers no scheduler switch, and a restart leaves the pending batch to run by itself, which exercises none of `resumeGeneration`. Time: L3-2 'advances past the grace' of 120 seconds on a 60 second cron, L3-5 'advances past the lease' of 35 minutes, L3-8 'advances 31 days', L3-4 needs eight attempts under a backoff that sums to minutes. No Spec makes any of these constants configurable and every one is read from `Date.now()` inside the function. The first experiment needs the interruptions to pass Layer 2, so it cannot be run from the Specs without inventing a harness, and the Layer 3 examples cannot run in bounded time at all.", "fix": "Add a harness contract to `spec:platform.acceptance-contract` as an [extension]. Interruption is a fault flag in the fixture app, a row a named batch or wrapper reads and throws on, never a scheduler kill, so the chain ends the way a developer error ends it and the operator path is what resumes it; a lost response is a caller that discards the result. Time: every duration the design pins, grace, cron interval, lease, backoff, deferral, retention horizons, rollback period, receipt window, is read from one module configuration with the pinned defaults, and a native run that shortens them records 'adjusted configuration' with the values, which the evidence rule already requires. Say which examples run with shortened values and that Sc L2-9's production-configuration run excludes them.", - "status": "open", - "note": "", - "slice": "S0", - "detector": "judgement" + "status": "partially-fixed", + "note": "S0, 2026-10-01. `spec:platform.acceptance-contract` now names the native tier's controls under E-15: the backend process (start on fresh storage, stop, restart on the same storage), the clients (close, discard a response), the environment variables and, on the fixture composition, fixture functions. It has no clock control and no scheduler switch. Two claims in the issue are too broad: three durations are already configuration or arguments (receipt retention per command, backoff per handler, the rollback period), and the sweeper measures elapsed time with performance.now(). The fix's 'one module configuration' is not taken: whether the durations Layer 3 pins become configuration is left to Layer 3 and sits in the owner queue, and a lease cannot be shortened apart from the action timeout it must exceed. Replacing every interruption with an injected throw is not taken either: L3-2 separates a cancelled dispatch from a failed wrapper. No Layer 0 to 2 example needs time to pass. Still open: the Layer 2 examples that say 'kills the backend's scheduler' or 'stops the scheduler' (L2-5, both L2-6 siblings, L2-8) name a control that does not exist. S4 and S5 reword each to a listed control when they build it, and the build must show `resumeGeneration` run after an interruption that leaves no pending batch. The Layer 3 to 6 examples that advance time wait for Layer 3.", + "slice": "S4", + "detector": "judgement", + "unreviewed": true }, { "round": 3, @@ -2684,10 +2685,11 @@ ], "issue": "The first experiment passes 'when every Layer 0, 1 and 2 scenario passes on a native backend', and many of those examples need a seam, an observation point or a domain that no Spec gives. This is beside r3-architecture-native-tier-cannot-move-time-or-interrupt, which covers interrupting a batch chain and moving time. (1) Faults inside one mutation. L1-2: 'The fixture context's adapter `execute` saves the folded state and then throws on a fault flag the test sets', 'appends the new events to the journal and then throws on a fault flag', 'The fault sits in the parent ... immediately before the receipt insert of step 10'. L2-1: 'injects a plain throw inside the Inventory component's `allocate` operation after Orders has appended its events'. ALL-1: 'points the fixture app's audit writer at a table whose validator refuses the record', 'replaces the fixture app's diagnostic sink with one that throws'. `execute`, `runPipeline` and `writeAudit(ctx, record)` are pinned with no hook, and baseline-operations says 'the production composition has no injection point'. (2) Observation inside the backend. L1-10: 'a spy on the decider records zero calls to `decide`'. L1-4: 'the Orders decider's `decide` did not run for the second call'. ALL-1: 'the fixture's swallowed-error counter is one', where `emitDiagnostic` 'increments a counter in memory', which a test process cannot read from a mutation's isolate. L2-3: 'exactly one `ctx.runMutation` on Orders and one on Inventory from the fixture counter', a 'fixture wrapper around `ctx.runMutation`', in an example that binds `{configuration: \"production configuration\"}`. (3) The fixture app. The contract says 'The kernel's fixture app is separate from the example app' and never says what it contains or which examples run on which app. L1-1, L1-10 and L1-11 bind states `draft` and `submitted` and operations `ship`, `amend naming the reviewed version` and `claim one unit`; the example domain has none of them, its model says '`PlaceOrder` and `CancelOrder` are the only public commands of the experiment', and neither names a reviewed version, so Sc L1-10 has no command to run on the experiment's application. (4) Sc L2-9 'runs the full Layer 0 to 2 suite' against a deployment where 'no test-only function is registered', while L2-7 uses 'a parent test mutation' and the items above need test code inside the backend.", "fix": "Add a harness section to `spec:platform.acceptance-contract`, as an extension beside the one r3-architecture-native-tier-cannot-move-time-or-interrupt asks for, and point every example at it. Say what the fixture app is: a second composition that mounts the same libraries with a small fixture context whose stream has the `draft` and `submitted` states and the `ship`, `amend` and `claim` operations, and say which examples run on it and which on the Orders and Inventory application; or move L1-1, L1-10 and L1-11 onto the example domain and give `CancelOrder` an optional reviewed version in `spec:application.orders-inventory-example`. Pin the fault seam once: for example the fixture context's decider or `combine` throws on a fault row for the context-side points, and the fixture app wraps `insertReceipt`, `writeAudit` and the diagnostic sink, so the shared library carries no hook. Replace each in-backend spy with an assertion on stored data or the returned error, such as `staleVersion` with `details.current` and no new event for 'decide never ran', and say how the swallowed diagnostic is observed, for example a log line the test reads. State how component calls are counted without a wrapper in the production composition, or that the count comes from the function log alone. State that Sc L2-9 runs the subset of the suite that needs no fixture function and name that subset.", - "status": "open", - "note": "", - "slice": "S0", - "detector": "judgement" + "status": "partially-fixed", + "note": "S0, 2026-10-01. `spec:platform.acceptance-contract` states under E-15: two compositions, fixture and production, with the rule that routes an example to one; faults only through parts the fixture composition supplies, with no hook or flag in the shared libraries; assertions only on what a client or the harness's admin access can read, and for 'did not run' a fixture part that fails the call if it runs; and Sc L2-9's production-configuration run as the scenarios routed to the production composition, which is a reading for the owner because the doc also says every Layer 0 to 2 scenario passes on a native backend. The L2-9 example's bullets follow that reading. Two substitutions in the fix are not taken: a throw from `decide` or `combine` reaches neither 'after the state write' nor 'after the journal append', and a `staleVersion` result does not show that `decide` never ran. Still open, by slice. S1: give the fixture context its states and operations (draft, submitted; ship, amend, claim), name the composition on each Layer 1 example, show the three L1-2 fault points reached with the library's own `execute` and pipeline unmodified, and replace each in-backend spy. S2: the same for L2-1's throw, and a source for the component-call count on the production composition. S5: ALL-1's audit and sink faults, the swallowed-error counter a test cannot read, and L2-7's parent test mutation.", + "slice": "S1", + "detector": "judgement", + "unreviewed": true }, { "round": 3, @@ -2888,10 +2890,11 @@ ], "issue": "Rubric item 18 holds as the plan's section 4 states it: all 43 rows have examples, 54 in all, each with the doc's pass condition verbatim as its outcome (checked by script), and nine rows are split into siblings. The split rule is applied unevenly, though. README's ID changes add a sibling for L2-1, L2-6, L3-2 and L3-6 'because the doc's scenario row enumerates two cases', and four rows of the same form keep one bound point with the second case only in a verification bullet: L1-7 'a worker or agent namespace' binds `worker` and tests `agent` in a bullet, L1-9 'Rate or capacity refusal' binds a rate refusal only, L2-4 'Rename a typed handler or break its argument contract' binds the rename only although the parent's space has all three values of `change`, and L1-11 'the same stock or unique value' binds stock only. For L1-11 the parent's example space cannot bind the other case at all: `state` is `\"draft\"|\"submitted\"|\"stock of 1\"` and no operation creates a subject with a claimed unique value, although the adapter's `uniqueValueWorked` bullet designs exactly that.", "fix": "Either add the four siblings, `client-claims-agent-namespace`, `capacity-refusal-then-retry`, `broken-argument-contract-fails-build` and `competing-unique-value` with a `claimed unique value` state and a creating operation in `spec:context.context-component`'s vocabulary, and list them in README's ID changes, or state in `spec:platform.acceptance-contract` the rule that decides when an 'or' in a row is a case and when it is a variant covered by a verification bullet, so the next reviewer can check it.", - "status": "open", - "note": "", - "slice": "S0", - "detector": "judgement" + "status": "partially-fixed", + "note": "S0, 2026-10-01. The rule is now exact in `PLAN.md` section 4 and in the acceptance contract's verification: an 'or' in a scenario's text enumerates cases and each gets a sibling example; an 'or' in the pass condition does not; a verification bullet never stands in for a case. The fix's alternative, a rule that lets a bullet cover a variant, is not taken, because it would change a rule the corpus already states in three places. One correction to the issue: the capacity and the unique-value cases are absent altogether, not carried by a bullet. Siblings still to write, each with the slice that builds its parent: S1 `client-claims-agent-namespace` (L1-7), `capacity-refusal-then-retry` (L1-9) and `competing-unique-value` (L1-11, which needs a claimed-unique-value state and a creating operation in the fixture context's vocabulary); S2 a query sibling of `committed-state-visible` (L2-2) and `broken-argument-contract-fails-build` (L2-4). README's ID changes list them when they exist.", + "slice": "S1", + "detector": "judgement", + "unreviewed": true }, { "round": 3, @@ -3587,10 +3590,11 @@ ], "issue": "E-13 gives every native test its identity through 'the local backend's admin key acting as an identity' and says 'the token's source is the one named difference from production' (tenancy-and-authority `nativeTierIdentity`; acceptance-contract `tierNativeIdentity` and the E-13 rule: 'the identity's signature source is the one named difference'). The mechanism exists: `setAdminAuth(token, actingAsIdentity)` in convex-js `src/browser/http_client.ts` and `ConvexTestingHelper.withIdentity` in `convex-helpers/testing.ts` (github.com/get-convex/convex-js and get-convex/convex-helpers, read 2026-10-01). But a request that carries the admin key is not checked like a client request. In `crates/udf/src/validation.rs` of get-convex/convex-backend (read 2026-10-01) `check_visibility_access` allows an internal function when `identity.is_admin() || identity.is_system() || identity.is_acting_as_user()` ('Admins may have the ability to run the internal function'), and the same test opens a component's public functions to such a caller. The docs agree for the admin tools: internal functions can be run 'using the Dashboard' and 'from the CLI' (docs.convex.dev/functions/internal-functions, read 2026-10-01). So the harness identity can call every `internalMutation` and every component function directly. Two things follow. First, the Sc L1-7 example's sibling assertion, 'The test also calls the internal entry from the client with namespace `agent` and asserts that Convex refuses the call', cannot pass when the client is the harness client E-13 pins for 'caller user-1': the call is accepted and the internal entry runs with a client-chosen actor and namespace. Second, the parity statement for Sc L2-9 is incomplete: under the acting-as identity `clientClaimRefused` ('a client call to the internal entry is refused by Convex before any handler runs') and `clientReach` in context-component do not hold for the test's own client, so a test that reaches an internal entry or a component function by mistake passes natively and fails in production. E-13 also says the alternative costs 'a JWKS endpoint per run'; the custom JWT page says of `jwks`: 'If you'd like to avoid hitting an external service you may use a data URI' (docs.convex.dev/auth/advanced/custom-jwt, read 2026-10-01), so no endpoint is needed.", "fix": "On `spec:platform.acceptance-contract` (owner of E-13) name the second difference: an admin acting-as identity passes Convex's visibility check for internal and component functions, so the harness client is more privileged than any production client. Pin that every assertion about visibility, Sc L1-7's direct call to the internal entry and any 'a client cannot reach a component function' check, is made from a second client that holds no admin key (unauthenticated is enough, since the refusal precedes authentication), and say so in the L1-7 example's verification bullet and in `clientClaimRefused`. Correct the E-13 alternative: a test issuer needs no endpoint, because a `customJwt` provider takes its JWKS as a data URI; the cost is one extra provider entry in the fixture's `auth.config.ts`, which the evidence record names as adjusted configuration. Add both differences to `evidenceRecord`.", - "status": "open", - "note": "", + "status": "fixed", + "note": "S0, 2026-10-01. E-13's option changed on `spec:platform.acceptance-contract`: a caller under test is an ordinary client carrying a token the harness signs for a fixture issuer, and `auth.config.ts` reads the issuer and a data-URI key set from the deployment's environment variables. The admin key is for setup and reading only (E-15). Restated in `tenancy-and-authority` (`nativeTierIdentity`), `first-experiment` (the rule and `identitySource`), the L2-9 and L1-7 examples and README. Four examples verify the contract on a native backend. Two corrections to the finding: the refusal precedes the handler, not authentication; and the fix's cost line is right, a data-URI key set needs no endpoint. The owner confirms E-13.", "slice": "S0", - "detector": "native local-backend test" + "detector": "native local-backend test", + "unreviewed": true }, { "round": 3, @@ -3704,9 +3708,9 @@ ], "issue": "The corpus treats whether transaction limits add up across nested calls as unknown: F13 `limitNestedCalls` ('does not say whether the per-transaction limits add up across nested calls and components; Probe 4 stands'), batch-shaped-api ('Probe 4 pending ... the bounds here assume they do'), command-pipeline `subTransactionLimits` ('the design passes none until Probe 4 says whether limits add up across the boundary'). The limits page links to the answer: 'These limits apply to each `query` or `mutation` function. To ensure you're within the limits, there are tools to calculate and limit consumption within a transaction', and that section states 'a mutation or query called by another mutation or query share the overall transaction limits' (docs.convex.dev/production/state/limits#transactions and docs.convex.dev/database/writing-data#write-performance-and-limits, read 2026-10-01). So the corpus's worst-case assumption is the documented behavior for nested calls; a component call is a `ctx.runMutation`, though the page does not name components. The same section documents two tools no Spec uses. `ctx.meta.getTransactionMetrics()` returns `{ used, remaining }` for bytes read, bytes written, documents read, documents written, database queries and scheduled functions. `transactionLimits` on a nested call exists to 'reserve space for work after the nested function call, even if the child reads or writes too much', and the docs' example is a wrapper that runs a nested mutation in a try block and records the failure, which is the local reaction wrapper's shape. Three places pay for not using them: the wrapper's `limitBodyBudget` leaves the settle write unprotected, so a body that exhausts the shared budget makes the catch path fail too and the attempt is spent as a rearm, not recorded as a retryable attempt; every batch stops on hand-derived byte arithmetic where it could stop on measured headroom; and first-experiment `captureDocuments` plans to read documents and bytes 'from the backend's execution records where the local backend exposes them' and records `not exposed` otherwise, while the mutation can read its own totals at step 11. The page states no nesting depth; the open-source backend bounds it at 8 (`MAX_REACTOR_CALL_DEPTH`, crates/common/src/knobs.rs, read 2026-10-01), which the deepest chain here, 4, fits.", "fix": "In `spec:facts.f13-transactions-have-limits` replace `limitNestedCalls` with the documented statement and its URL, add the depth bound as read from source, and narrow Probe 4 in `spec:facts.probe-plan` to confirming the sharing across a component boundary and the depth. Drop 'until Probe 4 answers' from the bounds that already assume sharing. In `spec:obligations.local-reaction-wrapper` pass `transactionLimits` to the body call, computed from `ctx.meta.getTransactionMetrics()` minus a stated reserve for the settle patch and the next schedule, and say so in `limitBodyBudget`; do the same for the sweeper's `recoverOne` calls. In `spec:application.first-experiment` make `ctx.meta.getTransactionMetrics()` at the pipeline's step 11 the source of `documentsRead`, `documentsWritten`, `bytesRead` and `bytesWritten`, emitted on the diagnostic record. Record under E-22 or E-40 that a batch may stop on measured headroom, with the byte arithmetic kept as the planning bound.", - "status": "open", - "note": "", - "slice": "S0", + "status": "partially-fixed", + "note": "S0, 2026-10-01. Done: `spec:facts.f13-transactions-have-limits` records the documented sharing with its page and date, the result of Probe 4 on a native backend (16 MiB read and 16 MiB written are each one budget across a nested call and across a component boundary), the depth the pinned backend allows (a stack of nine functions, a backend default, where the fix said eight from source) and a new Design key `transactionMetrics`. The 'until Probe 4 answers' wording is gone from `batch-shaped-api`, `context-component`, `command-pipeline`, `parent-use-cases`, `restore`, D10 and F2. Corrections to the fix as written: `getTransactionMetrics()` is asynchronous and returns `used` and `remaining` per metric, and the depth is not an immutable contract. Still to do, each with the slice that builds it: S1 decides whether a pipeline call reserves room with `transactionLimits` (the question is on `command-pipeline`); S3 makes `ctx.meta.getTransactionMetrics()` the source of the experiment's read and write counts, which also needs fields on the diagnostic record in `baseline-operations`; Layer 3 passes `transactionLimits` to the wrapper's body call and the sweeper's `recoverOne` calls.", + "slice": "S3", "detector": "judgement" }, { @@ -3722,10 +3726,11 @@ ], "issue": "F15 is carried as assumed: 'Nothing read so far states or denies it', `measurableBy: no page states it; S2 and S5 describe reactivity and component isolation separately`. A docs page states it: 'Queries, mutations, and action rules still apply - queries can only call component queries, mutations can also call component mutations, and actions can also call component actions. As a result, queries into components are reactive by default, and mutations have the same transaction guarantees' (docs.convex.dev/components/using#using-the-components-api-directly, read 2026-10-01). The system tables page adds that queries on system tables are 'reactive and realtime just like queries reading from all other tables'. The doc's ledger lists F15 as assumed and only the owner edits the doc, but the corpus records rechecks on the fact, as it did for F16. What stays open is the second half of Probe 5: whether the `paginator` page relayed by the parent stays contiguous under live writes and whether the split path works across the boundary.", "fix": "On `spec:facts.f15-parent-query-over-component-query-stays-reactive` record the recheck the way F16 does: status rechecked, the using page as `measurableBy`, the doc's own status left for the owner. Move F15 from `factsAssumed` to `factsRechecked` in `spec:facts.fact-ledger`, and narrow Probe 5 in `spec:facts.probe-plan` and the Probe 5 question in `spec:context.queries` to the pagination half.", - "status": "open", - "note": "", + "status": "fixed", + "note": "S0, 2026-10-01. F15 states `probed` and `defined`: the page on using components states the reactivity and Probe 5 showed it on a native backend. Its blocking question is gone. Restated in `fact-ledger` (`factsAssumed`, `factsProbed`), `probe-plan`, `context.queries`, `application.read-models`, `application.rebuild`, D8, the platform root, README and PLAN's rubric items 21 and 34; the fix's file list missed the last five. The doc keeps F15 as assumed until the owner edits it. The pagination half of Probe 5 ran too and is recorded on F15; what it found about the client hook is finding `s0-helper-hook-loses-rows-after-a-capped-split`.", "slice": "S0", - "detector": "judgement" + "detector": "judgement", + "unreviewed": true }, { "round": 3, @@ -3952,6 +3957,261 @@ "note": "", "slice": "S4", "detector": "judgement" + }, + { + "round": "S0", + "id": "s0-harness-rules-sit-on-the-acceptance-contract", + "lens": "sdp", + "severity": "minor", + "files": [ + "specs/platform/acceptance-contract.sdp.md", + "specs/platform/native-harness.sdp.md", + "specs/foundation.pack.sdp.md", + "README.md" + ], + "issue": "Slice S0 wrote the harness rules of E-13 and E-15 into `spec:platform.acceptance-contract` as seven bullets, several holding two or three rules, and bound the four harness examples to it with `verifies`. The graph then showed the acceptance contract as verified by tests that touch no scenario row, and the issuer as 'the one difference' was stated three times in one Spec. Found by the Opus review of commits a6889c1 and 951874d (minors 2 and 17).", + "fix": "Move the harness rules to a Spec of their own that refines the acceptance contract, one rule per bullet, and re-parent the four examples.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "judgement", + "note": "S0, 2026-10-01, ruling S1. `spec:platform.native-harness` now owns E-13 and E-15, their two open questions, 23 rules and the Design keys `backend`, `identity`, `adminReads`, `localWriteRate` and `evidenceKept`. The acceptance contract keeps the doc's rules and E-14, and one sentence that points at the new Spec. The four examples are `spec:platform.native-harness.`; their test anchors and contract imports follow. Restated in `tenancy-and-authority` (`nativeTierIdentity`), `first-experiment` (the identity rule), the L2-9 example, the foundation pack, README's register and PLAN's inventory, which cite E-13 or E-15 and no longer name the acceptance contract as the owner." + }, + { + "round": "S0", + "id": "s0-issuer-is-both-the-one-difference-and-an-adjustment", + "lens": "sdp", + "severity": "major", + "files": [ + "specs/platform/native-harness.sdp.md", + "specs/application/first-experiment.sdp.md", + "specs/application/first-experiment.native-acceptance-production-configuration.sdp.md" + ], + "issue": "The acceptance contract called the fixture issuer 'the one difference from production' and also required the production-configuration run of Sc L2-9 to have 'no adjusted configuration', while the harness wrote the issuer under `adjustments`. If the issuer is an adjustment, no production-configuration run can satisfy the rule. Opus review, M3.", + "fix": "State once what the issuer is, and have the evidence record follow.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "judgement", + "note": "S0, 2026-10-01, ruling S2. `spec:platform.native-harness`: 'The fixture issuer is the identity source of a native run and its one named difference from production; it is recorded as the identity source and is not an adjusted configuration.' `first-experiment`'s `typeMeasurement` already keeps `identitySource` apart from `adjustments`. The evidence module follows in the fold-in (code ruling C12)." + }, + { + "round": "S0", + "id": "s0-fixture-composition-claims-the-same-schemas", + "lens": "fidelity", + "severity": "major", + "files": [ + "specs/platform/native-harness.sdp.md", + "specs/platform/acceptance-contract.sdp.md", + "specs/application/first-experiment.native-acceptance-production-configuration.sdp.md" + ], + "issue": "The contract said a scenario on the fixture composition 'runs the same libraries, schemas and authority model', and `tierNative` said 'production schemas and authority', while the fixture composition mounts fixture contexts with their own schemas. An owner reads production parity into fixture runs. Opus review, M4.", + "fix": "Name which tables are shared and which are the fixture's.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "judgement", + "note": "S0, 2026-10-01, ruling S3. The rule now reads: a scenario on the fixture composition 'runs the shared libraries with their own tables and the same authority model, beside fixture contexts whose state is the fixture's own'. `tierNative` on the acceptance contract is back to the doc's sentence. The L2-9 example says the same." + }, + { + "round": "S0", + "id": "s0-test-hook-and-replaceable-part-undefined", + "lens": "completeness", + "severity": "minor", + "files": [ + "specs/platform/native-harness.sdp.md" + ], + "issue": "The contract said the shared libraries 'carry no test hook' and allowed 'a replaceable part it wires in place of the production one, such as the audit destination and the diagnostic sink'. The boundary between a hook and a replaceable part was undefined, and the line settled the mechanism of Sc ALL-1 ahead of slice S5. Opus review, minor 3.", + "fix": "Define a hook, and say what a composition may supply without naming S5's mechanism.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "judgement", + "note": "S0, 2026-10-01, ruling S4. Two rules: 'The shared libraries carry no test hook: they read no flag and take no callback that exists only for a test', and a fault enters only through 'a fixture context's declaration, a fixture function, or a dependency the libraries take from every composition'. The audit destination and the diagnostic sink are no longer named here; S5 decides how Sc ALL-1 breaks them." + }, + { + "round": "S0", + "id": "s0-refusal-examples-pass-when-the-target-is-missing", + "lens": "completeness", + "severity": "major", + "files": [ + "specs/platform/native-harness.sdp.md", + "specs/platform/native-harness.ordinary-client-refused-internal-function.sdp.md", + "specs/platform/native-harness.ordinary-client-refused-component-function.sdp.md" + ], + "issue": "The two refusal examples claimed 'Convex refuses the call before a handler runs' and showed only that the call failed and no row existed. Three native mutations survived on commit 951874d: the tests pass when the function or the component they call does not exist. The internal-function example cited Sc L1-7, so a reader could count the namespace scenario as covered by Convex's visibility check. Opus review minors 1 and 7, sol review 1 and 2, astra review 8.", + "fix": "A refusal example states what it shows: the refusal's own error, nothing stored, and the target existing on the same deployment.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "native backend", + "note": "S0, 2026-10-01, ruling S5. The rule on `spec:platform.native-harness` and both examples now require the error text (`Could not find public function`; `BadDeployKey` on the endpoint that takes a component path), zero rows, and one row after the same function is called with admin access on the same deployment. The component example covers both routes a client has. The Sc L1-7 citation is gone. The tests follow in the fold-in (code ruling C8), which reruns the three mutations." + }, + { + "round": "S0", + "id": "s0-example-steps-negated-by-a-boolean", + "lens": "sdp", + "severity": "minor", + "files": [ + "specs/platform/native-harness.fixture-issuer-token-yields-identity.sdp.md", + "specs/facts/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.sdp.md", + "specs/facts/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.sdp.md" + ], + "issue": "Several example steps were positive sentences whose bound value negated them: 'is stored {stored: false}', 'returns an identity {anonymousHasIdentity: false}', 'is stored after the parent caught the error and committed {childRowStored: false}'. A reader takes the sentence for the claim. The Probe 2 examples also named 'a client whose parent lets the error pass', a second parent no Given set up. Opus review, minors 4 and 5.", + "fix": "Reword with counts, and set up every actor in a Given.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "judgement", + "note": "S0, 2026-10-01, ruling S6. Steps now bind counts ('holds {rows: 0} rows') or carry no slot ('returns no identity'). Both Probe 2 examples have a Given for the two parents, the one that catches and the one that lets the error pass." + }, + { + "round": "S0", + "id": "s0-probe-examples-bind-the-answer-and-can-miss-the-boundary", + "lens": "completeness", + "severity": "major", + "files": [ + "specs/facts/probe-plan.sdp.md", + "specs/facts/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.sdp.md", + "specs/facts/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.sdp.md", + "specs/facts/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.sdp.md", + "specs/facts/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.sdp.md", + "specs/facts/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.sdp.md", + "specs/facts/f13-transactions-have-limits.probe-4-nesting-depth.sdp.md", + "specs/facts/f13-transactions-have-limits.probe-4-nested-call-shares-limits.sdp.md", + "specs/facts/f13-transactions-have-limits.probe-4-component-call-shares-limits.sdp.md", + "specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.sdp.md", + "specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.sdp.md", + "specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.sdp.md", + "README.md" + ], + "issue": "The probe examples wrote the expected answer in as the pass condition with nothing saying it was a hypothesis, so a contrary result would read as a red test to repair: the depth example bound 8 and the first native run showed 9. Probe 3 dropped function-call quota without saying so and its local timings would feed OQ1 unmarked. Probe 5's question, whether the client hook splits a page, sat in a verification bullet that allowed the run to skip it. Probe 1's Thens passed without reaching the guarantee's boundary: twenty trials with no row passed, the HTTP example lost no response, and a random kill might never land before the commit; the examples said 'WebSocket client' for a fact about the React client. The subscription example did not require a parent that reads no table of its own. Opus review, M5 to M8 and minors 5, 6 and 16; the first native run of the probes on 2026-10-01.", + "fix": "Say in the probe plan what a bound value is and what follows a different result; make each example fail when its boundary is not reached; name the client; give the hook its own example; say what a local backend cannot answer.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "native backend", + "note": "S0, 2026-10-01, ruling S7. `spec:facts.probe-plan` carries extension E-16 with six rules: a probe is example Specs that verify its fact; bound values are the expectation written before the first run; a different first result is recorded on the fact and the example is then bound to what the release does and says so; an example fails when its run did not reach its boundary; sizes are recorded and not asserted; what a local backend cannot answer stays open on the fact. Probe 1's three examples name `ConvexClient` or `ConvexHttpClient`, require a trial committed without confirmation, a kill on each side of the commit, and a lost response with one request per call. Probe 3 says quota is not measured and the time is a local backend's. Probe 4's depth binds 9 as observed and calls it a backend default; the shared-limit examples say the seeding is paced under the local backend's write rate and that each half of the write commits alone. Probe 5 gains `probe-5-hook-splits-across-boundary`; the subscription example's parent reads no table of its own. Opus minor 16, drop the depth example, is rejected: the round-3 finding on nested limits asks for the depth and the test is small." + }, + { + "round": "S0", + "id": "s0-helper-hook-loses-rows-after-a-capped-split", + "lens": "convex", + "severity": "major", + "files": [ + "specs/context/queries.sdp.md", + "specs/application/read-models.sdp.md", + "specs/facts/f15-parent-query-over-component-query-stays-reactive.sdp.md", + "specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.sdp.md" + ], + "issue": "`spec:context.queries` relays a component list's `paginator` page through the parent and relies on the client hook to split a page the helper marks `SplitRequired` (`componentPagination`, `paginationShape`, `parentPassThrough`). On a native backend, release `precompiled-2026-09-28-5c7cb5b`, with `convex-helpers` 0.1.124, the `usePaginatedQuery` hook of `convex-helpers/react` showed 40 of 45 rows after a page of 10 grew to 25 rows under `maximumRowsRead` 20: the five rows between the capped page's `continueCursor` and its end cursor were never loaded, and the hook reported no error. Read in the helper's source by the design agent: a capped page returns `continueCursor` at the last row it read, and the hook ends the second half of the split there. Reading the two halves directly, from the start to the split cursor and from the split cursor to the page's own end cursor, gives all 25 rows. So a list that is capped by `maximumRowsRead` and read with this hook can lose rows without a sign. Found by the design agent's bound test in slice S0, 2026-10-01, run twice; the orchestrator reran it after applying the prototype.", + "fix": "Slice S2 decides before it builds lists: whether a context list sets `maximumRowsRead` at all, whether the parent or the client closes the range to the page's end cursor, or whether the client uses another hook. The Probe 5 example keeps the observed value so a later helper release that fixes it turns the test red.", + "status": "open", + "note": "", + "slice": "S2", + "detector": "native backend" + }, + { + "round": "S0", + "id": "s0-f11-documented-unavailable-observed-available", + "lens": "convex", + "severity": "minor", + "files": [ + "specs/facts/f11-components-have-no-ctx-auth.sdp.md", + "specs/decisions/d11-tenant-scope-and-authority.sdp.md", + "specs/command/tenancy-and-authority.sdp.md", + "specs/command/command-pipeline.sdp.md", + "specs/context/context-component.sdp.md", + "specs/context/queries.sdp.md" + ], + "issue": "F11, 'Components have no `ctx.auth`', is listed by the doc as documented, and the components page says `ctx.auth` is not available within a component. On a native backend, release `precompiled-2026-09-28-5c7cb5b` with `convex` 1.46.0, `ctx.auth.getUserIdentity()` inside a component query returned the caller's identity (the unbound native test `component-auth.test.ts` records it; the run's record is `evidence/native-20261001T162936Z-ba5eddd.json`). `convex-test` 0.0.60 returns null for the same call. Six Specs state as fact that components have no `ctx.auth` or never see it. The design does not depend on which holds: D11 passes the actor explicitly and no component function reads `ctx.auth`. But a reader takes 'cannot read the caller's identity' as an isolation guarantee, and on this backend it is a convention. Opus review of the foundation, M1; ruling O2.", + "fix": "The owner rules whether F11's wording and status follow the documentation or the backend. Provisional reading, recorded as an open question on F11: the documentation's sentence stands as the fact, the observation is recorded beside it, and the rule that no component function reads `ctx.auth` is a design rule under D11 and not a consequence of the platform.", + "status": "owner", + "note": "S0, 2026-10-01. The observation is an open question on `spec:facts.f11-components-have-no-ctx-auth`. No other Spec changed.", + "slice": "S1", + "detector": "native backend" + }, + { + "round": "S0", + "id": "s0-e16-silent-on-a-later-different-run", + "lens": "completeness", + "severity": "major", + "files": [ + "specs/facts/probe-plan.sdp.md", + "specs/platform/native-harness.sdp.md", + "specs/facts/f13-transactions-have-limits.probe-4-nesting-depth.sdp.md" + ], + "issue": "Extension E-16 said what a different first run means and nothing about a different later run. Seven bound values are what one backend release or one package version does (the depth of 9, the hook's 40 shown and 5 missing, the socket wait, two refusal texts, one log record per call), so the next pin bump would turn them red with nothing saying that red is a finding and not a regression. No example whose expectation held said so. The size rule ('an example asserts a size only where the fact states a bound') forbade the depth example's own Then. Opus review of the fold-in, M1 and M5.", + "fix": "Say what a red on a new release is; let a reader tell a held expectation from an observed value; rule between the size rule and the depth example.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "judgement", + "note": "S0, 2026-10-01. `spec:facts.probe-plan` gains the rule that a later run showing another value is again a finding and the example is rebound, and the size rule now tells a measured size, which is recorded and not asserted, from a limit the backend enforces, which is bound as observed. `spec:platform.native-harness` gains the same rule for the values its four examples bind. Twelve probe examples say their bound values held on the first run; the depth and hook examples already said theirs were observed. The depth example also binds the refusal's text." + }, + { + "round": "S0", + "id": "s0-fault-rule-contradicts-the-harness-controls", + "lens": "sdp", + "severity": "major", + "files": [ + "specs/platform/native-harness.sdp.md" + ], + "issue": "One rule said a fault enters a native run only through something the fixture composition supplies, while other rules and the Design entries give the harness a kill, a closed client and a lost response, and all of Probe 1 injects its fault that way. A slice S1 implementer would read the first rule as forbidding the tools the Spec hands over. Opus review of the fold-in, M2.", + "fix": "One statement of which faults come from the harness and which only from fixture code.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "judgement", + "note": "S0, 2026-10-01. Two rules: a fault inside a function enters only through what the fixture composition supplies; a fault outside the functions, a killed backend, a closed client or a lost response, comes from the harness's controls." + }, + { + "round": "S0", + "id": "s0-harness-design-mixes-stubs-detail-and-readings", + "lens": "sdp", + "severity": "major", + "files": [ + "specs/platform/native-harness.sdp.md" + ], + "issue": "After the design pass the harness Spec's Design section held about thirty entries under a preamble that called all of them observations: the stubs slice S1 builds on, implementation detail that goes stale at the first code change (system query names, CLI flags, retry counts, thirteen fields copied from the backend's log), and readings of one backend release written as rules. The fixture token was said to yield 'the shape a production token yields' while it carries five claims. Opus review of the fold-in, M3 and minor 10; the owner's direction that a Spec carries the design of its key abstractions.", + "fix": "Keep the stubs later slices build on, mark one release's readings as readings, drop the entries whose only reader is the harness's own code.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "judgement", + "note": "S0, 2026-10-01. Ten entries are gone: the table-read and log mechanisms, the executable resolver and its type, the pin file, the issuer constructor, `startBackend`, the readiness check, the completion record's full type and the CLI call. Three entries are renamed `observedWriteRate`, `observedFunctionLog` and `observedTableRead` and the preamble says they are readings, not rules. `identity` names the five claims the fixture token carries. `adminReads` names the six fields of a completion record a test may rely on. Left for polish: several rules are still said twice or hold two rules in one bullet (Opus minors 1 and 2)." + }, + { + "round": "S0", + "id": "s0-socket-route-claimed-as-a-refusal", + "lens": "completeness", + "severity": "major", + "files": [ + "specs/platform/native-harness.ordinary-client-refused-component-function.sdp.md" + ], + "issue": "The component refusal example said Convex 'refuses the call on every route', and for the WebSocket route it showed only that a promise stayed pending for three seconds. The test passed when the mutation was never sent, which the behavior review showed, and when the public route called a function that does not exist, which a native mutation showed. Opus review M4, astra review 6, sol review 6.", + "fix": "The outcome says what each route showed; the socket step shows that the request left the client and that the backend closed the connection; the public route checks its target.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "native backend", + "note": "S0, 2026-10-01. The outcome now reads: refused on the two HTTP routes with its own error, connection closed on the WebSocket route without an answer. The socket step binds the wait and at least one close by the backend, and the verification bullets say what would show a call that ran. The test follows in fold-in 2." + }, + { + "round": "S0", + "id": "s0-sweep-rule-promises-more-than-a-process-id-allows", + "lens": "convex", + "severity": "minor", + "files": [ + "specs/platform/native-harness.sdp.md" + ], + "issue": "The harness rule said a run 'never signals a process it did not start'. The sweep signals by process id after reading the process table, and between the read and the signal a process id can change hands. The review of the second fold-in showed the interleaving on a model of the code (astra, report U, finding 1), and showed on a real child process that a failed inspection left the process alive, removed its directory and reported success (finding 2).", + "fix": "The rule says what the harness does before a signal and what it does when it cannot inspect a process; a Design entry says the interval between read and signal is not closed.", + "status": "fixed", + "unreviewed": true, + "slice": "S0", + "detector": "pure test", + "note": "S0, 2026-10-01, third main thread. The rule is split in two: cleanup, and inspection before a signal with the failed-inspection case. Design entry `sweepOwnership` states the open interval and why it matters only for a backend that exits during the sweep. The code follows in fold-in 3, list A, item A1." } ], "sessions": [ @@ -3969,6 +4229,11 @@ "date": "2026-10-01", "unit": "review", "summary": "Round 3, lenses sdp, convex, fidelity and completeness, full scope at f44cad4, run as four fable-xhigh agents in parallel at the owner's instruction, a one-off departure from the main-thread rule. Each agent edited no Spec and appended to its own design/reviews/r3-.jsonl; the main thread merged the four files into the ledger. 95 findings added (1 blocker, 26 majors, 68 minors), all open. Of the 37 unreviewed round-2 fixes, 35 confirmed and 2 set to partially-fixed. No lens approved." + }, + { + "date": "2026-10-01", + "unit": "slice", + "summary": "Slice S0, three main threads. Built the native harness, the fixture app with one component, four test tiers, codegen, a watch mode and CI, and ran probes 1 to 5 as eighteen example Specs bound to native tests. spec:platform.native-harness is new and owns E-13 and E-15; the probe plan carries E-16; F14 and F15 are probed. Fourteen findings entered with round S0: twelve fixed and unreviewed, one open for S2 (the helper hook loses rows after a capped split), one for the owner (F11 observed available in a component). Two round-3 findings of the slice are fixed and unreviewed, and three are partially-fixed with the kernel half waiting for S1. Five fold-ins, each reviewed by GPT lanes on a frozen copy; thirty-seven native mutations run by the main thread. The slice stopped on tactical decision 19, with what was left tracked in STATE.md under Leads. Native tier at 61c3879 on a clean tree: 21 files, 31 tests." } ] } \ No newline at end of file diff --git a/design/specs/application/first-experiment.native-acceptance-production-configuration.sdp.md b/design/specs/application/first-experiment.native-acceptance-production-configuration.sdp.md index b3d6e5f..93508de 100644 --- a/design/specs/application/first-experiment.native-acceptance-production-configuration.sdp.md +++ b/design/specs/application/first-experiment.native-acceptance-production-configuration.sdp.md @@ -25,6 +25,6 @@ Then authority, schemas, concurrency and code path {parity: "match a release"} ## Verification — executable - Runs in the end-to-end tier on the production composition; every test owns its disposable backend. -- The test deploys the release build, not a test build, with the production `auth.config.ts`, grants, namespaces and entry points and the production schema, and runs the full Layer 0 to 2 suite against it. -- The test obtains each identity through the local backend's admin key acting as an identity whose `issuer` and `subject` are the production issuer's, because a production issuer cannot mint tokens for a disposable backend; the evidence record names the identity source as the one difference from production, under E-13 of the acceptance contract. +- The test deploys the release build, not a test build, with the production `auth.config.ts`, grants, namespaces and entry points and the production schema, and runs against it every Layer 0 to 2 scenario that E-15 routes to the production composition; the scenarios it routes to the fixture composition run there with the shared libraries, their own tables and the same authority model. +- The test obtains each identity from a token the harness signs for the fixture issuer that the deployment's environment variables name, carried by an ordinary client, because a production issuer cannot mint tokens for a disposable backend; the evidence record names that issuer as the identity source, the one named difference from production, under E-13. - The test asserts that no test-only function is registered in the deployment and that the evidence record of the run states no configuration difference from production other than the identity source. diff --git a/design/specs/application/first-experiment.sdp.md b/design/specs/application/first-experiment.sdp.md index 982d526..50799e2 100644 --- a/design/specs/application/first-experiment.sdp.md +++ b/design/specs/application/first-experiment.sdp.md @@ -70,7 +70,7 @@ The six cost targets are the constraint Specs this workflow is constrained by. T - rule: The cost targets are one top-level commit per successful command, zero core projection jobs, O(N) business work allowed, one public command execution per business intent, no application-wide counter, and no queue recovery needed for essential reads (First experiment) - rule: The results feed the open question on component cost and the Layer 3 decisions (First experiment, OQ1, Decision method rule 4) - rule: Native acceptance runs with production configuration: the same authority, schemas, concurrency and code path as a release (Sc L2-9) -- rule: [extension] The native tier obtains its identities through the local backend's admin key acting as an identity whose issuer and subject are the ones production's `auth.config.ts` names, so the actor mapping, the grants, the namespaces and the entry points are the production ones; the token's source is the one named difference from production and every run's evidence record states it, as `spec:platform.acceptance-contract` rules under E-13 (E-13, S13, Sc L2-9) +- rule: [extension] The native tier obtains its identities from tokens the harness signs for the fixture issuer that the deployment's environment variables name, carried by ordinary clients, so the actor mapping, the grants, the namespaces, the entry points and Convex's visibility check are the production ones; the issuer is the identity source, the one named difference from production, and every run's evidence record states it (E-13, S13, Sc L2-9) - rule: Every run is recorded as acceptance evidence: commit, installed layers, backend and dependency versions, configuration, dataset, command and result, and a run with adjusted configuration states how it differs from production (Acceptance scenarios) - rule: The kernel's fixture app is separate from the example app, and test-only functions never ship (Acceptance scenarios) - rule: [extension] The second lifecycle command is `CancelOrder`, the essential summary is the order summary read model, and the provisional maximum is 100 lines (E-46, First experiment) @@ -89,7 +89,7 @@ The six cost targets are the constraint Specs this workflow is constrained by. T The experiment is an application, a test suite and a report. The application is the production composition: two context components, the parent with its pipeline, use cases, read models, registry and gate. The suite is the acceptance scenarios of Layers 0 to 2, run against a disposable local backend as the acceptance contract prescribes. The report is a set of measurement records, one per run, compared against the six constraint targets. -- typeMeasurement: `[extension] interface Measurement { commit: string; backendVersion: string; convexVersion: string; configuration: "production" | "adjusted"; adjustments?: string; identitySource: "issuer" | "admin-acting-as"; dataset: string; command: "PlaceOrder" | "CancelOrder"; lines: number; contention: number; path: "healthy" | "retry"; topLevelCommits: number; functionCalls: number; componentCalls: number; documentsRead: number; documentsWritten: number; bytesRead: number; bytesWritten: number; rowsLeftBehind: number; occRetries: number | "not exposed"; occExhaustions: number; latencyMs: number }` where `contention` is the number of concurrent callers and `occExhaustions` counts commands the engine failed with its documents-changed error (E-47, E-13, First experiment) +- typeMeasurement: `[extension] interface Measurement { commit: string; backendVersion: string; convexVersion: string; configuration: "production" | "adjusted"; adjustments?: string; identitySource: "production-issuer" | "fixture-issuer"; dataset: string; command: "PlaceOrder" | "CancelOrder"; lines: number; contention: number; path: "healthy" | "retry"; topLevelCommits: number; functionCalls: number; componentCalls: number; documentsRead: number; documentsWritten: number; bytesRead: number; bytesWritten: number; rowsLeftBehind: number; occRetries: number | "not exposed"; occExhaustions: number; latencyMs: number }` where `contention` is the number of concurrent callers and `occExhaustions` counts commands the engine failed with its documents-changed error (E-47, E-13, First experiment) - captureCommits: top-level commits are the successful top-level mutation executions in the backend's function log for the run (E-47, First experiment) - captureCalls: function calls are every function execution the log shows; component calls are counted by a fixture wrapper around `ctx.runMutation` and `ctx.runQuery` on component references, and the two are reconciled (E-47, F4) - captureDocuments: documents and bytes read and written per function come from the backend's execution records where the local backend exposes them; where it does not, the record says `not exposed` and the dashboard's insights on a cloud run stand in (E-47, First experiment) diff --git a/design/specs/application/parent-use-cases.sdp.md b/design/specs/application/parent-use-cases.sdp.md index 229f2d1..528deea 100644 --- a/design/specs/application/parent-use-cases.sdp.md +++ b/design/specs/application/parent-use-cases.sdp.md @@ -45,13 +45,12 @@ The boundary sits at the component API. The use case sees DTOs and stream versio - risk: The parent grows with each cross-context rule, so use cases are grouped by business flow (D10) - risk: An operation too large for one transaction is rejected or becomes a separate import command with honest partial progress, and the largest order the placement command supports is still a product decision (D10, F13, OQ3) - assumption: Component calls commit or roll back with the calling mutation, and each call is a sub-transaction isolated from other calls (F2, S2) -- assumption: A `ConvexError` thrown inside a component sub-transaction reaches the use case with its data intact; Probe 2 confirms it (F14, Probe 2) -- assumption: Transactions have limits, and whether they add up across nested calls and components is what Probe 4 shows (F13, Probe 4) +- assumption: A `ConvexError` thrown inside a component sub-transaction reaches the use case with its data intact, which Probe 2 showed on the pinned backend (F14, Probe 2) +- assumption: Transactions have limits, and they are one budget across nested calls and components, as the docs state for nested calls and Probe 4 showed for both (F13, Probe 4) ### Open questions - [non-blocking] OQ3: the largest order `PlaceOrder` supports is a product decision; the limit bullet below carries a placeholder and the first experiment measures with a provisional maximum (OQ3, D10) -- [non-blocking] Probe 4 pending: whether transaction limits add up across nested calls and components decides how large one use case may be and therefore where the too-large rule bites (Probe 4, F13, D10) - [non-blocking] Extension E-41: the doc says a too-large operation is rejected or becomes a separate import command with honest partial progress; it does not say what the import command records. The option taken here: the input bound is the declaration's `bounds.maxItems` of `spec:command.command-declaration` under E-38, checked by the pipeline's step 1 and by no body, the rejection is the outcome boundary's reserved `operationTooLarge`, and this extension owns only the import command, a caller-driven sequence of bounded batch commands that record progress on an `imports` row (E-41, E-38, D10) - [non-blocking] Extension E-44: the doc says keeping a refused request as a fact is a business policy chosen per command, and the executor signature is pinned by `spec:command.command-declaration`; what neither says is how a use case declares what a context's business failure means to it. The option taken here is a `BusinessFailurePolicy` per context call, carry or reject, applied inside the executor body through the pinned helper `carryOrReject` right after the context call, because the executor is a plain function and the declaration carries no slot for it; `reject` rolls back the events the context recorded as its business failure, which D4 says belong in history, and the doc's per-command choice is read as the use case's because D10 makes the use case the command, so the owner confirms that a context's business failure may be overridden by its caller (E-44, D4, D10) diff --git a/design/specs/application/read-models.sdp.md b/design/specs/application/read-models.sdp.md index f147653..06e84c7 100644 --- a/design/specs/application/read-models.sdp.md +++ b/design/specs/application/read-models.sdp.md @@ -40,11 +40,11 @@ This Spec carries the read-need table as rules, the properties of a projection, - risk: Every parent read of context data is a component call whose cost Probe 3 measures; if it breaks the read budgets, OQ1 changes the context component's shape and this Spec's defaults move with it (D8, F4, Probe 3, OQ1) - risk: A read model written in every command is one more write per command, counted against the command's budget (D8, Sc L2-3) - assumption: Queries are reactive; subscriptions synchronize state and are not durable event delivery (F7) -- assumption: A parent query calling a component query stays reactive, and a component's paginated list, built with `paginator` from `convex-helpers` because the built-in `.paginate()` does not work in a component, stays contiguous across the boundary in the way Probe 5 shows (F15, Probe 5, S4) +- assumption: A parent query calling a component query stays reactive, which the docs state and Probe 5 showed, and a component's paginated list, built with `paginator` from `convex-helpers` because the built-in `.paginate()` does not work in a component, stays contiguous across the boundary when each page is pinned by its end cursor, as Probe 5 showed on the pinned backend (F15, Probe 5, S4) ### Open questions -- [non-blocking] Probe 5 pending: a parent query over component queries must stay reactive, and the probe shows whether a component list's `paginator` page relayed by the parent stays contiguous under live writes when the client passes the end cursor; until then the small cross-context view is designed as a parent query over a bounded number of component reads (Probe 5, F15, S4, D8) +- [non-blocking] Probe 5 ran on 2026-10-01: pinned pages stay contiguous under live writes, and the `usePaginatedQuery` hook of `convex-helpers/react` lost rows after a page capped by `maximumRowsRead` split; until slice S2 decides how a context list is capped, the small cross-context view stays a parent query over a bounded number of component reads (Probe 5, F15, S4, D8) - [non-blocking] Probe 3 pending: the cost of one component call in latency and function-call quota decides whether one entity's detail stays a component query or hot reads move, which is OQ1 (Probe 3, F4, OQ1) - [non-blocking] Extension E-45: the doc says a small cross-context view is a parent query over bounded component reads and does not give the bound; the option taken here is at most 8 component reads per parent query and at most 4 read-model rows written per command, both measured by the first experiment (E-45, D8) @@ -114,4 +114,4 @@ And the number of workers, jobs or queues that ran is {workers:number} - A reviewer confirms that every read need in the first experiment maps to one row of the read-need table and names its default. - A reviewer confirms that no scheduled function, cron, action or component maintains a read model on the core path. - A reviewer confirms that every read-model index leads with `tenantId` and that every query over a read model takes `tenantId` in its `args`. -- A reviewer confirms that F15 is named as an assumption with Probe 5 wherever a parent query calls a component query. +- A reviewer confirms that F15 is cited wherever a parent query calls a component query. diff --git a/design/specs/application/rebuild.sdp.md b/design/specs/application/rebuild.sdp.md index f5669f4..52442a7 100644 --- a/design/specs/application/rebuild.sdp.md +++ b/design/specs/application/rebuild.sdp.md @@ -47,7 +47,7 @@ The registry, the per-entity marker and the gate document are pinned by `spec:ap - risk: The batch bound and the runner are design choices under the transaction limits until Probe 6 shows how a batch races a live command and whether the migrations component fits (F13, F17, Probe 6) - assumption: Mutations are serializable under optimistic concurrency, so a backfill batch and a live command that touch the same row serialize and the loser retries (F1) - assumption: `@convex-dev/migrations` fits a generation backfill; its product page states resumable batches with saved progress, resume from the last checkpoint and running over a subset, and states no batch size, so the batch bound below is this design's own under E-40 and F13, and whether a migration can iterate a table inside a component is what Probe 6 shows (F17, Probe 6) -- assumption: A parent query calling a component query stays reactive, so switching the active generation in the registry re-runs every subscription that reads it, and the enumeration's `paginator` page relayed by the parent stays contiguous under live writes; this is F15, assumed, and Probe 5 settles it (F15, Probe 5, S4) +- assumption: A parent query calling a component query stays reactive, so switching the active generation in the registry re-runs every subscription that reads it, and the enumeration's `paginator` page relayed by the parent stays contiguous under live writes; this is F15, and Probe 5 showed both on the pinned backend (F15, Probe 5, S4) - assumption: Scheduling from a mutation commits with it, and a scheduled mutation retries internal errors and runs once (F8, F9) - assumption: Backups exclude pending scheduled functions, so a restore drops the batch that was driving a generation and leaves its row at the checkpoint; the restore procedure's inventory and resume are the recovery (F12, E-42) diff --git a/design/specs/application/restore.sdp.md b/design/specs/application/restore.sdp.md index 33304b2..385b3ad 100644 --- a/design/specs/application/restore.sdp.md +++ b/design/specs/application/restore.sdp.md @@ -103,7 +103,7 @@ The restore itself is a platform operation outside any mutation. Everything the - doorExemption: `startRestoreRun`, `streamCheckBatch`, `readModelCheckBatch`, `receiptCheckBatch`, `pendingWorkBatch` and `finishRestoreRun` do not read the gate through `assertWritable` or `gateAllows` and write only `restoreRuns`, except that `finishRestoreRun`'s accepted branch also patches the generation rows it resumes; `startRestoreRun` refuses when `MAINTENANCE_MODE` is not `restore`, and each later mutation refuses when its run is not `running`, so no operational row is written outside a restore and no business table is written by them at all (E-42, D19) - checkOrder: journal, domain, read model, receipt, then the inventory; a later check does not start while an earlier one has findings, because a broken journal makes the fold check meaningless, and the inventory runs last because it is not a check and its items never fail the run (E-42, D3) - gapRecord: the run record's `backupTakenAt` is the gap start; the procedure writes no synthetic receipt and invents no event for the gap (D19, Existing systems) -- limitCheckBatch: at most `min(25, floor(2 MiB / budgetBytes))` streams per stream-check batch, which is 8 at the default 256 KiB stream budget and 25 for a stream type whose budget is 80 KiB or less, and at most the journal's `maxFoldEvents` of 600 events folded per batch, enforced through `maxEvents` rather than by a stop after the fold that crossed a bound; the worst case is therefore one 600-event fold of at most 17 KiB per event, about 10 MiB, plus the `list` page of at most 2 MiB of stream rows, plus the same rows read again when each `rebuild` re-reads its row before folding, at most 2 MiB, plus the run record, about 14 MiB under the assumption that limits add up across the nested queries until Probe 4 answers, below the 16 MiB read ceiling; a typical batch folds far less, and the 1 second timeout binds first for a batch that folds one full tail, which is why the check is a resumable batch; 25 receipts per receipt batch, each one component call plus one `list` call; 100 rows per read-model batch with one `list` call capped by the context's byte-derived page; the per-stream calls are the cost Probe 3 measures (E-42, E-24, E-25, F13, S3, S9, Probe 3, Probe 4) +- limitCheckBatch: at most `min(25, floor(2 MiB / budgetBytes))` streams per stream-check batch, which is 8 at the default 256 KiB stream budget and 25 for a stream type whose budget is 80 KiB or less, and at most the journal's `maxFoldEvents` of 600 events folded per batch, enforced through `maxEvents` rather than by a stop after the fold that crossed a bound; the worst case is therefore one 600-event fold of at most 17 KiB per event, about 10 MiB, plus the `list` page of at most 2 MiB of stream rows, plus the same rows read again when each `rebuild` re-reads its row before folding, at most 2 MiB, plus the run record, about 14 MiB against the one budget that nested queries share, as Probe 4 showed, below the 16 MiB read ceiling; a typical batch folds far less, and the 1 second timeout binds first for a batch that folds one full tail, which is why the check is a resumable batch; 25 receipts per receipt batch, each one component call plus one `list` call; 100 rows per read-model batch with one `list` call capped by the context's byte-derived page; the per-stream calls are the cost Probe 3 measures (E-42, E-24, E-25, F13, S3, S9, Probe 3, Probe 4) - limitFoldPerStream: a fold check reads the events since the latest baseline, at most the journal's `maxFoldEvents` of 600 and never more than the batch's remaining `maxEvents`; the `rebuild` query reads the stream row and refuses before reading an event, with `tailTooLong` above the fold bound, which is a finding of the journal check and not a bigger batch, and with `budgetExhausted` above the batch's remaining budget, which is the resume point of the next batch and never a finding (E-42, E-25, F13, D5) - limitPendingWorkBatch: 100 `imports` rows per inventory call, one gate document and at most three registry index ranges per declared read model on the first call, all far below the ceilings; a generation in `building`, `verifying` or `verified` is at most one per read model, so the registry part is bounded by the number of declared read models (E-42, F13) - limitFindingsKept: at most 100 findings per check on the run record; further findings are counted, so the record stays under the document limit, and at most 200 pending-work items are kept the same way (E-42, F13) diff --git a/design/specs/command/command-pipeline.sdp.md b/design/specs/command/command-pipeline.sdp.md index 335a71a..e31632f 100644 --- a/design/specs/command/command-pipeline.sdp.md +++ b/design/specs/command/command-pipeline.sdp.md @@ -52,15 +52,15 @@ The pipeline is one code path with two registered entries per command. The publi - risk: Every read of context data from the parent is a component call whose cost Probe 3 measures; if it breaks the budgets, the shape of the context call changes, not the order of the steps (D2, Probe 3, OQ1) - assumption: Mutations are serializable under optimistic concurrency, and component calls commit or roll back with the calling mutation (F1, F2) - assumption: `ctx.runMutation` inside a mutation gives partial rollback and the parent can catch and continue; the pipeline relies on it only by never catching on the public path (F3) -- assumption: A `ConvexError` thrown inside a context operation reaches the pipeline's outermost catch with its `data` intact, which `rejectionRelay` rests on; this is F14, assumed, and Probe 2 settles it (F14, Probe 2) +- assumption: A `ConvexError` thrown inside a context operation reaches the pipeline's outermost catch with its `data` intact, which `rejectionRelay` rests on; this is F14, which Probe 2 showed on the pinned backend (F14, Probe 2) ### Open questions - [non-blocking] Extension E-30: the doc names the outcome, the operation ID and the affected stream versions as what a command returns, but not a wire shape; the shape here is a union discriminated by `replayed`, whose executed member carries the declaration's result and whose replayed member carries `result` null because a receipt stores no result, so the entry's returns validator admits a replay as a value; affected IDs are application IDs, never private document IDs (E-30, D2, D6, D8) - [non-blocking] Extension E-31: the doc says an operation's ID ties together everything one request caused but not how it is minted, and it says a derived command carries causation and an event's `causedBy` may be a triggering event without saying how that cause reaches the pipeline; here the pipeline mints the operation ID once per top-level mutation with `crypto.randomUUID()` before the first context call, a duplicate returns the operation ID stored on the receipt, and the internal entry takes an optional `causedBy` argument that step 7 copies onto the `OperationRef` when present, so a reaction that issues a derived command passes `causedBy` of kind `event` naming its publication event and the envelope's `event` variant has exactly one producer, while an entry called without it mints `causedBy` of kind `command` from the declaration's name; the obligation and attempt behind a derived command are found through its receipt, whose request key is the obligation's effect key, and never through the envelope (E-31, E-54, D2, D13, Vocab: operation) - [non-blocking] Extension E-32: the doc places rate admission for new intent after the duplicate check and makes a rate or capacity refusal a retryable error, but names no admission mechanism; here admission is an optional per-command policy in the declaration, absent by default, evaluated inside the mutation so that a refusal that throws leaves no consumption behind; the flip side is that a consumption the policy writes commits only with the command, so the policy counts committed intent and cannot bound refused or unauthorized traffic, and a product that must bound refused traffic needs the dispatcher form of D7, where the parent survives the body's rejection (E-32, D4, D6, D7) -- [non-blocking] Extension E-33: the doc lists the callers that need receipts and leaves UI commands to the client guarantee; here a public caller may still supply a request key to opt into a receipt, which Probe 1 may make necessary for the HTTP client, and the internal entry always requires one (E-33, D6, Probe 1) -- [non-blocking] Probe 4 pending: whether transaction limits add up across the parent and its component sub-transactions decides whether the per-command bounds are checked against one budget or several; `ctx.runMutation` accepts a `transactionLimits` option that the design does not use until the probe answers (Probe 4, F13, S3) +- [non-blocking] Extension E-33: the doc lists the callers that need receipts and leaves UI commands to the client guarantee; here a public caller may still supply a request key to opt into a receipt, which Probe 1 showed a caller of `ConvexHttpClient` needs when it retries, and the internal entry always requires one (E-33, D6, Probe 1) +- [non-blocking] Probe 4 ran on 2026-10-01: the transaction limits are one budget across the parent and its component sub-transactions, so the per-command bounds are checked against one budget; `ctx.runMutation` accepts a `transactionLimits` option that the design does not use, and slice S1 decides whether a call reserves room with it (Probe 4, F13, S3) ## Workflow @@ -111,14 +111,14 @@ The pipeline depends on three contracts it does not own: the context component's - factsCapture: the executor is the one place that captures outside facts, such as an accepted price list, a currency or a policy version, from parent tables or configuration inside this transaction, and passes them as `facts` on each context call; they reach the decider through its decision context, never through `input`, so the fingerprint of step 5 excludes them (D3, E-22, E-36) - contextCall: `await ctx.runMutation(components.inventory.operations.allocate, { tenantId, actor, operation, input: { lines }, facts })`, one call per context named by the declaration, lists in and the operation outcome out, the DTO with the versions and one `streams` entry per planned stream with its DTO, version, created flag and the events the call appended; the `args` shape is the one `spec:context.context-component` fixes, `operation` is the `OperationRef` of `spec:context.event-envelope` minted at step 7, and the return is the `OperationOutcome` of `spec:context.persistence-adapter` (D10, F2, E-22, E-23, E-26) - contextCallIsolation: each component call is a sub-transaction isolated from the other calls; a throw inside it rolls that sub-transaction back, and uncaught it rolls the parent back, so the parent never observes a half-applied context (F2, F3, S2) -- rejectionRelay: a `ConvexError` carrying the kernel's `Rejection` thrown inside a component reaches the pipeline with its data intact under F14, which is assumed; `normalizeThrown` at the handler's outermost level rethrows it as the boundary's `RejectionData`; if Probe 2 refutes F14, contexts return the rejection as a value and the pipeline throws it through the same helper, with the wire shape unchanged (F14, Probe 2, D7) +- rejectionRelay: a `ConvexError` carrying the kernel's `Rejection` thrown inside a component reaches the pipeline with its data intact under F14, which Probe 2 showed on the pinned backend; `normalizeThrown` at the handler's outermost level rethrows it as the boundary's `RejectionData`; the fallback for a refuted F14, contexts returning the rejection as a value, is not needed (F14, Probe 2, D7) - readModelWrites: parent-owned read-model tables written by the pipeline at step 9, one `applyProjection`, `applyAggregate` or `applyHistoryProjection` call per matching `streams` entry per binding the declaration lists, with the generations `generationsToWrite` returns; the executor holds the DTOs and the appended events only long enough to return them and writes no read-model table, so the declaration's list is the one wiring fact for read models (D8, D9, D12, E-7, E-9) - writesCheck: `decl.writes` is proven at step 9 against the `streams` entries the executor returned, in memory and with no extra read; an entry outside the list throws a plain error, so a declaration whose `writes` understates its executor fails its first native run rather than passing a closed source scope at step 7 (E-8, E-38, Law 6) - auditWrite: `writeAudit` of `spec:operations.baseline-operations` at step 10 for a declaration whose `audit` field is set, inside the transaction and after the receipt insert; its failure throws and the command rolls back (D19, E-43, Sc ALL-1) - diagnosticEmit: `emitDiagnostic` of `spec:operations.baseline-operations` at step 11, after every business write and inside a try and catch that swallows every error, so no diagnostic failure reaches the mutation (D19, E-43, Sc ALL-1) - receiptWrites: the `receipts` table of `spec:command.receipt-table`, read at step 5 by `by_key` and inserted at step 10 in the same mutation (D6) - admissionHook: `admission?: (ctx: MutationCtx, call: PipelineCall) => Promise<{ admitted: true } | { admitted: false; code: "rateLimited" | "capacity"; retryAfterMs?: number }>` on the declaration, absent by default; a refusal throws the transient error and any consumption it wrote rolls back with the throw, as it does with every later throw of the same mutation, so an admitted call that is then rejected leaves no consumption either (D6, D4, E-32) -- subTransactionLimits: `ctx.runMutation` accepts `{ transactionLimits }` per call; the design passes none until Probe 4 says whether limits add up across the boundary (S3, Probe 4, F13) +- subTransactionLimits: `ctx.runMutation` accepts `{ transactionLimits }` per call; the limits are shared across the boundary, as Probe 4 showed, and the design passes none until slice S1 decides whether a call reserves room for the work after it (S3, Probe 4, F13) - limitMutationTimeout: the whole pipeline runs under the 1-second query and mutation timeout; every per-command bound in a declaration is chosen so that one command's reads and writes finish well inside it (F13) - limitTransaction: 16 MiB read, 16 MiB written, 32,000 documents scanned, 4,096 index ranges read, 16,000 documents written per transaction; the per-command `bounds` in the declaration, such as the maximum order lines, are ceilings chosen against these, and the largest order is a product decision (F13, D10, OQ3) - limitContextCalls: one `ctx.runMutation` per context named by the declaration per command; O(N) business work inside a call is fine, O(N) calls is not (D10) diff --git a/design/specs/command/idempotency-and-receipts.sdp.md b/design/specs/command/idempotency-and-receipts.sdp.md index 41ef1cd..a0cf994 100644 --- a/design/specs/command/idempotency-and-receipts.sdp.md +++ b/design/specs/command/idempotency-and-receipts.sdp.md @@ -32,14 +32,14 @@ This Spec owns the rules: where receipts are required, how the key and the finge - outcome: Each business intent executes once: the client guarantee covers UI commands, a receipt covers every receipted caller, a create carries a client-generated ID, and a rejected or transiently refused command stores nothing so its key stays free (D6, D7) - value: Public UI commands carry no receipt write, and where receipts exist they are thin, so no result format has to stay decodable for the retry window and the client reads state through reactive queries anyway (D6) - risk: The standing cost is one `receipts` table in the parent, one indexed read and one insert per receipted command, an explicit expiry policy and a tombstone for irreversible commands (D6) -- risk: The exact set of callers that need receipts depends on where the client guarantee ends, which Probe 1 has not yet shown; the internal entry requires a key and the public entry accepts one, so a wrong guess costs a receipt, never a double execution (D6, Probe 1) +- risk: The exact set of callers that need receipts depends on where the client guarantee ends; Probe 1 showed that it does not reach a caller of `ConvexHttpClient` that retries after a lost response; the internal entry requires a key and the public entry accepts one, so such a caller executes twice unless it supplies a key (D6, Probe 1) - assumption: The React client retries mutations until confirmed and the backend executes each call once (F5) - assumption: React and Rust clients run one client's mutations one at a time, in order (F6) - assumption: Mutations are serializable under optimistic concurrency, which is what makes the read-check-insert pair serialize (F1) ### Open questions -- [non-blocking] Probe 1 pending: it must show where the client guarantee ends, for a tab closed with a pending mutation, a server restart and the HTTP client; the answer fixes whether the HTTP client's callers must supply a request key on the public entry (Probe 1, D6, E-33) +- [non-blocking] Probe 1 ran on 2026-10-01: a closed client leaves its mutation executed once or not at all, a backend restart leaves it executed once, and `ConvexHttpClient` does not retry, so a caller's retry is a second execution; whether the public entry requires a request key from callers of the HTTP client, or leaves it optional as now, is the owner's ruling (Probe 1, D6, E-33) - [non-blocking] Extension E-33: a public caller may supply a request key to opt into a receipt, and the internal entry always requires one; the doc lists the callers that need receipts and does not say whether a UI caller may opt in (E-33, D6) - [non-blocking] Extension E-34: the doc says expiry is explicit and names no number; here the default retry window is 7 days per command, expiry is checked lazily at lookup so the core path needs no job, and a bounded per-tenant sweep deletes expired receipts in batches of at most 1,000; the request key is bounded to 256 characters and a receipt to 1,000 affected refs (E-34, D6, D19, F13) - [non-blocking] Extension E-36: the doc says the fingerprint covers business input and its contract version; here it is the SHA-256 digest, computed with the Convex runtime's `crypto.subtle.digest`, over the canonical JSON of the business input with object keys sorted, followed by the contract version; the outside facts the executor captures travel beside the input and are excluded, so a retry that captures a fresher fact is not a conflict, which the decider contract cites; a change to the canonical form is a contract version bump (E-36, D6, D3) diff --git a/design/specs/command/outcome-boundary.sdp.md b/design/specs/command/outcome-boundary.sdp.md index 1d52a58..ecd8368 100644 --- a/design/specs/command/outcome-boundary.sdp.md +++ b/design/specs/command/outcome-boundary.sdp.md @@ -29,7 +29,7 @@ The closed code list has two halves. The platform codes below are closed here an - outcome: Pin the wire shape of the four outcomes and the closed error-code list, so that a client can tell a rejection from a transient refusal from a technical failure without parsing messages, and so that nothing but applied and business failure ever commits (D4, D7) - value: A lost rejection followed by a retry is correct by construction, the public path carries no nested mutation and no rejection receipt, and a reviewer can check the code list against the declarations in one place (D7) -- risk: The data of a `ConvexError` thrown inside a component is assumed to arrive intact; until Probe 2 runs, the pipeline also throws on a rejection returned as a value, which costs nothing but keeps the boundary correct under either answer (F14, Probe 2) +- risk: The data of a `ConvexError` thrown inside a component arrives intact on the pinned backend, as Probe 2 showed, and no Convex page promises it, so a later release could change it; the pipeline still throws on a rejection returned as a value, which costs nothing, until slice S1 decides whether that path stays (F14, Probe 2) - assumption: `ctx.runMutation` inside a mutation gives partial rollback and the parent can catch and continue, which the conditional dispatcher relies on (F3) - assumption: `ConvexError` data survives a nested mutation and a component boundary (F14) @@ -38,7 +38,6 @@ The closed code list has two halves. The platform codes below are closed here an - [non-blocking] Extension E-5: the doc fixes the four outcomes and that a rejection is a structured `ConvexError`, but not the data shape or the code list; the shape and the platform codes below are the design's, and the domain codes are closed per declaration (E-5, D4, D7) - [non-blocking] Extension E-35: the write pause of D9 refuses writers in its scope while a cross-stream history view rebuilds; the doc does not say how a refused writer is answered; this contract reserves the transient code `writePaused` for Package D so the refusal is retryable and stores nothing, which Package D confirms or replaces (E-35, D9, D4) - [non-blocking] OQ2: whether the product needs a record of refused commands anywhere, for security audit or agent proposals, decides whether the generic internal dispatcher below exists; until then it is a conditional design and no command uses it (OQ2, D7) -- [non-blocking] Probe 2 pending: it must show that a `ConvexError` thrown inside a context operation reaches the parent and the client with its `data` intact; until it does, the rejection relay rests on an assumed fact (Probe 2, F14) ## Contract @@ -50,7 +49,7 @@ The closed code list has two halves. The platform codes below are closed here an - Keeping a refused request as a fact is a business policy chosen per command; a command that waits for stock is a different command from one that rejects when stock is short, and this boundary never makes that choice by accident (D4) - A rejection thrown inside a context operation rolls that sub-transaction back; the pipeline's outermost catch rethrows it as `RejectionData`, so the whole mutation still commits nothing and the caller reads one wire shape whether the rejection came from a context or from the pipeline (F3, D7, F14) - The outermost catch never continues, never wraps anything but a `ConvexError` carrying the kernel's `Rejection`, and passes every other throw through unchanged, so it is not the catch-and-continue that D7 reserves for the nested mutation (D7, Law 6) -- If Probe 2 shows that `ConvexError` data does not survive the component boundary, contexts return the kernel's rejection outcome as a value instead and the pipeline throws it through the same `reject` helper; the wire shape is unchanged either way (D7, Probe 2, F14) +- Probe 2 showed that `ConvexError` data survives the component boundary, so contexts throw the kernel's rejection; had it not, contexts would return the rejection outcome as a value and the pipeline would throw it through the same `reject` helper, with the wire shape unchanged (D7, Probe 2, F14) - If the response to a rejection is lost and the command runs again, the retry runs against current state and nothing of the first attempt remains; if it now succeeds, the intent ran once (D7, Sc L1-12) - A transient refusal or a rejection never leaves a receipt, so its idempotency key stays unused and the same key can carry a later successful execution (D6, D7) - The code list is closed: the platform codes are the `errorCode` entries below, the domain codes are the `rejections` a command's declaration names, and a code outside both is a technical failure by definition (D4, D12, E-5) diff --git a/design/specs/command/tenancy-and-authority.client-claims-system-namespace.sdp.md b/design/specs/command/tenancy-and-authority.client-claims-system-namespace.sdp.md index f29ebf8..a33c54f 100644 --- a/design/specs/command/tenancy-and-authority.client-claims-system-namespace.sdp.md +++ b/design/specs/command/tenancy-and-authority.client-claims-system-namespace.sdp.md @@ -31,4 +31,4 @@ And the namespace the server assigned is {namespace: "none"} - Runs in the native tier; every test owns its disposable backend. - The test asserts that no receipt, event or read-model row was written and that the error is not a `ConvexError` of the outcome boundary. -- The test also calls the internal entry from the client with namespace `agent` and asserts that Convex refuses the call, so both routes to a system namespace are closed. +- The test also calls the internal entry from the same ordinary client, which holds no admin key, with namespace `agent` and asserts that Convex refuses the call, so both routes to a system namespace are closed. diff --git a/design/specs/command/tenancy-and-authority.sdp.md b/design/specs/command/tenancy-and-authority.sdp.md index 99c13fa..bd4f743 100644 --- a/design/specs/command/tenancy-and-authority.sdp.md +++ b/design/specs/command/tenancy-and-authority.sdp.md @@ -71,7 +71,7 @@ Nothing here signs anything. The internal entry is unreachable by clients becaus - clientClaimRefused: a `namespace` or `actor` field sent to the public entry fails its `args` validator, and a client call to the internal entry is refused by Convex before any handler runs (D11, Sc L1-7) - workerModes: the two modes of the one `Authority` shape pinned in `spec:command.actor-and-scope`, never restated here: `recheckDelegator` re-reads the delegating user's grants at execution through `authorize` with the captured actor and scope, and `serviceAuthority` runs under the service actor's own grants with no recheck; both carry the obligation on the actor's `delegationRef` (D11, D13, E-6) - trustBoundary: one trusted deployment; component privacy and type brands, never a signed token, and no claim against a malicious administrator (D11) -- nativeTierIdentity: on the native tier the identity `ctx.auth.getUserIdentity()` returns comes from the local backend's admin key acting as an identity whose `issuer` and `subject` are the ones production's `auth.config.ts` names, so `establishActor`, the grants read and the namespaces run unchanged; the token's source is the one named difference from production, recorded under E-13 on `spec:platform.acceptance-contract` (E-13, S13, Sc L2-9) +- nativeTierIdentity: on the native tier the identity `ctx.auth.getUserIdentity()` returns comes from a token the harness signs for the fixture issuer that the deployment's environment variables name, carried by an ordinary client, so `establishActor`, the grants read, the namespaces and Convex's visibility check run unchanged; the issuer is the identity source of a native run and its one named difference from production (E-13, S13, Sc L2-9) - limitGrantsPerPrincipal: at most 500 grants read per principal per tenant per command; the read takes 501 and a 501st row throws a plain `Error`, a technical failure, never a silent truncation, because `.take(500)` alone cannot show that a 501st exists; a principal with more is a product problem the failure surfaces (F13, E-37) ## Example space diff --git a/design/specs/context/batch-shaped-api.sdp.md b/design/specs/context/batch-shaped-api.sdp.md index 68c02b3..39a18c0 100644 --- a/design/specs/context/batch-shaped-api.sdp.md +++ b/design/specs/context/batch-shaped-api.sdp.md @@ -18,12 +18,11 @@ Context APIs take lists, such as `inventory.allocate({ lines })`, so a use case - outcome: Every context operation takes and returns lists, a use case makes one call per context, and a list above the operation's declared bound is rejected rather than split (D10) - value: The first experiment can count one call per context per use case and hold it for orders of 1 line, 10 lines and the maximum (D10, Sc L2-3) -- risk: Whether the transaction limits add up across nested calls and components is Probe 4's question; until it answers, every bound assumes the worst case, that they add up (F13, Probe 4) +- risk: The transaction limits are one budget across nested calls and components, as Probe 4 showed, so every bound here is checked against the sum (F13, Probe 4) - assumption: Transactions have limits, and platform limits are ceilings, not batch sizes (F13, D19) ### Open questions -- [non-blocking] Probe 4 pending: whether transaction limits add up across nested calls and components decides how large one use case may be; the bounds here assume they do (Probe 4, F13, D10) - [non-blocking] OQ3: the largest order the placement command supports is a product decision; each operation's `maxStreams` is set from it and the default ceiling of 256 stands until then (OQ3, D10) ## Rule diff --git a/design/specs/context/context-component.sdp.md b/design/specs/context/context-component.sdp.md index 2166f62..8dee353 100644 --- a/design/specs/context/context-component.sdp.md +++ b/design/specs/context/context-component.sdp.md @@ -109,7 +109,7 @@ One component per context, one mount per context in the parent, and the parent a - registeredFunctions: per context, one mutation per operation plus four queries per stream type plus one operation query plus one maintenance mutation; a context with two operations and one stream type registers eight functions (Decision method rule 2, E-22) - limitStreamsPerCall: 256 streams per operation call unless the operation declares a lower `maxStreams`, and in bytes the planned streams' `budgetBytes`, each the whole stream with head and parts together, sum to at most 8 MiB, which is 32 streams at the default 256 KiB budget, 16 for a derived stream type at the 512 KiB cap and 256 streams once a stream type declares 32 KiB or less; above either bound the adapter rejects with `operationTooLarge` before any read (D10, F13, E-2, E-22) - limitDocumentsWrittenPerCall: 800 documents and 8 MiB written per operation call, counting stream rows at their budgets, events at their serialized size and parts, one twentieth of the 16,000 documents and half of the 16 MiB ceiling so the parent's receipts, read models and other contexts fit in the same transaction; the arithmetic is the adapter's `limitDocumentsWrittenPerCall` (F13, D19, E-12, E-22) -- limitReadsPerCall: one stream row plus its parts plus one index range per stream, so a call at the stream bound reads under 300 documents for single-mapping streams and at most 33 per stream, under 8,500, for derived ones, against the 32,000 ceiling, and, by the byte bound above, under 8 MiB of stream documents against the 16 MiB read ceiling; the bounds assume that limits add up across the parent and its components until Probe 4 answers (F13, Probe 4, E-2, E-22) +- limitReadsPerCall: one stream row plus its parts plus one index range per stream, so a call at the stream bound reads under 300 documents for single-mapping streams and at most 33 per stream, under 8,500, for derived ones, against the 32,000 ceiling, and, by the byte bound above, under 8 MiB of stream documents against the 16 MiB read ceiling; the limits are one budget across the parent and its components, as Probe 4 showed (F13, Probe 4, E-2, E-22) - historyMode: `history: "rebuildable" | "auditOnly"` on the journal; an audit-only context registers no `rebuild` query and its documentation never advertises rebuild from events (D5) - crossContextReference: `type EventRef = { tenantId: string; contextId: string; eventId: string }` is the only shape a context stores about another context's events (D2) - probeDependency: the shape of this component is what Probe 3 and OQ1 test; a result that breaks the read budgets changes where hot reads land before Layer 3 is designed (F4, Probe 3, OQ1) diff --git a/design/specs/context/queries.sdp.md b/design/specs/context/queries.sdp.md index 43a12b9..dc46892 100644 --- a/design/specs/context/queries.sdp.md +++ b/design/specs/context/queries.sdp.md @@ -17,13 +17,13 @@ relations: Layer 1 · Detail: full · Traces: D2, D5, D8, D11, D19, Law 5, Law 11, F11, F13, F15, Probe 5, S4, E-6, E-24. -A context exposes authorized queries that return DTOs. The parent authenticates and authorizes, then calls a context query through the component API with the tenant and the scope as arguments; the context reads its own tables by index and maps state to the DTO its registration declares, so its private schema stays private. The queries serve the first two rows of the read-need table, one entity's detail and an essential list, plus journal inspection and the rebuild check the first experiment needs. Pagination inside the component uses `paginator` from `convex-helpers`, because the components page states that the built-in `.paginate()` does not work in a component; the parent relays the helper's page, and Probe 5 checks reactivity and the helper's end-cursor behavior across the boundary. +A context exposes authorized queries that return DTOs. The parent authenticates and authorizes, then calls a context query through the component API with the tenant and the scope as arguments; the context reads its own tables by index and maps state to the DTO its registration declares, so its private schema stays private. The queries serve the first two rows of the read-need table, one entity's detail and an essential list, plus journal inspection and the rebuild check the first experiment needs. Pagination inside the component uses `paginator` from `convex-helpers`, because the components page states that the built-in `.paginate()` does not work in a component; the parent relays the helper's page, and Probe 5 showed reactivity and the helper's end-cursor behavior across the boundary on the pinned backend. ## Intent - outcome: Pin the query surface of a context, its arguments, its DTO mapping and how pagination crosses the boundary (D8, E-24) - value: One entity's detail and an essential list are served by an authorized indexed query with no projection job, and a client subscribed to the parent's query sees committed state or later (D8, F7) -- risk: Reactivity of a parent query over a component query is assumed until Probe 5 runs, and whether the helper's page, which does not subscribe to its end cursor, stays contiguous under live writes across the boundary is the same probe's question (F15, Probe 5, S4) +- risk: The helper's page does not subscribe to its end cursor; Probe 5 showed pinned pages contiguous under live writes across the boundary, and showed the `convex-helpers/react` hook leaving out the rows between a capped page's continue cursor and its end cursor after a split, so a list capped by `maximumRowsRead` can lose rows on the client without a sign (F15, Probe 5, S4) - risk: A reactive re-run of a page carries the page's end cursor, and the helper then reads to that cursor whatever `numItems` says, so a page whose index range fills with live inserts is bounded only by the `maximumRowsRead` and `maximumBytesRead` the query passes; a capped page comes back as `SplitRequired` with a split cursor, which costs the client one more query per split (S4, F13, E-24) - risk: A fold reads every event since the latest baseline and deep-compares the result inside one query, so the 1 second query timeout binds before the 16 MiB read ceiling; the fold bound is provisional until the first experiment and the L2-7 example measure it (F13, E-25) - assumption: A parent query calling a component query stays reactive (F15) @@ -32,7 +32,7 @@ A context exposes authorized queries that return DTOs. The parent authenticates ### Open questions -- [non-blocking] Probe 5 pending: a parent query over component queries must stay reactive, and the probe shows whether the `paginator` page relayed by the parent stays contiguous under live writes when the client passes the end cursor as `convex-helpers` describes, and whether the split path, a `SplitRequired` page whose `splitCursor` the `convex-helpers/react` hook turns into two pages, works across the boundary; the mechanism and the bound are documented in the helper's source, so the probe asks neither whether pagination crosses the boundary nor whether the bound exists (Probe 5, F15, S4, D8, E-24) +- [non-blocking] Probe 5 ran on 2026-10-01 on the pinned backend with `convex-helpers` 0.1.124: the parent query stays reactive, pinned pages stay contiguous, and a `SplitRequired` page crosses the boundary with its `splitCursor`; the `convex-helpers/react` hook then showed 40 of 45 rows, because it ends the second half at the capped page's `continueCursor`; slice S2 decides before it builds a list whether a context list sets `maximumRowsRead`, who closes the range to the page's end cursor, or which hook the client uses (Probe 5, F15, S4, D8, E-24) - [non-blocking] Extension E-24: the doc says component queries are authorized and return DTOs; this Spec fixes the four queries per stream type, the operation query, the `scope` argument, the `toDto` mapping with the stream version on every DTO, pagination through `paginator`, the byte-derived page bound passed as `maximumRowsRead` and `maximumBytesRead` so that it holds on a reactive re-run with an end cursor, which the helper's source shows ignores `numItems`, the relay of `pageStatus` and `splitCursor` unchanged, the `maxEvents` argument of the `rebuild` query and its `budgetExhausted` refusal for a batching caller, and the fold refusal of the `rebuild` query; the owner confirms (D8, S4, E-24, E-42) ## Contract @@ -73,7 +73,7 @@ The four stream queries are built once per stream type by `defineStreamQueries`, - parentPassThrough: `export const listOrders = query({ args: { tenantId: v.string(), paginationOpts: paginationOptsValidator }, returns: paginationResultValidator(orderDtoValidator), handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (identity === null) throw new ConvexError({ code: "unauthenticated", message: "sign in" }); const actor: Actor = { kind: "human", id: identity.tokenIdentifier }; const decision = await authorize(ctx, { actor, tenantId: args.tenantId, permission: "orders.read" }); if (!decision.allowed) throw new ConvexError({ code: "forbidden", message: decision.reason }); const scope: TenantScope = { tenantId: args.tenantId }; return ctx.runQuery(components.orders.queries.order.list, { tenantId: args.tenantId, scope, paginationOpts: args.paginationOpts }); } })` where the tenant is the `tenantId` argument, never inferred from the identity, `ctx.auth` is read once for the actor only as the pipeline's public entry does, `authorize` is the helper of `spec:command.actor-and-scope` reading the grants in the same transaction, and the `scope` is derived from the authorized tenant; the client subscribes through `usePaginatedQuery` from `convex-helpers/react`, because the built-in hook cannot maintain the pagination journal across the component boundary (D11, Law 11, Law 5, F15, S4, E-6, E-24) - dtoMapping: `registration.toDto(state, meta)` is the only path from state to a DTO, so the private schema stays private; the DTO validator is the registration's and pins the shape, and every DTO carries `version: StreamVersion` built from `meta`, which the adapter's `streams` entries, the projection's version compare and the backfill all read (D8, D12, E-24) - scopeNarrowing: the query narrows rows to `scope` and never widens it; a DTO policy that hides fields from some actors is the context's, decided from `scope`, never from `ctx.auth` (Law 5, D11, F11) -- reactivityAssumption: a client subscription to the parent query re-runs when the component's tables change is F15, assumed; Probe 5 settles it (F15, Probe 5) +- reactivityAssumption: a client subscription to the parent query re-runs when the component's tables change; this is F15, which the docs state and Probe 5 showed (F15, Probe 5) - limitListPage: `min(200, floor(8 MiB / registration.mapping.budgetBytes))` rows per page, where `budgetBytes` is the whole stream, row and parts together, which is 32 rows at the default 256 KiB budget, 16 rows for a derived stream type at the 512 KiB cap and 200 rows for a stream type whose budget is 40 KiB or less, passed as the cap on `numItems` for the first run and as `maximumRowsRead` for every run, because a reactive re-run with an end cursor ignores `numItems`, so one page reads at most 8 MiB of stream documents, half the 16 MiB read ceiling, whatever the budget and however the range has filled; the query caps a larger `numItems` rather than refusing it, and a capped re-run returns `SplitRequired` (F13, E-2, E-24, S4) - limitListBytes: 8 MiB, passed as `maximumBytesRead` on every `list` page as the byte form of the same bound, so that the helper's own byte count stops a page that `maximumRowsRead` would not, such as rows that grew after the page bound was computed (F13, E-2, E-24) - limitHistoryPage: 500 events per page, which at the 16 KiB payload bound plus the envelope reads under 9 MiB, plus the baseline events the page spans at up to the stream's `budgetBytes` each, of which a stream carries one per change of meaning; passed as the cap on `numItems` and as `maximumRowsRead`, so a reactive re-run with an end cursor is bounded the same way (F13, E-2, E-12, E-24) diff --git a/design/specs/decisions/d06-idempotency-client-and-receipts.sdp.md b/design/specs/decisions/d06-idempotency-client-and-receipts.sdp.md index 80b3a52..d07168b 100644 --- a/design/specs/decisions/d06-idempotency-client-and-receipts.sdp.md +++ b/design/specs/decisions/d06-idempotency-client-and-receipts.sdp.md @@ -30,7 +30,7 @@ The Convex React client retries a mutation until it is confirmed, and the backen ### Open questions -- [non-blocking] Probe 1 pending: where the client guarantee ends, for a tab closed with a pending mutation, a server restart and the HTTP client; the answer fixes the exact list of callers that need receipts (Probe 1, D6) +- [non-blocking] Probe 1 ran on 2026-10-01: the guarantee holds across a backend restart, a closed client leaves its mutation executed once or not at all, and the HTTP client does not retry; `ConvexHttpClient` is a Convex client the guarantee does not reach, so whether its callers join the callers that need receipts is the owner's ruling (Probe 1, D6) ## Decision diff --git a/design/specs/decisions/d07-rejections-thrown-not-stored.sdp.md b/design/specs/decisions/d07-rejections-thrown-not-stored.sdp.md index 3dc23d4..d95fe78 100644 --- a/design/specs/decisions/d07-rejections-thrown-not-stored.sdp.md +++ b/design/specs/decisions/d07-rejections-thrown-not-stored.sdp.md @@ -28,7 +28,6 @@ A rejected command throws a structured `ConvexError`. The whole mutation rolls b ### Open questions -- [non-blocking] Probe 2 pending: `ConvexError` data must arrive intact through a nested mutation and a component boundary; until it does, the closed error-code list at the boundary rests on an assumed fact (Probe 2, F14) - [non-blocking] OQ2: whether the product needs a record of refused commands anywhere, for security audit or agent proposals, decides where the nested mutation is required and whether the generic internal dispatcher exists (OQ2, D7) ## Decision diff --git a/design/specs/decisions/d08-read-models-in-command.sdp.md b/design/specs/decisions/d08-read-models-in-command.sdp.md index 1fbdc41..b35cc76 100644 --- a/design/specs/decisions/d08-read-models-in-command.sdp.md +++ b/design/specs/decisions/d08-read-models-in-command.sdp.md @@ -21,13 +21,13 @@ The core path runs zero projection jobs and the simplest read that works wins. O - problem: A successful command followed by a query or subscription must show committed state without any worker; a projection job between the write and the read makes essential screens depend on queue recovery, and an invariant that reads such a view decides on stale data (Sc L2-2, Law 9) - outcome: The core path runs zero projection jobs; the simplest read that works wins, and after a successful command an authorized query sees the committed state or a later one (D8) - value: No event-to-WebSocket layer, no queue recovery for essential reads and no second copy of every field; reactive queries carry the freshness (D8, F7) -- risk: Every parent read of context data is a component call whose cost Probe 3 measures, and reactivity across that boundary is assumed until Probe 5 runs (D8, F4, F15) +- risk: Every parent read of context data is a component call; Probe 3 gave its cost a first reading on a local backend, and its hosted cost is still open (D8, F4, F15) - assumption: Queries are reactive; subscriptions synchronize state and are not durable event delivery (F7) - assumption: A parent query calling a component query stays reactive (F15) ### Open questions -- [non-blocking] Probe 5 pending: a parent query over component queries must stay reactive, and the probe shows how pagination crosses the boundary (Probe 5, F15, D8) +- [non-blocking] Probe 5 ran on 2026-10-01: a parent query over component queries stays reactive, and pagination crosses the boundary with pages pinned by their end cursors; the client hook lost rows after a capped page split, which slice S2 settles before it builds a list (Probe 5, F15, D8) ## Decision diff --git a/design/specs/decisions/d10-contexts-meet-in-parent-use-cases.sdp.md b/design/specs/decisions/d10-contexts-meet-in-parent-use-cases.sdp.md index 3cecba5..86b45a0 100644 --- a/design/specs/decisions/d10-contexts-meet-in-parent-use-cases.sdp.md +++ b/design/specs/decisions/d10-contexts-meet-in-parent-use-cases.sdp.md @@ -28,7 +28,6 @@ Contexts never read each other's tables and never call each other. A rule linkin ### Open questions -- [non-blocking] Probe 4 pending: whether transaction limits add up across nested calls and components decides how large one use case may be (Probe 4, F13, D10) - [non-blocking] OQ3: the largest order the placement command supports is a product decision; until it is made the limit is a placeholder in the first experiment (OQ3, D10) ## Decision diff --git a/design/specs/facts/f02-component-calls-commit-with-caller.sdp.md b/design/specs/facts/f02-component-calls-commit-with-caller.sdp.md index d8ee561..3aa0b56 100644 --- a/design/specs/facts/f02-component-calls-commit-with-caller.sdp.md +++ b/design/specs/facts/f02-component-calls-commit-with-caller.sdp.md @@ -21,4 +21,4 @@ The components page states that each mutation call to a component is a sub-trans - statement: Component calls commit or roll back with the calling mutation (F2) - flavor: convex-fact - target: evidence.status:documented -- measurableBy: S2 https://docs.convex.dev/components/understanding; doc status Documented, S2; no probe assigned; Probe 4 checks whether limits add up across the boundary (F2, D1, D2) +- measurableBy: S2 https://docs.convex.dev/components/understanding; doc status Documented, S2; no probe assigned; Probe 4 showed that the transaction limits are shared across the boundary (F2, D1, D2) diff --git a/design/specs/facts/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.sdp.md b/design/specs/facts/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.sdp.md new file mode 100644 index 0000000..8c45ad6 --- /dev/null +++ b/design/specs/facts/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.sdp.md @@ -0,0 +1,32 @@ +--- +id: spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f04-nested-calls-cost-more-than-helpers + verifies: spec:facts.f04-nested-calls-cost-more-than-helpers +--- +# Probe 3: the cost of a component call from a mutation + +Probe 3 · native tier · fixture composition. + +## Intent + +- outcome: One component call costs more than a helper call, by an amount the run records. (Probe 3, F4) + +```gwt +Given a parent mutation that reads the same document {reads: 50} times in a row through one of three paths: a helper function, a nested query, a component query +When a client calls the parent mutation once for each path, several times over +Then the median execution time through the component is {componentAgainstHelper: "higher"} than through the helper +And the local function log holds {recordsPerCall: 1} completion record for one parent call through the component, the parent's own +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The execution time is the one the backend's function log reports for the parent call; the run's evidence holds the three medians and the difference per call. +- The test checks that each path read the document the bound number of times before it accepts a sample. +- The size is recorded and not asserted. It is a local backend's time on one machine, a first reading for OQ1 and not a hosted deployment's cost. +- Function-call quota is not measured. A local backend has none, and its function log holds one completion record for the parent call and none for the calls inside it, which does not say how a hosted deployment counts them. That half of Probe 3 needs a hosted deployment and stays open on F4. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.sdp.md b/design/specs/facts/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.sdp.md new file mode 100644 index 0000000..49d172f --- /dev/null +++ b/design/specs/facts/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.sdp.md @@ -0,0 +1,33 @@ +--- +id: spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f04-nested-calls-cost-more-than-helpers + verifies: spec:facts.f04-nested-calls-cost-more-than-helpers +--- +# Probe 3: the cost of a component call from a query + +Probe 3 · native tier · fixture composition. + +## Intent + +- outcome: One component call costs more than a helper call, by an amount the run records. (Probe 3, F4) + +```gwt +Given a parent query that reads the same document {reads: 50} times in a row through one of three paths: a helper function, a nested query, a component query +When a client calls the parent query once for each path, several times over, with arguments that defeat the query cache +Then the median execution time through the component is {componentAgainstHelper: "higher"} than through the helper +And the local function log holds {recordsPerCall: 1} completion record for one parent call through the component, the parent's own +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The execution time is the one the backend's function log reports for the parent call, because the clock inside a query does not advance; the run's evidence holds the three medians and the difference per call. +- The test checks that each path read the document the bound number of times before it accepts a sample. +- A cached query also logs a completion record, with `cachedResult` true and a time near zero, so the test gives each call fresh arguments and counts a sample only when its record says the result was not cached. +- The size is recorded and not asserted. It is a local backend's time on one machine, a first reading for OQ1 and not a hosted deployment's cost. +- Function-call quota is not measured, for the reason the mutation example gives. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f04-nested-calls-cost-more-than-helpers.sdp.md b/design/specs/facts/f04-nested-calls-cost-more-than-helpers.sdp.md index 9e1c5e0..81a0051 100644 --- a/design/specs/facts/f04-nested-calls-cost-more-than-helpers.sdp.md +++ b/design/specs/facts/f04-nested-calls-cost-more-than-helpers.sdp.md @@ -10,7 +10,7 @@ relations: F4 · Status: rechecked · Doc status: Documented, S10, rechecked 2026-09-29; size unknown · Decisions: D2, D7. -The page states that nested calls in a mutation have extra overhead compared to plain TypeScript functions, and that components require `ctx.runQuery` or `ctx.runMutation`. The size of the overhead is not stated anywhere, which is why Layer 1's choice to put every context read behind a component call is measured by the first experiment before Layer 3 is designed. If the measured cost breaks the read budgets, OQ1's answer changes the context component's shape. +The page states that nested calls in a mutation have extra overhead compared to plain TypeScript functions, and that components require `ctx.runQuery` or `ctx.runMutation`. The size of the overhead is not stated anywhere, which is why Layer 1's choice to put every context read behind a component call is measured by the first experiment before Layer 3 is designed. If the measured cost breaks the read budgets, OQ1's answer changes the context component's shape. Probe 3 gave a first reading on a native backend on 2026-10-01, release `precompiled-2026-09-28-5c7cb5b` with `convex` 1.46.0, on one machine: 50 reads of one document took about 3 ms through a helper, about 70 ms through nested queries and about 110 to 125 ms through component queries, from a parent mutation and from a parent query alike, which is about 2 ms more per component call than per helper call. A run earlier the same day read about half of that, so the size is an order of magnitude and not a number to budget with. It is a local backend's time, not a hosted deployment's, and function-call quota was not measured, because a local backend has none. ## Intent @@ -21,4 +21,4 @@ The page states that nested calls in a mutation have extra overhead compared to - statement: `ctx.runQuery` and `ctx.runMutation` inside a mutation cost more than a helper call (F4) - flavor: convex-fact - target: evidence.status:rechecked -- measurableBy: S10 https://docs.convex.dev/understanding/best-practices; doc status Documented, S10, rechecked 2026-09-29; size unknown; Probe 3 measures the size in latency and function-call quota (F4, D2, D7) +- measurableBy: S10 https://docs.convex.dev/understanding/best-practices; doc status Documented, S10, rechecked 2026-09-29; size unknown in the docs; Probe 3 measured latency on a local backend on 2026-10-01 and leaves function-call quota to a hosted deployment (F4, D2, D7) diff --git a/design/specs/facts/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.sdp.md b/design/specs/facts/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.sdp.md new file mode 100644 index 0000000..88258d8 --- /dev/null +++ b/design/specs/facts/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.sdp.md @@ -0,0 +1,32 @@ +--- +id: spec:facts.f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f05-react-client-retries-until-confirmed + verifies: spec:facts.f05-react-client-retries-until-confirmed +--- +# Probe 1: a backend restart with a mutation in flight + +Probe 1 · native tier · fixture composition. + +## Intent + +- outcome: The client retries until confirmed and the backend executes the call once. (Probe 1, F5) + +```gwt +Given a WebSocket client, the ConvexClient of convex/browser, that sends a mutation which inserts one marker row +When the backend process is killed and restarted on the same storage while the client stays open, in each of {trials: 10} trials +Then the mutation's promise resolves after the restart in {resolvedTrials: 10} trials +And every trial leaves exactly {markerRows: 1} marker row +And at least {leastKilledBeforeCommit: 1} trial was killed before its commit and at least {leastKilledAfterCommit: 1} after it +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The client is `ConvexClient` from `convex/browser`, which sends mutations through the same `BaseConvexClient` as the React client; the React client itself is not run. +- The kill is a SIGKILL, sent after a delay that the trials spread from none to a few tens of milliseconds. The test holds the backend's responses back from the client, reads the marker table with admin access after the restart and before the client reconnects, and so tells a kill before the commit from a kill after it. +- A trial killed after its commit is the one that tests the guarantee: the client never saw the result, sends the mutation again when it reconnects, and the backend must not run it twice. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.sdp.md b/design/specs/facts/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.sdp.md new file mode 100644 index 0000000..862a8a4 --- /dev/null +++ b/design/specs/facts/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.sdp.md @@ -0,0 +1,31 @@ +--- +id: spec:facts.f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f05-react-client-retries-until-confirmed + verifies: spec:facts.f05-react-client-retries-until-confirmed +--- +# Probe 1: a client closed with a pending mutation + +Probe 1 · native tier · fixture composition. + +## Intent + +- outcome: The backend executes the call once or not at all, never more. (Probe 1, F5) + +```gwt +Given a WebSocket client, the ConvexClient of convex/browser, that sends a mutation which inserts one marker row +When the client is closed before the mutation's result reaches it, in each of {trials: 20} trials +Then no trial leaves more than {mostMarkerRows: 1} marker row +And at least {leastCommittedUnconfirmed: 1} trial leaves its row although its client never received the result +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The client is `ConvexClient` from `convex/browser`, which sends mutations through the same `BaseConvexClient` as the React client; the React client itself is not run. +- The test holds the backend's responses back from the client, so every close happens with the mutation pending, and varies the time between the send and the close. +- The test records how many trials left no row and how many left one, which is the measure of what a closed tab can lose. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.sdp.md b/design/specs/facts/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.sdp.md new file mode 100644 index 0000000..412e6d9 --- /dev/null +++ b/design/specs/facts/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.sdp.md @@ -0,0 +1,30 @@ +--- +id: spec:facts.f05-react-client-retries-until-confirmed.probe-1-http-client-retry +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f05-react-client-retries-until-confirmed + verifies: spec:facts.f05-react-client-retries-until-confirmed +--- +# Probe 1: the HTTP client and a caller's retry + +Probe 1 · native tier · fixture composition. + +## Intent + +- outcome: The HTTP client sends each call once, and a caller's retry after a lost response is a second execution. (Probe 1, F5) + +```gwt +Given an HTTP client, ConvexHttpClient, whose transport delivers each request to the backend and then fails as a lost response would +When its caller sends a mutation which inserts one marker row, sees the call fail, and sends the same mutation again +Then the client sent {requestsPerCall: 1} request for each call +And the backend holds {markerRows: 2} marker rows +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The transport is the `fetch` the test passes to `ConvexHttpClient`: it performs the request, drops the response and throws, as a connection lost after the commit would. +- The test also asserts that one call on an unfaulted transport sends one request and leaves one row. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f05-react-client-retries-until-confirmed.sdp.md b/design/specs/facts/f05-react-client-retries-until-confirmed.sdp.md index 8960fd5..603f25f 100644 --- a/design/specs/facts/f05-react-client-retries-until-confirmed.sdp.md +++ b/design/specs/facts/f05-react-client-retries-until-confirmed.sdp.md @@ -10,7 +10,7 @@ relations: F5 · Status: rechecked · Doc status: Documented, S14, rechecked 2026-09-29 · Decisions: D6. -A lost response to a UI command is already handled by the client: it retries the mutation until the backend confirms it, and the backend executes each mutation call once. This is the fact that lets D6 drop v0.1's receipt on every public command. Where the guarantee ends, a tab closed with a pending mutation, a server restart, the HTTP client, is what Probe 1 must show. +A lost response to a UI command is already handled by the client: it retries the mutation until the backend confirms it, and the backend executes each mutation call once. This is the fact that lets D6 drop v0.1's receipt on every public command. Probe 1 showed where the guarantee ends, on a native backend on 2026-10-01, release `precompiled-2026-09-28-5c7cb5b` with `convex` 1.46.0, with the `ConvexClient` of `convex/browser`, which sends mutations through the same `BaseConvexClient` as the React client; the React client itself was not run. A client closed with a pending mutation left its row once or not at all, never twice, so a closed tab can lose a command and can also have committed one it never saw confirmed. Across a kill and restart of the backend on the same storage, every pending mutation resolved and left exactly one row, whether the kill came before the commit or after it. `ConvexHttpClient` sends each mutation in one request and does not retry, so a caller that retries after a lost response executes the mutation a second time. ## Intent @@ -21,4 +21,4 @@ A lost response to a UI command is already handled by the client: it retries the - statement: The React client retries mutations until confirmed, and the backend executes each call once (F5) - flavor: convex-fact - target: evidence.status:rechecked -- measurableBy: S14 https://docs.convex.dev/client/react; doc status Documented, S14, rechecked 2026-09-29; Probe 1 finds where the guarantee ends (F5, D6) +- measurableBy: S14 https://docs.convex.dev/client/react; doc status Documented, S14, rechecked 2026-09-29; Probe 1, run on a native backend on 2026-10-01, shows where the guarantee ends (F5, D6) diff --git a/design/specs/facts/f11-components-have-no-ctx-auth.sdp.md b/design/specs/facts/f11-components-have-no-ctx-auth.sdp.md index 578865a..c1cacb4 100644 --- a/design/specs/facts/f11-components-have-no-ctx-auth.sdp.md +++ b/design/specs/facts/f11-components-have-no-ctx-auth.sdp.md @@ -16,6 +16,10 @@ Code inside a component cannot read data that is not explicitly provided to it, - outcome: Record that components have no `ctx.auth`, with its evidence status as a target, so that every design resting on it is found by its `constrainedBy` edge (F11) +### Open questions + +- [non-blocking] Observed against the documentation on 2026-10-01: the components page says `ctx.auth` is not available within a component; on the pinned native backend, release `precompiled-2026-09-28-5c7cb5b` with `convex` 1.46.0, `ctx.auth.getUserIdentity()` inside a component query returned the caller's identity, and `convex-test` 0.0.60 returns null for the same call; the design does not depend on which holds, because D11 passes the actor explicitly and no component function reads `ctx.auth`; whether this fact's wording and status follow the documentation or the backend is the owner's ruling (F11, D11) + ## Constraints - statement: Components have no `ctx.auth` (F11) diff --git a/design/specs/facts/f13-transactions-have-limits.probe-4-component-call-shares-limits.sdp.md b/design/specs/facts/f13-transactions-have-limits.probe-4-component-call-shares-limits.sdp.md new file mode 100644 index 0000000..5091e95 --- /dev/null +++ b/design/specs/facts/f13-transactions-have-limits.probe-4-component-call-shares-limits.sdp.md @@ -0,0 +1,32 @@ +--- +id: spec:facts.f13-transactions-have-limits.probe-4-component-call-shares-limits +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f13-transactions-have-limits + verifies: spec:facts.f13-transactions-have-limits +--- +# Probe 4: a component call draws on the caller's transaction limits + +Probe 4 · native tier · fixture composition. + +## Intent + +- outcome: The limits are shared across the component boundary, not applied per call. (Probe 4, F13) + +```gwt +Given documents of {documentSize: "900 KiB"} each, {parentDocuments: 10} that a parent mutation reads and {childDocuments: 10} more that a component query reads +When a client calls the parent mutation, which reads its documents and then calls the component query +Then the call {together: "fails on the bytes-read limit"} +And the parent reading alone {parentAlone: "commits"} and the component query called alone {childAlone: "returns"} +And the same split of bytes written by a parent mutation and a component mutation {writesTogether: "fails on the bytes-written limit"} +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The test also reads `ctx.meta.getTransactionMetrics()` in the parent before and after a component read that stays under the limit and asserts that `bytesRead.used` and `documentsRead.used` grew by what the component read. +- The test also shows that each half of the write commits alone, so the failure is the sum. +- The local backend refuses writes past a rate, with `TooManyWrites` and the text "limited to 4 MiB bytes written per 1 second", so the test paces the transactions that seed and write its documents. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f13-transactions-have-limits.probe-4-nested-call-shares-limits.sdp.md b/design/specs/facts/f13-transactions-have-limits.probe-4-nested-call-shares-limits.sdp.md new file mode 100644 index 0000000..d1d6094 --- /dev/null +++ b/design/specs/facts/f13-transactions-have-limits.probe-4-nested-call-shares-limits.sdp.md @@ -0,0 +1,32 @@ +--- +id: spec:facts.f13-transactions-have-limits.probe-4-nested-call-shares-limits +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f13-transactions-have-limits + verifies: spec:facts.f13-transactions-have-limits +--- +# Probe 4: a nested call draws on the caller's transaction limits + +Probe 4 · native tier · fixture composition. + +## Intent + +- outcome: The limits are shared across the nested call, not applied per call. (Probe 4, F13) + +```gwt +Given documents of {documentSize: "900 KiB"} each, {parentDocuments: 10} that a parent mutation reads and {childDocuments: 10} more that a nested query reads +When a client calls the parent mutation, which reads its documents and then calls the nested query through ctx.runQuery +Then the call {together: "fails on the bytes-read limit"} +And the parent reading alone {parentAlone: "commits"} and the nested query called alone {childAlone: "returns"} +And the same split of bytes written by a parent mutation and a nested mutation {writesTogether: "fails on the bytes-written limit"} +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The test also reads `ctx.meta.getTransactionMetrics()` in the parent before and after a nested read that stays under the limit and asserts that `bytesRead.used` and `documentsRead.used` grew by what the child read. +- The test also shows that each half of the write commits alone, so the failure is the sum. +- The local backend refuses writes past a rate, with `TooManyWrites` and the text "limited to 4 MiB bytes written per 1 second", so the test paces the transactions that seed and write its documents. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f13-transactions-have-limits.probe-4-nesting-depth.sdp.md b/design/specs/facts/f13-transactions-have-limits.probe-4-nesting-depth.sdp.md new file mode 100644 index 0000000..f88305c --- /dev/null +++ b/design/specs/facts/f13-transactions-have-limits.probe-4-nesting-depth.sdp.md @@ -0,0 +1,29 @@ +--- +id: spec:facts.f13-transactions-have-limits.probe-4-nesting-depth +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f13-transactions-have-limits + verifies: spec:facts.f13-transactions-have-limits +--- +# Probe 4: how deep nested calls may go + +Probe 4 · native tier · fixture composition. + +## Intent + +- outcome: Nested calls have a depth bound, which this backend release sets at eight calls below the top-level function. (Probe 4, F13) + +```gwt +Given a mutation that calls itself through ctx.runMutation until its call stack holds a requested number of functions +When a client asks for each number from 1 upward +Then the deepest call stack that commits holds {deepestStack: 9} functions, the top-level mutation included +And the first stack that does not commit is refused with an error whose text holds {depthRefusal: "Cross component call depth limit exceeded"} +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The value is observed, not expected: the expectation written before the first run was 8 functions, and the run on release `precompiled-2026-09-28-5c7cb5b` showed 9. +- The bound is a limit the backend enforces, a default of this release and not a platform contract. Its error text names components, although the recursion crosses none. diff --git a/design/specs/facts/f13-transactions-have-limits.sdp.md b/design/specs/facts/f13-transactions-have-limits.sdp.md index 167cb37..a725fb2 100644 --- a/design/specs/facts/f13-transactions-have-limits.sdp.md +++ b/design/specs/facts/f13-transactions-have-limits.sdp.md @@ -10,7 +10,7 @@ relations: F13 · Status: documented · Doc status: Documented, S9 · Decisions: D10, D19. -The numbers below were read from the limits page on 2026-09-30 and match the plan's section 9. Every batch, sweeper, fan-out and backfill in the design states its own bound below them, because platform limits are ceilings, not batch sizes. The page states no limit on nested `runQuery` or `runMutation` calls per function and does not say whether the per-transaction limits add up across a component boundary, so Probe 4 stands. +The numbers below were read from the limits page on 2026-09-30 and match the plan's section 9. Every batch, sweeper, fan-out and backfill in the design states its own bound below them, because platform limits are ceilings, not batch sizes. The limits page states no limit on nested `runQuery` or `runMutation` calls per function. The page on writing data, read on 2026-10-01, states that "a mutation or query called by another mutation or query share the overall transaction limits" and documents `ctx.meta.getTransactionMetrics()` and the `transactionLimits` option; it does not name components. Probe 4 ran on a native backend on 2026-10-01, release `precompiled-2026-09-28-5c7cb5b` with `convex` 1.46.0: the 16 MiB read limit and the 16 MiB write limit are each one budget across a nested call and across a component boundary, and a call stack of nine functions commits where ten fails. ## Intent @@ -21,7 +21,7 @@ The numbers below were read from the limits page on 2026-09-30 and match the pla - statement: Transactions have limits (F13) - flavor: convex-fact - target: evidence.status:documented -- measurableBy: S9 https://docs.convex.dev/production/state/limits, read again 2026-09-30 and by this package the same day; doc status Documented, S9; Probe 4 checks whether the limits add up across nested calls and components (F13, D10, D19) +- measurableBy: S9 https://docs.convex.dev/production/state/limits, read again 2026-09-30 and by this package the same day; https://docs.convex.dev/database/writing-data, read 2026-10-01, for the sharing across nested calls; doc status Documented, S9; Probe 4, run on a native backend on 2026-10-01, shows the limits shared across nested calls and components (F13, D10, D19) ## Design @@ -35,5 +35,6 @@ The numbers are the ceilings the design treats as such; every batch states a bou - limitActionTimeout: 30 minutes in the Convex runtime and 10 minutes in the Node runtime (F13, S9) - limitScheduling: 1000 scheduled functions per mutation, 4 MiB per scheduled argument set, 16 MiB of scheduled arguments per mutation, 1,000,000 outstanding scheduled functions (F13, S9) - limitSchema: 32 indexes per table, 16 fields per index, 10,000 tables per deployment (F13, S9) -- limitNestedCalls: the page states no limit on nested `runQuery` or `runMutation` calls per function, and does not say whether the per-transaction limits add up across nested calls and components; Probe 4 stands (F13, S9, Probe 4) +- limitNestedCalls: the limits page states no count of nested `runQuery` or `runMutation` calls per function; the page on writing data states that nested calls share the overall transaction limits, and Probe 4 showed the same across a component boundary for bytes read and bytes written; the pinned backend commits a call stack of nine functions and fails the tenth with "Cross component call depth limit exceeded", which is a backend default and not a documented limit (F13, S9, Probe 4) +- transactionMetrics: `ctx.meta.getTransactionMetrics()` is asynchronous and returns `used` and `remaining` per metric, and a nested call accepts `transactionLimits`; Probe 4 read the metrics in a parent around a child's read, nested and in a component, and they grew by what the child read (F13, Probe 4) - limitsAreCeilings: platform limits are ceilings, not batch sizes; every bulk operation, sweeper and batch states a bound tested against the pinned Convex version (D19, F13) diff --git a/design/specs/facts/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.sdp.md b/design/specs/facts/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.sdp.md new file mode 100644 index 0000000..ab2cb69 --- /dev/null +++ b/design/specs/facts/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.sdp.md @@ -0,0 +1,33 @@ +--- +id: spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f14-convex-error-survives-nested-and-component-boundary + verifies: spec:facts.f14-convex-error-survives-nested-and-component-boundary +--- +# Probe 2: ConvexError data through a component boundary + +Probe 2 · native tier · fixture composition. + +## Intent + +- outcome: The parent and then the client read the same data. (Probe 2, F14) + +```gwt +Given a mutation inside a component that writes one row and then throws a ConvexError carrying structured data with code {code: "staleVersion"} +And two parent mutations that call it through the component API, one that writes a row of its own, catches the error and returns its data, and one that lets the error pass +When a client calls each parent +Then the catching parent returns the thrown data unchanged +And the client of the other parent reads the thrown data unchanged from the error it receives +And after the catching parent committed, the component mutation's table holds {childRows: 0} rows and the parent's own table holds {parentRows: 1} row +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The thrown data holds a string, a number, a boolean, a null, an array, a nested object and a 64-bit integer, and the test compares decoded values, not messages. +- The test shows that the thrower had written its row before it threw, so a thrower that never wrote does not pass. +- The test also throws an ordinary `Error` with an added property through the same path, checks that the error it sees is the thrower's, and records whether the property arrives. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.sdp.md b/design/specs/facts/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.sdp.md new file mode 100644 index 0000000..6de03f3 --- /dev/null +++ b/design/specs/facts/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.sdp.md @@ -0,0 +1,33 @@ +--- +id: spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f14-convex-error-survives-nested-and-component-boundary + verifies: spec:facts.f14-convex-error-survives-nested-and-component-boundary +--- +# Probe 2: ConvexError data through a nested mutation + +Probe 2 · native tier · fixture composition. + +## Intent + +- outcome: The parent and then the client read the same data. (Probe 2, F14) + +```gwt +Given a mutation that writes one row and then throws a ConvexError carrying structured data with code {code: "staleVersion"} +And two parent mutations that call it through ctx.runMutation, one that writes a row of its own, catches the error and returns its data, and one that lets the error pass +When a client calls each parent +Then the catching parent returns the thrown data unchanged +And the client of the other parent reads the thrown data unchanged from the error it receives +And after the catching parent committed, the throwing mutation's table holds {childRows: 0} rows and the parent's own table holds {parentRows: 1} row +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The thrown data holds a string, a number, a boolean, a null, an array, a nested object and a 64-bit integer, and the test compares decoded values, not messages. +- The test shows that the thrower had written its row before it threw, so a thrower that never wrote does not pass. +- The test also throws an ordinary `Error` with an added property through the same path, checks that the error it sees is the thrower's, and records whether the property arrives. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f14-convex-error-survives-nested-and-component-boundary.sdp.md b/design/specs/facts/f14-convex-error-survives-nested-and-component-boundary.sdp.md index d977254..2af23d2 100644 --- a/design/specs/facts/f14-convex-error-survives-nested-and-component-boundary.sdp.md +++ b/design/specs/facts/f14-convex-error-survives-nested-and-component-boundary.sdp.md @@ -2,28 +2,24 @@ id: spec:facts.f14-convex-error-survives-nested-and-component-boundary kind: constraint altitude: story -readiness: scoped +readiness: defined relations: refines: spec:facts.fact-ledger --- # ConvexError data survives a nested mutation and a component boundary -F14 · Status: assumed · Doc status: Assumed · Decisions: D7. +F14 · Status: probed · Doc status: Assumed · Decisions: D7. -The design throws a rejection as a structured `ConvexError` from inside a context component and expects the parent, and then the client, to read the same data. The application-errors page, read on 2026-09-30, says that in mutations throwing an error prevents the transaction from committing and that the exception bubbles through `runQuery`, `runMutation` and `runAction`; it does not say the data arrives intact across a component boundary. The fact stays assumed and Probe 2 stands. +The design throws a rejection as a structured `ConvexError` from inside a context component and expects the parent, and then the client, to read the same data. The application-errors page, read on 2026-09-30, says that in mutations throwing an error prevents the transaction from committing and that the exception bubbles through `runQuery`, `runMutation` and `runAction`; it does not say the data arrives intact across a component boundary. Probe 2 ran on a native backend on 2026-10-01, release `precompiled-2026-09-28-5c7cb5b` with `convex` 1.46.0: data holding a string, a number, a boolean, a null, an array, a nested object and a 64-bit integer reached the parent's catch and the client unchanged, through a nested mutation and through a component boundary, and the thrower's write was rolled back while the catching parent committed. An ordinary `Error` with an added property arrived without the property. The doc keeps its own status until the owner edits it. ## Intent - outcome: Record that `ConvexError` data survives a nested mutation and a component boundary, with its evidence status as a target, so that every design resting on it is found by its `constrainedBy` edge (F14) -- assumption: The fact is assumed, not documented; every Spec that rests on it names this assumption and the probe plan carries the probe (F14, Probe 2) - -### Open questions - -- [blocking] Probe 2 pending: until it runs on a native backend this fact stays assumed, and the decisions it serves (D7) rest on the author's reading of the pages (F14, Probe 2) +- assumption: The fact is probed on one pinned backend release and no page states it, so the probe is rerun when the pin moves (F14, Probe 2) ## Constraints - statement: `ConvexError` data survives a nested mutation and a component boundary (F14) - flavor: convex-fact -- target: evidence.status:assumed -- measurableBy: the application-errors page https://docs.convex.dev/functions/error-handling/application-errors states that the exception bubbles through runQuery, runMutation and runAction but not that the data survives a component boundary; doc status Assumed; Probe 2 (F14, D7) +- target: evidence.status:probed +- measurableBy: the application-errors page https://docs.convex.dev/functions/error-handling/application-errors states that the exception bubbles through runQuery, runMutation and runAction but not that the data survives a component boundary; doc status Assumed; Probe 2, run on a native backend on 2026-10-01 (F14, D7) diff --git a/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.sdp.md b/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.sdp.md new file mode 100644 index 0000000..58e0b76 --- /dev/null +++ b/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.sdp.md @@ -0,0 +1,32 @@ +--- +id: spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f15-parent-query-over-component-query-stays-reactive + verifies: spec:facts.f15-parent-query-over-component-query-stays-reactive +--- +# Probe 5: the client hook splits a page across the boundary and loses rows + +Probe 5 · native tier · fixture composition. + +## Intent + +- outcome: On this release the hook splits the page and does not show the rows between the capped page's continue cursor and its end cursor. (Probe 5, F15, S4) + +```gwt +Given a component list of {rows: 30} rows read through the parent by the usePaginatedQuery hook of convex-helpers/react, in pages of {pageSize: 10} with maximumRowsRead {maximumRowsRead: 20} +And the hook has loaded every page +When rows are inserted inside the first page's range until it holds {rowsInRange: 25} rows +Then the hook's results hold {rowsShown: 40} rows, in order and each once +And {rowsMissing: 5} rows of the list are not shown, the ones between the capped page's continue cursor and its end cursor +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition, with a DOM environment for the hook; every test owns its disposable backend. +- The test shows through a second subscription that the capped page came back `SplitRequired`, so a pass cannot come from a page that never needed the split. +- The hook is the `usePaginatedQuery` of `convex-helpers/react`, whose pages carry an end cursor. Convex's own `usePaginatedQuery` is not the subject. +- The values are observed, not expected: the expectation written before the first run was 45 rows shown and none missing. The run on release `precompiled-2026-09-28-5c7cb5b` with `convex-helpers` 0.1.124 showed 40. +- The cause is read in the helper's source and not probed further: a capped page returns its `continueCursor` at the last row it read, and the hook ends the second half of the split at that cursor and not at the page's end cursor. diff --git a/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.sdp.md b/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.sdp.md new file mode 100644 index 0000000..ab9d811 --- /dev/null +++ b/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.sdp.md @@ -0,0 +1,30 @@ +--- +id: spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f15-parent-query-over-component-query-stays-reactive + verifies: spec:facts.f15-parent-query-over-component-query-stays-reactive +--- +# Probe 5: pages of a component list stay contiguous under live writes + +Probe 5 · native tier · fixture composition. + +## Intent + +- outcome: The pages stay contiguous. (Probe 5, F15, S4) + +```gwt +Given a component list of {rows: 30} rows that a parent query relays from paginator, read in pages of {pageSize: 10} +And a client subscribed to every page, each bounded by the end cursor its first load returned +When {inserted: 5} rows are inserted inside the first page's range +Then the subscribed pages together hold {rowsHeld: 35} rows, every row of the list once and in order +And the first page now holds {firstPageRows: 15} rows +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The test also deletes a row inside the second page's range and asserts the same contiguity. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.sdp.md b/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.sdp.md new file mode 100644 index 0000000..11caa05 --- /dev/null +++ b/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.sdp.md @@ -0,0 +1,30 @@ +--- +id: spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f15-parent-query-over-component-query-stays-reactive + verifies: spec:facts.f15-parent-query-over-component-query-stays-reactive +--- +# Probe 5: a page that outgrows its read cap comes back through the parent as a split + +Probe 5 · native tier · fixture composition. + +## Intent + +- outcome: The parent relays the split, and the pages on either side of it leave no gap. (Probe 5, F15, S4) + +```gwt +Given a component list of {rows: 30} rows and a subscribed page of {pageSize: 10}, bounded by its end cursor and read through the parent with maximumRowsRead {maximumRowsRead: 20} +When rows are inserted inside the page's range until it holds {rowsInRange: 25} rows +Then the page the parent relays carries pageStatus {pageStatus: "SplitRequired"} and a split cursor +And the page from the start to the split cursor and the page from the split cursor to the first page's end cursor together hold {rowsHeld: 25} rows, every row of the range once and in order +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- This example reads the relayed pages directly. Whether the client hook acts on the split is the next example's question. +- The second half ends at the end cursor the page was subscribed with. The capped page's own `continueCursor` sits at the last row it read, and a second half that ends there holds 20 rows and leaves the rest of the range out. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.sdp.md b/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.sdp.md new file mode 100644 index 0000000..c245bfa --- /dev/null +++ b/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.sdp.md @@ -0,0 +1,28 @@ +--- +id: spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates +kind: example +altitude: story +readiness: defined +relations: + refines: spec:facts.f15-parent-query-over-component-query-stays-reactive + verifies: spec:facts.f15-parent-query-over-component-query-stays-reactive +--- +# Probe 5: a subscription to a parent query follows the component's data + +Probe 5 · native tier · fixture composition. + +## Intent + +- outcome: The subscription updates. (Probe 5, F15) + +```gwt +Given a client subscribed to a parent query that reads no table of its own and returns what a component query reads +When a mutation changes the document inside the component +Then the subscription delivers {updates: 1} changed value without the client asking again +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The client is a `ConvexClient` over a WebSocket, and the test fails if no update arrives within a stated wait. +- On the first run of this example, on 2026-10-01 on release `precompiled-2026-09-28-5c7cb5b`, the bound values held. diff --git a/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.sdp.md b/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.sdp.md index 888a89f..57293ae 100644 --- a/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.sdp.md +++ b/design/specs/facts/f15-parent-query-over-component-query-stays-reactive.sdp.md @@ -2,28 +2,24 @@ id: spec:facts.f15-parent-query-over-component-query-stays-reactive kind: constraint altitude: story -readiness: scoped +readiness: defined relations: refines: spec:facts.fact-ledger --- # A parent query over a component query stays reactive -F15 · Status: assumed · Doc status: Assumed · Decisions: D8. +F15 · Status: probed · Doc status: Assumed · Decisions: D8. -The read-need table's default for one entity's detail is an authorized component query returning a DTO, and a small cross-context view is a parent query over bounded component reads. Both assume that a subscription to the parent query re-runs when the component's tables change. Nothing read so far states or denies it, so the fact is assumed and every Spec that rests on it names Probe 5. One neighbouring fact is documented rather than assumed: the components page, S4, states that the built-in `.paginate()` does not work in a component because of how Convex tracks reactive pagination, recommends `paginator` from `convex-helpers` inside a component and the `usePaginatedQuery` hook from `convex-helpers` on the client; the corpus designs its component lists that way, and Probe 5's pagination half is therefore narrowed to whether the helper's page, which does not subscribe to its end cursor, stays contiguous across the boundary under live writes. +The read-need table's default for one entity's detail is an authorized component query returning a DTO, and a small cross-context view is a parent query over bounded component reads. Both rest on a subscription to the parent query re-running when the component's tables change. The doc lists this as assumed. Two things settled it on 2026-10-01: the page on using components states that "queries into components are reactive by default", and Probe 5 showed a subscription to a parent query that reads no table of its own receive a value changed inside a component, on a native backend, release `precompiled-2026-09-28-5c7cb5b` with `convex` 1.46.0. Pagination across the boundary is a neighbouring matter that Probe 5 also ran. The components page, S4, states that the built-in `.paginate()` does not work in a component, and recommends `paginator` from `convex-helpers` inside a component and the `usePaginatedQuery` hook from `convex-helpers` on the client; the corpus designs its component lists that way. On the same release with `convex-helpers` 0.1.124, pages pinned by their end cursors stayed contiguous under an insert and a delete, a page capped by `maximumRowsRead` came back through the parent as `SplitRequired`, and its two halves, read to the page's end cursor, held every row. The hook then showed 40 of 45 rows: it did not load the rows between the capped page's continue cursor and its end cursor. ## Intent - outcome: Record that a parent query calling a component query stays reactive, with its evidence status as a target, so that every design resting on it is found by its `constrainedBy` edge (F15) -- assumption: The fact is assumed, not documented; every Spec that rests on it names this assumption and the probe plan carries the probe (F15, Probe 5) - -### Open questions - -- [blocking] Probe 5 pending: until it runs on a native backend this fact stays assumed, and the decisions it serves (D8) rest on the author's reading of the pages (F15, Probe 5) +- assumption: The fact is documented and was probed on one pinned backend release; the pagination results are observations of one release of `convex-helpers` and are rerun when either pin moves (F15, Probe 5) ## Constraints - statement: A parent query calling a component query stays reactive (F15) - flavor: convex-fact -- target: evidence.status:assumed -- measurableBy: no page states it; S2 and S5 describe reactivity and component isolation separately; doc status Assumed; Probe 5, which also shows whether a component list paginated with the `paginator` helper, the documented replacement for the built-in `.paginate()` that S4 says does not work in a component, stays contiguous across the boundary (F15, S4, D8) +- target: evidence.status:probed +- measurableBy: https://docs.convex.dev/components/using, read 2026-10-01, states that queries into components are reactive by default; doc status Assumed; Probe 5, run on a native backend on 2026-10-01, which also shows how a component list paginated with the `paginator` helper, the documented replacement for the built-in `.paginate()` that S4 says does not work in a component, behaves across the boundary (F15, S4, D8) diff --git a/design/specs/facts/fact-ledger.sdp.md b/design/specs/facts/fact-ledger.sdp.md index 15b97c3..41384a7 100644 --- a/design/specs/facts/fact-ledger.sdp.md +++ b/design/specs/facts/fact-ledger.sdp.md @@ -12,13 +12,13 @@ Feature · Detail: full transcription · Traces: Fact ledger, Probes, Sources, F The ledger is what the design relies on: seventeen Convex facts, each with a status and the decisions that rest on it. The doc numbers neither the facts nor the probes' targets; the plan numbers the rows F1 to F17 in table order and the probes 1 to 7 in list order, and every citation in the corpus uses those tokens. Each fact is a constraint Spec refining this one, its target is its evidence status, and every Spec that relies on a fact points `constrainedBy` at it, so a graph query answers which designs rest on an assumption. That is the corpus's reading of the doc's fifth open question. -Thirteen facts are documented and four are assumed as the doc states them. Four of the documented ones were rechecked on 2026-09-29 during the review. On 2026-09-30 the lead read the limits, scheduling, backup, application-errors, components, best-practices, Workflow and migrations pages again; the only status that changed is F16, which the scheduling page documents, and the recheck is recorded on that fact while the doc keeps its own wording until the owner edits it. Nothing is probed yet. +Thirteen facts are documented and four are assumed as the doc states them. Four of the documented ones were rechecked on 2026-09-29 during the review. On 2026-09-30 the lead read the limits, scheduling, backup, application-errors, components, best-practices, Workflow and migrations pages again; the only status that changed is F16, which the scheduling page documents, and the recheck is recorded on that fact while the doc keeps its own wording until the owner edits it. Slice S0 ran probes 1 to 5 on a native backend on 2026-10-01: F14 and F15 are probed, and F4, F5 and F13 carry what their probes showed. The doc's own sentence, that nothing is probed yet, stands until the owner edits it. ## Intent - outcome: Every Convex fact the design relies on is recorded once with its status, its source and the decisions that rest on it, and every assumed fact names the probe that would settle it (Fact ledger) - value: A reviewer refutes a mechanism by its ledger fact, and a graph query lists the designs that rest on an assumption before the probes run (Decision method rule 1, Decision method rule 5) -- risk: Four facts are assumed; a probe that fails changes the decisions it serves, which the decision Specs record and the component Specs inherit (F14, F15, F16, F17) +- risk: One fact is still assumed, F17, and three that the doc lists as assumed are rechecked or probed in the corpus; a probe that fails changes the decisions it serves, which the decision Specs record and the component Specs inherit (F14, F15, F16, F17) - assumption: The sources support the Convex facts, not the correctness or performance of the design, which nothing has run yet (Sources) ## Rule @@ -28,7 +28,7 @@ Thirteen facts are documented and four are assumed as the doc states them. Four - Rechecked means read again on 2026-09-29 during the review (Fact ledger) - Probed means a native backend test showed it (Fact ledger) - Assumed means nothing beyond the author's reading supports it yet, and an assumed fact names the probe that would settle it (Fact ledger, Probes) -- Nothing is probed yet (Fact ledger) +- The doc says that nothing is probed yet; the corpus records each probe that has run on its fact and in `factsProbed` (Fact ledger) - The sources support the Convex facts, not the correctness or performance of this design, which nothing has run yet (Sources) - A fact's status changes only by a recheck against its named source or by a probe on a native backend, and the corpus records a recheck on the fact while the doc keeps its own status until the owner edits it (Fact ledger, F16) - Each probe is one small test on a native backend, and the probes run in the order the decisions need them (Probes) @@ -41,8 +41,8 @@ The four statuses map to the constraint target `evidence.status:documented`, `ev - factsDocumented: F1, F2, F7, F8, F9, F10, F11, F12, F13 (Fact ledger) - factsRechecked: F3, F4, F5, F6 on 2026-09-29; F16 on 2026-09-30 against S6, changed from assumed (Fact ledger, F16) -- factsAssumed: F14, F15, F17, with Probe 2, Probe 5 and Probe 6 respectively (Fact ledger, Probes) -- factsProbed: none (Fact ledger) +- factsAssumed: F17, with Probe 6; the doc also lists F14 and F15, which the corpus records as probed (Fact ledger, Probes) +- factsProbed: F14 by Probe 2 and F15 by Probe 5, on 2026-10-01 on backend release `precompiled-2026-09-28-5c7cb5b` with `convex` 1.46.0; Probe 1 on F5, Probe 3 on F4 and Probe 4 on F13 ran the same day, and their results are recorded on those facts, whose status stays documented or rechecked (Fact ledger, Probes) - sourcesRead: S1 to S13 by v0.1's author on 2026-09-29, S10, S14 and S15 again during the review the same day, and S2, S6, S8, S9, S10, S11 and the migrations and application-errors pages by the lead on 2026-09-30 (Sources) ## Verification — reviewed diff --git a/design/specs/facts/probe-plan.sdp.md b/design/specs/facts/probe-plan.sdp.md index be7ea27..b83e22d 100644 --- a/design/specs/facts/probe-plan.sdp.md +++ b/design/specs/facts/probe-plan.sdp.md @@ -18,9 +18,9 @@ relations: --- # The probe plan -Feature · Detail: full transcription · Traces: Probe 1 to Probe 7, F4, F5, F12, F13, F14, F15, F16, F17, S13. +Feature · Detail: full transcription · Traces: Probe 1 to Probe 7, F4, F5, F12, F13, F14, F15, F16, F17, S13, E-16. -Seven probes, in the order the decisions need them, each one small test on a native backend. A probe turns an assumed fact into a probed one, or measures a size the docs leave open. The recheck of 2026-09-30 narrowed two of them and left the rest standing: the limits page states no limit on nested calls, so Probe 4 stands; the application-errors page does not state that `ConvexError` data survives a component boundary, so Probe 2 stands; the scheduling page documents the states and the 7-day retention of `_scheduled_functions`, so Probe 7 need only confirm them and answer what a restore leaves; the backup page says nothing about component data or the scheduler, so that half of Probe 7 stands. +Seven probes, in the order the decisions need them, each one small test on a native backend. A probe turns an assumed fact into a probed one, or measures a size the docs leave open. The recheck of 2026-09-30 narrowed two of them and left the rest standing: the limits page states no limit on nested calls, so Probe 4 stands; the application-errors page does not state that `ConvexError` data survives a component boundary, so Probe 2 stands; the scheduling page documents the states and the 7-day retention of `_scheduled_functions`, so Probe 7 need only confirm them and answer what a restore leaves; the backup page says nothing about component data or the scheduler, so that half of Probe 7 stands. Slice S0 ran probes 1 to 5 on a native backend on 2026-10-01, and each result is recorded on its fact; probes 6 and 7 have not run. The probes are not the first experiment. The experiment builds Layers 0 to 2 and measures cost; the probes settle single facts and can run before it, in a fixture app that is separate from the example app. @@ -35,13 +35,12 @@ The probes are not the first experiment. The experiment builds Layers 0 to 2 and ### Open questions -- [non-blocking] Probe 1 pending: it must show where the client's exactly-once guarantee ends, for a tab closed with a pending mutation, a server restart and the HTTP client (Probe 1, D6) -- [non-blocking] Probe 2 pending: it must show that `ConvexError` data arrives intact through a nested mutation and through a component boundary (Probe 2, F14, D7) -- [non-blocking] Probe 3 pending: it must give the cost of one component call in latency and function-call quota (Probe 3, F4, D2, D8) -- [non-blocking] Probe 4 pending: it must show whether transaction limits add up across nested calls and components (Probe 4, F13, D10) -- [non-blocking] Probe 5 pending: it must show that a parent query over component queries stays reactive, and whether a component list paginated with `paginator` from `convex-helpers`, the documented replacement for the built-in `.paginate()` that S4 says does not work in a component, stays contiguous across the boundary when the client passes the end cursor and splits correctly when the component's `maximumRowsRead` or `maximumBytesRead` caps a re-run; that the cap exists is read in the helper's source and is not the probe's question (Probe 5, F15, S4, D8) +- [non-blocking] Probe 1 ran on 2026-10-01: a closed client leaves its mutation executed once or not at all, a backend restart leaves it executed once, and the HTTP client does not retry; whether callers of `ConvexHttpClient` must supply a request key is D6's question for the owner (Probe 1, D6) +- [non-blocking] Probe 3 ran on 2026-10-01 for latency on a local backend; a local backend has no function-call quota, so that half waits for a hosted deployment (Probe 3, F4, D2, D8) +- [non-blocking] Probe 5 ran on 2026-10-01: a parent query over a component query stays reactive, pages pinned by their end cursors stay contiguous, and a capped page crosses the boundary as `SplitRequired`; the `convex-helpers/react` hook then lost rows, and slice S2 decides how a context list is capped before it builds one (Probe 5, F15, S4, D8) - [non-blocking] Probe 6 pending: it must show a backfill batch racing a live command under optimistic concurrency, and that the migrations component fits a generation backfill (Probe 6, F17, D9) - [non-blocking] Probe 7 pending: before Layer 3, it must show what a restore leaves of Workpool and Workflow state and confirm the states and retention of `_scheduled_functions` on a native backend (Probe 7, F12, F16, D13, D19) +- [non-blocking] Extension E-16: the doc says each probe is one small test on a native backend and that a probed fact is one a native backend test showed, and does not say how a test that settles an unknown states what it expects or what follows when the backend answers otherwise; the option taken here: a probe is written as example Specs that verify its fact, each bound to its native test; an example's bound values are the expectation written before the first run; a first run that shows another value is a finding recorded on the fact, and the example is then bound to what that release does and says so; an example fails when its run did not reach the boundary it is about; a later run on a new release that shows another value is again a finding, and the test is not repaired to keep the old value; a size is recorded and not asserted, while a limit the backend enforces is bound as observed; and what a local backend cannot answer is named on the example and stays open on the fact; the owner confirms (E-16, Probes, Acceptance scenarios) ## Workflow @@ -49,6 +48,13 @@ The probes are not the first experiment. The experiment builds Layers 0 to 2 and - rule: Probes run in the order the decisions need them (Probes) - rule: A probe result changes a fact's status to probed; it never changes a decision by itself, and the decision Spec records what changed (Probes, Decision method rule 1) - rule: A probe run records the commit, backend and dependency versions, configuration, command and result, like any acceptance run (Acceptance scenarios) +- rule: [extension] A probe is written as example Specs that verify its fact, one example per case, each bound to its native test (E-16) +- rule: [extension] An example's bound values are the expectation written before the first run (E-16) +- rule: [extension] A first run that shows another value is a finding recorded on the fact with the backend release that showed it; the example is then bound to what that release does, and its text says the value was observed (E-16) +- rule: [extension] A later run, on a new backend release or a new version of a pinned package, that shows another value is again a finding recorded on the fact, and the example is rebound; the test is not repaired to keep the old value (E-16) +- rule: [extension] An example fails when its run did not reach the boundary it is about (E-16) +- rule: [extension] A measured size, a time or a cost, is recorded in the run's evidence and on the fact and is not asserted; a limit the backend enforces is bound as observed (E-16) +- rule: [extension] An example names what a local backend cannot answer, and that part stays open on the fact (E-16) - Probe 1 sends a mutation from a tab that closes while it is pending, across a server restart, and from the HTTP client, and records for each whether the backend executed it once, more than once or not at all; it serves D6 (Probe 1, F5, F6) - Probe 2 throws a `ConvexError` carrying structured data from a nested mutation and from a mutation inside a component, and checks that the parent and then the client read the same data; it serves D7 (Probe 2, F14) - Probe 3 measures one component call from a parent mutation and from a parent query in latency and in function-call quota, and reports the difference from a plain helper; it serves D2 and D8 (Probe 3, F4) @@ -59,7 +65,7 @@ The probes are not the first experiment. The experiment builds Layers 0 to 2 and ## Design -Each probe is a fixture-app test on a disposable backend. The probe app is separate from the example app and ships no test-only function to production. Where the probes live is an open question of the doc that the corpus does not decide. +Each probe is a fixture-app test on a disposable backend. The probe app is separate from the example app and ships no test-only function to production. - backend: one disposable native backend per probe run, from the same local backend S13 describes (Acceptance scenarios, S13) - fixtureApp: the probe app is the kernel's fixture app, separate from the example app, and test-only functions never ship (Acceptance scenarios) @@ -69,4 +75,4 @@ Each probe is a fixture-app test on a disposable backend. The probe app is separ ## Verification — reviewed -- A reviewer confirms that every assumed fact (F14, F15, F17) and the rechecked F16 name a probe in this plan, and that every decision Spec with a Probe line records it as an open question naming the same probe number. +- A reviewer confirms that every fact the doc lists as assumed (F14, F15, F17) and the rechecked F16 name a probe in this plan, that each probe that has run has example Specs bound to native tests and its result on its fact, and that every decision Spec with a Probe line for a probe that has not run records it as an open question naming the same probe number. diff --git a/design/specs/foundation.pack.sdp.md b/design/specs/foundation.pack.sdp.md index 597d4c3..096d5e0 100644 --- a/design/specs/foundation.pack.sdp.md +++ b/design/specs/foundation.pack.sdp.md @@ -6,6 +6,11 @@ specs: - spec:platform.decision-method - spec:platform.vocabulary - spec:platform.acceptance-contract + - spec:platform.native-harness + - spec:platform.native-harness.fixture-issuer-token-yields-identity + - spec:platform.native-harness.ordinary-client-refused-internal-function + - spec:platform.native-harness.ordinary-client-refused-component-function + - spec:platform.native-harness.admin-key-acting-as-identity-reaches-internal-function - spec:platform.existing-systems - spec:laws.law01-sanctioned-writes-only - spec:laws.law02-state-and-events-commit-together @@ -44,7 +49,12 @@ specs: - spec:facts.f02-component-calls-commit-with-caller - spec:facts.f03-nested-run-mutation-partial-rollback - spec:facts.f04-nested-calls-cost-more-than-helpers + - spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation + - spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query - spec:facts.f05-react-client-retries-until-confirmed + - spec:facts.f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation + - spec:facts.f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation + - spec:facts.f05-react-client-retries-until-confirmed.probe-1-http-client-retry - spec:facts.f06-client-mutations-run-in-order - spec:facts.f07-queries-reactive-not-durable-delivery - spec:facts.f08-scheduling-commits-with-mutation @@ -53,8 +63,17 @@ specs: - spec:facts.f11-components-have-no-ctx-auth - spec:facts.f12-backups-exclude-pending-scheduled-functions - spec:facts.f13-transactions-have-limits + - spec:facts.f13-transactions-have-limits.probe-4-nested-call-shares-limits + - spec:facts.f13-transactions-have-limits.probe-4-component-call-shares-limits + - spec:facts.f13-transactions-have-limits.probe-4-nesting-depth - spec:facts.f14-convex-error-survives-nested-and-component-boundary + - spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation + - spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary - spec:facts.f15-parent-query-over-component-query-stays-reactive + - spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates + - spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous + - spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed + - spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary - spec:facts.f16-scheduled-functions-table-shows-failed-runs - spec:facts.f17-migrations-fits-generation-backfill modelRefs: @@ -62,6 +81,6 @@ modelRefs: --- # Foundation -Package A. The decisions document carried into the graph: the platform epic, the twelve laws, the nineteen decisions, the fact ledger and its probe plan, the decision method, layers and profiles, the acceptance contract, existing-systems rules and the vocabulary. Every other package cites these IDs; none of them redefines them. +Package A. The decisions document carried into the graph: the platform epic, the twelve laws, the nineteen decisions, the fact ledger and its probe plan, the decision method, layers and profiles, the acceptance contract and the native harness, existing-systems rules and the vocabulary. Every other package cites these IDs; none of them redefines them. -Membership order follows the plan's inventory: the platform Specs, then the laws in number order, then the decisions in number order, then the fact ledger, the probe plan and the facts in number order. Fifty-six Specs. +Membership order follows the plan's inventory: the platform Specs, then the laws in number order, then the decisions in number order, then the fact ledger, the probe plan and the facts in number order. Each parent's examples follow it: the native harness and its four examples after the acceptance contract, and each probe's examples after the fact they verify. Seventy-five Specs. diff --git a/design/specs/kernel/outcome-model.sdp.md b/design/specs/kernel/outcome-model.sdp.md index c3588bd..bbf9590 100644 --- a/design/specs/kernel/outcome-model.sdp.md +++ b/design/specs/kernel/outcome-model.sdp.md @@ -28,12 +28,11 @@ The kernel defines the shapes and does not define wire codes. The closed error-c - outcome: One `Outcome` union, defined here and nowhere else, gives every command exactly one of applied, business failure, rejection or technical failure, each with its commit behavior fixed (D4) - value: Whether a refused request becomes a fact is visible in the command's name, never hidden in error handling; the boundary maps one union and every caller learns one shape (D4) - risk: The standing cost is one closed union and one reserved-code convention that every context, the adapter and the boundary must honor (D4) -- assumption: A rejection thrown inside a context component arrives at the parent with its `data` intact; this is F14, assumed, and Probe 2 settles it (F14, Probe 2) +- assumption: A rejection thrown inside a context component arrives at the parent with its `data` intact; this is F14, which Probe 2 showed on the pinned backend (F14, Probe 2) ### Open questions - [non-blocking] Extension E-21: the doc names the four outcomes and their commit behavior but not their TypeScript shape; this Spec fixes `Outcome`, `Rejection`, `CommittedOutcome`, `AffectedRef`, the rule that a committed outcome carries at least one event, and the three rejection codes the adapter reserves, `staleVersion`, `operationTooLarge` and `entityExists`, the last being the adapter's answer to a create planned at expected version 0 whose stream already exists, which is the uniqueness check D6 names for a client-generated entity ID; the owner confirms the shapes or renames the reserved codes before code names them (D4, D6, E-21) -- [non-blocking] Probe 2 pending: until it runs, the claim that a `ConvexError` thrown by a context operation reaches the parent and the client with the same `Rejection` data rests on the author's reading of the pages (F14, Probe 2) ## Rule @@ -68,7 +67,7 @@ The union below is the kernel's. Applied and business failure are the value a co - validatorRejection: `const rejectionValidator = v.object({ code: v.string(), message: v.string(), details: v.optional(v.record(v.string(), v.any())) })` (D7, E-21) - validatorCommittedOutcome: `const committedOutcomeValidator = v.object({ kind: v.union(v.literal("applied"), v.literal("businessFailure")), result: v.any(), versions: v.array(streamVersionValidator) })` (D4) - throwShape: `throw new ConvexError(rejection)` with the `Rejection` value as the error's `data`, which must be a Convex value and is (D7, F14) -- assumptionAcrossBoundary: the `data` of a thrown `ConvexError` arriving intact through a component boundary is F14, assumed; the application-errors page states only that the exception bubbles through `runQuery`, `runMutation` and `runAction`, so Probe 2 stands (F14, Probe 2) +- assumptionAcrossBoundary: the `data` of a thrown `ConvexError` arriving intact through a component boundary is F14; the application-errors page states only that the exception bubbles through `runQuery`, `runMutation` and `runAction`, and Probe 2 showed the data intact on the pinned backend (F14, Probe 2) ## Verification — reviewed diff --git a/design/specs/obligations/local-reaction-wrapper.sdp.md b/design/specs/obligations/local-reaction-wrapper.sdp.md index 3105b2e..529aace 100644 --- a/design/specs/obligations/local-reaction-wrapper.sdp.md +++ b/design/specs/obligations/local-reaction-wrapper.sdp.md @@ -27,7 +27,7 @@ The wrapper is the scheduled mutation that carries out one attempt of a local re ## Intent - outcome: Pin the wrapper as a step sequence with the outcome table: success commits effect and completion together; a known rejection settles; a retryable failure rolls the body back and records the next attempt; a maintenance refusal reschedules the same attempt without counting it; a wrapper failure leaves nothing and the sweeper rearms (D13, F3, F9) -- assumption: A `ConvexError` thrown by the body inside its nested mutation reaches the wrapper's catch with its `data` intact, which `errorClassification` rests on; this is F14, assumed, and Probe 2 settles it (F14, Probe 2) +- assumption: A `ConvexError` thrown by the body inside its nested mutation reaches the wrapper's catch with its `data` intact, which `errorClassification` rests on; this is F14, which Probe 2 showed on the pinned backend (F14, Probe 2) ### Open questions diff --git a/design/specs/platform/acceptance-contract.sdp.md b/design/specs/platform/acceptance-contract.sdp.md index cd69387..a45a136 100644 --- a/design/specs/platform/acceptance-contract.sdp.md +++ b/design/specs/platform/acceptance-contract.sdp.md @@ -8,20 +8,19 @@ relations: --- # The acceptance contract -Feature · Detail: full transcription · Traces: Acceptance scenarios (table, tiers, evidence), First experiment, S13, Sc L2-4, E-13, E-14. +Feature · Detail: full transcription · Traces: Acceptance scenarios (table, tiers, evidence), First experiment, S13, Sc L2-4, E-14. -The doc's acceptance table is the contract. It lists forty-three scenario rows by layer with a pass condition each, and states how each row is tested (the tier), what a test run must record (the evidence) and how readiness is stated. This Spec carries those rules. The rows themselves become example Specs under the component that first provides the capability, one example per row and one sibling example per enumerated case, so that each pass condition is a bound point in that component's example space; the plan's section 4 gives the assignment and the example IDs. +The doc's acceptance table is the contract. It lists forty-three scenario rows by layer with a pass condition each, and states how each row is tested (the tier), what a test run must record (the evidence) and how readiness is stated. This Spec carries those rules. The rows themselves become example Specs under the component that first provides the capability, one example per row and one sibling example per enumerated case, so that each pass condition is a bound point in that component's example space; the plan's section 4 gives the assignment and the example IDs. How a native test obtains an identity, what the fixture app contains and what a test controls are the rules of `spec:platform.native-harness`, which refines this Spec. ## Intent - outcome: Every acceptance scenario is tested as behavior on the tier that can prove it, with recorded evidence, and readiness is stated in plain terms that a run can back (Acceptance scenarios) - value: The scenarios are the spine of the decision method: a mechanism is justified only by a scenario the do-nothing option fails, so the contract is also the refutation surface for every mechanism (Decision method rule 3) - risk: A native tier costs a disposable backend per test and a production-configuration run; skipping it leaves component, nested-mutation, scheduling and contention behavior proven only in a simulator, which is not native proof (Acceptance scenarios) -- assumption: Native tests run against a local Convex backend as S13 describes, and the testing guide S13 links obtains a test's identity through the backend's admin key acting as that identity (S13) +- assumption: Native tests run against a local Convex backend as S13 describes (S13) ### Open questions -- [non-blocking] Extension E-13: the doc says native acceptance runs with production authority and a disposable backend per test and does not say how a test obtains a signed identity, which a production issuer cannot mint for a throwaway backend; the option taken here is the local backend's admin key acting as an identity whose issuer and subject are the ones production's `auth.config.ts` names, the mechanism the guide S13 links uses, with the actor mapping, grants, namespaces and entry points unchanged and the identity source named on every evidence record as the one difference from production; the owner confirms, or supplies a test issuer with keys the harness holds, which would remove the difference at the cost of a JWKS endpoint per run (E-13, S13, Sc L2-9) - [non-blocking] Extension E-14: the doc's four tiers, domain, simulator, native and end to end, name no compile-time check, while Sc L2-4's pass condition is a build, type or registration check that fails before any traffic; the option taken here is a fifth tier, the build tier, a compile of the fixture app with the change applied that asserts a non-zero `tsc` exit or a failed registration check, run as a step of the fixture app's build before the native suite and recorded like any run; the alternative, folding it into the native tier as a compile step that precedes the native run, was not taken because a native run needs a backend and the check needs none; the owner confirms (E-14, Sc L2-4, Acceptance scenarios) ## Rule @@ -43,7 +42,6 @@ The doc's acceptance table is the contract. It lists forty-three scenario rows b - Readiness is stated plainly as specified, implemented, tested under named conditions, or operationally accepted (Acceptance scenarios) - The first experiment passes when every Layer 0, 1 and 2 scenario passes on a native backend (First experiment) - The one all-layer scenario, breaking metrics and logging and separately breaking mandatory audit, applies to every installed layer (Sc ALL-1) -- [extension] Parity with a release for Sc L2-9 means the same code path, schema, concurrency and authority model, the grants, namespaces, entry points and actor mapping, while the identity's signature source is the one named difference; a native run authenticates through the local backend's admin key acting as an identity with the production issuer's name, and its evidence record says so (E-13, S13, Sc L2-9) - [extension] A build-tier scenario passes when a compile of the fixture app with the change applied fails at `tsc` or at the registration check before any deployment; the build tier is the fifth tier, named by Sc L2-4 alone, and needs no backend (E-14, Sc L2-4) ## Design @@ -52,14 +50,13 @@ The corpus maps the doc's readiness words onto SDP's rungs without collapsing th - tierDomain: pure tests of `decide`, `evolve` and the fold with no I/O; Sc L0-1 and L0-2 (Acceptance scenarios) - tierSimulator: outcome combinations, serialization and classification of the four outcomes and the error codes (Acceptance scenarios, D4) -- tierNative: a disposable local backend per test, production schemas and authority, proving component and nested-mutation behavior, scheduling, contention and deployment (Acceptance scenarios, S13) -- tierNativeIdentity: the harness holds the local backend's admin key and acts as the identity under test, setting `issuer` to the name production's `auth.config.ts` configures and `subject` to the test principal, so `ctx.auth.getUserIdentity()` in the public entry returns the same shape a production token yields and `establishActor`, the grants read and the namespace rules run unchanged; no production issuer is asked to sign for a disposable backend (E-13, S13, D11) +- tierNative: a disposable local backend per test, proving component and nested-mutation behavior, scheduling, contention and deployment (Acceptance scenarios, S13) - tierEndToEnd: the production composition on a representative dataset; Sc L2-8, L2-9, L3-7, L4-3 (Acceptance scenarios) - tierBuild: [extension] a compile of the kernel's fixture app with the change under test applied, asserting a non-zero `tsc` exit naming the caller or a failed registration check, run as a build step before the native suite with no backend; Sc L2-4 (E-14, Sc L2-4) -- evidenceRecord: commit, installed layers, backend and dependency versions, configuration, dataset, command and result, plus the difference from production when configuration was adjusted, and the identity source, issuer-signed or admin acting-as, which on the native tier is the one difference a production-configuration run names (Acceptance scenarios, E-13) +- evidenceRecord: commit, installed layers, backend and dependency versions, configuration, dataset, command and result, plus the difference from production when configuration was adjusted (Acceptance scenarios) - readinessWords: specified, implemented, tested under named conditions, operationally accepted; never a percentage or a score (Acceptance scenarios) ## Verification — reviewed -- A reviewer confirms that all forty-three scenario rows have example Specs, that enumerated cases are split into sibling examples, and that each example names its tier and states the doc's pass condition as its outcome. +- A reviewer confirms that all forty-three scenario rows have example Specs, that each case a row enumerates and each alternative a scenario's text joins with "or" has its own sibling example, that a verification bullet adds assertions to a case and never stands in for one, and that each example names its tier and states the doc's pass condition as its outcome. - A reviewer confirms that no example claims a passing run; the graph records a verifier's existence, never its result. diff --git a/design/specs/platform/native-harness.admin-key-acting-as-identity-reaches-internal-function.sdp.md b/design/specs/platform/native-harness.admin-key-acting-as-identity-reaches-internal-function.sdp.md new file mode 100644 index 0000000..6febcab --- /dev/null +++ b/design/specs/platform/native-harness.admin-key-acting-as-identity-reaches-internal-function.sdp.md @@ -0,0 +1,30 @@ +--- +id: spec:platform.native-harness.admin-key-acting-as-identity-reaches-internal-function +kind: example +altitude: story +readiness: defined +relations: + refines: spec:platform.native-harness + verifies: spec:platform.native-harness +--- +# The admin key acting as an identity reaches an internal function + +E-13 · native tier · fixture composition. + +## Intent + +- outcome: The call runs, which is why no caller under test carries the admin key. (E-13) + +```gwt +Given a disposable backend running the fixture composition, which registers an internal mutation that writes one row +And a client holding the admin key and acting as the identity with subject {subject: "user-1"} +When the client calls the internal mutation by name +Then the call returns without an error +And the table the internal mutation writes holds {rows: 1} row +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The test asserts that a public query called by the same client returns the identity it acts as, so the client is indistinguishable from an ordinary caller inside a handler. +- This is the one test in which a caller holds the admin key. diff --git a/design/specs/platform/native-harness.fixture-issuer-token-yields-identity.sdp.md b/design/specs/platform/native-harness.fixture-issuer-token-yields-identity.sdp.md new file mode 100644 index 0000000..7b418e2 --- /dev/null +++ b/design/specs/platform/native-harness.fixture-issuer-token-yields-identity.sdp.md @@ -0,0 +1,29 @@ +--- +id: spec:platform.native-harness.fixture-issuer-token-yields-identity +kind: example +altitude: story +readiness: defined +relations: + refines: spec:platform.native-harness + verifies: spec:platform.native-harness +--- +# A token signed by the fixture issuer yields the caller's identity + +E-13 · native tier · fixture composition. + +## Intent + +- outcome: An ordinary client carrying a token the harness signed is the identity the token names, and a client with no token is nobody. (E-13) + +```gwt +Given a disposable backend whose environment variables name the fixture issuer {issuer: "https://fixture-issuer.test"} and its data-URI key set +And an ordinary client carrying a token the harness signed for subject {subject: "user-1"} +When the client calls a public query that returns the caller's identity +Then the identity names issuer {identityIssuer: "https://fixture-issuer.test"} and subject {identitySubject: "user-1"} +And the same query from a client with no token returns no identity +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The test asserts that `tokenIdentifier` is the issuer and the subject joined by a vertical bar, and that a token signed by a key outside the key set is refused. diff --git a/design/specs/platform/native-harness.ordinary-client-refused-component-function.sdp.md b/design/specs/platform/native-harness.ordinary-client-refused-component-function.sdp.md new file mode 100644 index 0000000..775f2f8 --- /dev/null +++ b/design/specs/platform/native-harness.ordinary-client-refused-component-function.sdp.md @@ -0,0 +1,37 @@ +--- +id: spec:platform.native-harness.ordinary-client-refused-component-function +kind: example +altitude: story +readiness: defined +relations: + refines: spec:platform.native-harness + verifies: spec:platform.native-harness +--- +# An ordinary client cannot call a component's function + +E-13, E-15 · native tier · fixture composition. + +## Intent + +- outcome: Convex refuses the call on the two HTTP routes with its own error and closes the connection on the WebSocket route without an answer; nothing is stored, and the same function runs for the harness's admin access. (E-13, E-15, D2) + +```gwt +Given a disposable backend running the fixture composition, which mounts a component with a mutation that writes one row +And an ordinary client carrying a token the harness signed for subject {subject: "user-1"} +When the client calls the component's mutation by each of the three routes a client has +Then the call to the public endpoint with a component-qualified name fails with an error whose text holds {publicRouteRefusal: "Could not find public function"} +And the call to the endpoint that takes a component path fails with an error whose text holds {componentRouteRefusal: "BadDeployKey"} +And the call over the WebSocket protocol with a component path is sent, gets no answer within {socketWaitMs: 3000} ms, and the backend closes the connection at least {leastSocketCloses: 1} time +And the table the component's mutation writes holds {rowsAfterRefusal: 0} rows +And the same mutation called with the harness's admin access writes {rowsAfterAdminCall: 1} row +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- Each HTTP refusal is identified by the text of Convex's own error, so a call that fails for another reason does not pass. +- On the public route the test checks that the refused call named the same function the admin call runs, so a call to another name does not pass. +- `BadDeployKey` is the gate of the endpoint that takes a component path: it answers every caller without the admin key, whatever the path, so on that route the error says nothing about the function, and the admin call is what shows that the function exists. +- The WebSocket route has no error to show: on the pinned release the backend closes the connection, and the client reconnects and sends again without end. The test shows that the mutation left the client on the socket, that no answer came within the wait and that the backend closed the connection, so a call that was never sent does not pass. A call that ran would leave a row, and the row counts before and after the admin call would show it. +- The test repeats the two HTTP calls from a client with no token and asserts the same errors, and reads the component's table with the harness's admin access. +- The admin call comes last and on the same deployment, so the refused function is shown to exist and to write its row. diff --git a/design/specs/platform/native-harness.ordinary-client-refused-internal-function.sdp.md b/design/specs/platform/native-harness.ordinary-client-refused-internal-function.sdp.md new file mode 100644 index 0000000..9a504b3 --- /dev/null +++ b/design/specs/platform/native-harness.ordinary-client-refused-internal-function.sdp.md @@ -0,0 +1,32 @@ +--- +id: spec:platform.native-harness.ordinary-client-refused-internal-function +kind: example +altitude: story +readiness: defined +relations: + refines: spec:platform.native-harness + verifies: spec:platform.native-harness +--- +# An ordinary client cannot call an internal function + +E-13, E-15 · native tier · fixture composition. + +## Intent + +- outcome: Convex refuses the call with its own error, nothing is stored, and the same function runs for the harness's admin access. (E-13, E-15) + +```gwt +Given a disposable backend running the fixture composition, which registers an internal mutation that writes one row +And an ordinary client carrying a token the harness signed for subject {subject: "user-1"} +When the client calls the internal mutation by name +Then the call fails with an error whose text holds {refusal: "Could not find public function"} +And the table the internal mutation writes holds {rowsAfterRefusal: 0} rows +And the same mutation called with the harness's admin access writes {rowsAfterAdminCall: 1} row +``` + +## Verification — executable + +- Runs in the native tier on the fixture composition; every test owns its disposable backend. +- The refusal is identified by the text of Convex's own error, so a call that fails for another reason does not pass. +- The test repeats the refused call from a client with no token and asserts the same error, and reads the table with the harness's admin access. +- The admin call comes last and on the same deployment, so the refused function is shown to exist and to write its row. diff --git a/design/specs/platform/native-harness.sdp.md b/design/specs/platform/native-harness.sdp.md new file mode 100644 index 0000000..2ca822f --- /dev/null +++ b/design/specs/platform/native-harness.sdp.md @@ -0,0 +1,94 @@ +--- +id: spec:platform.native-harness +kind: rule +altitude: feature +readiness: defined +relations: + refines: spec:platform.acceptance-contract +--- +# The native harness + +Feature · Detail: extension · Traces: Acceptance scenarios (tiers, evidence), S13, Sc L2-9, D11, E-13, E-15. + +The doc says that native tests prove component and nested-mutation behavior, scheduling, contention and deployment, that the kernel's fixture app is separate from the example app, that test-only functions never ship and that every test owns its disposable backend. It does not say how a test obtains an identity, what the fixture app contains, how a fault enters a run, what a test may read or what it controls. This Spec carries those rules as two extensions, E-13 and E-15. Every rule here is the corpus's provisional reading until the owner rules. The doc's own rules on tiers and evidence stay in `spec:platform.acceptance-contract`. + +## Intent + +- outcome: A native test runs on a backend it owns, as an ordinary caller, on a named composition, and asserts only what a client or the harness's admin access can read (Acceptance scenarios) +- value: Every later slice builds its native tests on this harness, so a harness that is more privileged or more lenient than production lowers the worth of every native pass (Sc L2-9) +- risk: One backend per test costs a process start, a deploy and a teardown per test, and a harness that leaks a process or a port fails tests that did nothing wrong (Acceptance scenarios) +- assumption: Native tests run against a local Convex backend as S13 describes; the testing guide S13 links obtains a test's identity through the backend's admin key acting as that identity, which this harness does not use for a caller under test (S13, E-13) + +### Open questions + +- [non-blocking] Extension E-13: the doc says native acceptance runs with production authority and a disposable backend per test and does not say how a test obtains a signed identity, which a production issuer cannot mint for a throwaway backend; the option taken here is a fixture issuer: a composition that runs natively reads its issuer, application ID and key set from the deployment's environment variables, the harness sets the key set to a data URI whose private key it holds, and each caller under test is an ordinary client carrying a token the harness signs; the option first taken, the backend's admin key acting as an identity, was dropped on 2026-10-01 because such a caller passes Convex's visibility check for internal functions, which `spec:platform.native-harness.admin-key-acting-as-identity-reaches-internal-function` shows; this reading reaches into production, because running the production composition natively with no changed file means the example application's `auth.config.ts` is a `customJwt` provider whose trust root is whatever the deployment's environment names, and a provider that fetches its keys from an issuer's own endpoint cannot be run on a disposable backend this way; the owner rules on both halves, the test identity and the production provider (E-13, S13, Sc L2-9, D11) +- [non-blocking] Extension E-15: the doc does not say what the fixture app contains, how a fault enters a run, what a test may observe, or what a test controls on a backend that offers no clock and no scheduler switch; the option taken here is the rules below; two of them read the doc for the owner: the production-configuration run of Sc L2-9 is taken as the scenarios routed to the production composition, where the corpus earlier asked for the full Layer 0 to 2 suite on a release build, and evidence records that back a pass claim are kept in the repository; whether the durations Layer 3 pins become configuration, so that a test can outwait them, is left to Layer 3 (E-15, Acceptance scenarios, Sc L2-9) + +## Rule + +- [extension] Parity with a release for Sc L2-9 means the same code path, schema, concurrency and authority model: the grants, namespaces, entry points and actor mapping, and Convex's own visibility check on every call a caller under test makes (E-13, S13, Sc L2-9) +- [extension] A caller under test is an ordinary client that carries a token signed by the fixture issuer, or no token (E-13) +- [extension] A composition that runs natively reads its issuer, its application ID and its key set from the deployment's environment variables, and the harness sets the key set to a data URI that holds the fixture issuer's public key (E-13, S13) +- [extension] No caller under test carries the admin key, alone or acting as an identity, because a call made with it passes Convex's visibility check (E-13) +- [extension] The fixture issuer is the identity source of a native run and its one named difference from production; it is recorded as the identity source and is not an adjusted configuration (E-13, Sc L2-9) +- [extension] The harness uses the admin key to deploy, to generate code, to set and read environment variables, to read stored data and the function log, and to run fixture functions (E-15) +- [extension] Every Convex CLI call the harness makes names the backend it started on the command line and gets an environment of its own, so that no variable of the caller's shell and no dotenv file selects another deployment (E-15) +- [extension] No generated secret, the instance secret or the admin key, reaches an error message, test output or a record (E-15) +- [extension] An assertion about authority, identity or visibility is made through an ordinary client (E-15, E-13) +- [extension] A native scenario runs on one of two compositions, the fixture composition or the production composition, and its example names which (E-15) +- [extension] The fixture composition mounts the shared libraries beside fixture contexts and fixture functions; the production composition is the example application with no test-only function registered (E-15, Acceptance scenarios) +- [extension] A scenario runs on the production composition unless it needs a fixture-only state or operation, a fault or a fixture function (E-15) +- [extension] The production-configuration run of Sc L2-9 is the scenarios routed to the production composition, deployed with no test-only function and no adjusted configuration (E-15, Sc L2-9) +- [extension] A scenario on the fixture composition is native evidence for its own row; it runs the shared libraries with their own tables and the same authority model, beside fixture contexts whose state is the fixture's own (E-15, First experiment) +- [extension] The shared libraries carry no test hook: they read no flag and take no callback that exists only for a test (E-15) +- [extension] A fault inside a function, a throw at a chosen point or a part that fails, enters a native run only through something the fixture composition supplies: a fixture context's declaration, a fixture function, or a dependency the libraries take from every composition (E-15) +- [extension] A fault outside the functions, a killed backend, a closed client or a lost response, comes from the harness's controls (E-15) +- [extension] A native assertion reads only what a client or the harness's admin access can read: a returned value, the data of a thrown error, stored documents and the function log (E-15) +- [extension] A table read or a function log read that may be incomplete fails (E-15) +- [extension] Where a scenario asserts that something did not run, the test shows the refusal's own error, shows that nothing the thing writes is stored, and shows on the same deployment that the thing exists and runs when called with admin access; where no error tells a refusal from a failure, the fixture composition supplies a part that fails the call if it runs (E-15) +- [extension] An error text, a count or a wait that an example of this harness binds is what the pinned backend release answers; another answer on a new release is a finding to rule on, and the example is rebound, not the test repaired to keep the old value (E-15) +- [extension] The native tier controls a run through four things: the backend process, its clients, the deployment's environment variables and, on the fixture composition, fixture functions (E-15) +- [extension] The harness starts the backend on fresh storage, stops it, kills it, and restarts it on the same storage (E-15, S13) +- [extension] The harness registers the cleanup of a backend before the start of that backend can stall; a run that ends or is interrupted kills every backend process it started and removes every backend directory (E-15) +- [extension] Before the harness signals a process it reads from the process table the instance name and the storage path it gave that backend, and it signals no process that does not show both; a backend whose process it cannot inspect keeps its directory and its ownership record, and the run fails naming it (E-15) +- [extension] A client under test may close, and may lose a response (E-15) +- [extension] The native tier cannot move the backend's clock and cannot switch its scheduler off (E-15, S13) +- [extension] A scenario that needs time to pass waits in real time or shortens a duration that its owning Spec makes configuration, and a run with a shortened duration is adjusted configuration (E-15, Acceptance scenarios) +- [extension] A native run's record names the backend that ran by the hash of its executable and, when that hash is the pinned one, by its release; it names the identity source, and the commit with whether the tree was clean (E-15, Acceptance scenarios) +- [extension] A run's record takes its facts from the backends that ran, and a measurement is stored on the test that recorded it (E-15, Acceptance scenarios) +- [extension] A claim that a tier passed cites a record that is kept in the repository and was made on a clean tree at the commit it names (E-15, Acceptance scenarios) + +## Design + +The harness is plain test support with no Convex function in it. The entries whose keys start with `fn` or `type` are the stubs later slices build their native tests on, as signatures; the code under `harness/` holds the rest. The entries whose keys start with `observed` are readings of the backend release the repository pins, `precompiled-2026-09-28-5c7cb5b`, with `convex` 1.46.0, taken on 2026-10-01. They are not rules, and they are read again when the pin moves. + +- backend: [extension] the disposable backend is the `convex-local-backend` binary of one pinned release, checked against a recorded hash; the harness starts it on local ports with fresh storage, a generated instance secret and the admin key the binary derives from it, sets the environment variables, deploys the composition's function directory, and at the end of the test stops the process and removes the storage (E-15, S13) +- identity: [extension] a caller is a `ConvexHttpClient` or a `ConvexClient` whose `setAuth` carries a token the harness signs; `auth.config.ts` declares one `customJwt` provider whose `issuer`, `applicationID` and `jwks` are environment variables; the token carries `iss`, `aud`, `sub`, `iat` and `exp` and no other claim, so `ctx.auth.getUserIdentity()` in a public entry returns the identity those claims yield, and `establishActor`, the grants read and the namespace rules run unchanged as long as they read nothing a provider adds beyond them (E-13, S13, D11) +- adminReads: [extension] the harness reads a table of the app or of a component, and the function log's completion records, with the admin key; a table read returns every document with every Convex value intact or fails, never a sample, and is several queries, not one snapshot; a log read returns every completion record after a point the test marks or fails, and a mark taken before a restart cannot be read after it; a completion record carries at least `identifier`, `componentPath`, `requestId`, `executionTime` in seconds, `cachedResult` and `error` (E-15) +- typeFixtureIssuer: [extension] `interface FixtureIssuer { issuer: string; applicationID: string; jwks: string; token(subject: string): Promise }` (E-13) +- typeBackend: [extension] `interface Backend { url: string; adminKey: string; issuer: FixtureIssuer; admin: AdminAccess; stop(): Promise; kill(): Promise; restart(): Promise; dispose(): Promise; facts(): BackendFacts }`; `stop` and `kill` keep the storage; `dispose` alone removes it and is final, so a disposed backend cannot be started again; `adminKey` is there for the one example that shows why no caller carries it (E-15) +- typeAdminAccess: [extension] `interface AdminAccess { deploy(): Promise; codegen(): Promise; setEnvironment(variables: Record): Promise; environment(): Promise>; run(name: string, args?: Record, options?: { component?: string }): Promise; readTable(table: string, options?: { component?: string; pageSize?: number }): Promise[]>; logMark(): Promise; completionsSince(mark: LogMark, until: (records: readonly CompletionRecord[]) => boolean, timeoutMs?: number): Promise }` holds every use of the admin key and takes no identity (E-15) +- fnOrdinaryClient: [extension] `ordinaryClient(url: string, options?: { token?: string; fetch?: typeof fetch }): ConvexHttpClient` (E-13) +- fnOrdinarySocketClient: [extension] `ordinarySocketClient(url: string, options?: { token?: string; webSocket?: typeof WebSocket }): ConvexClient` (E-13) +- fnCountingFetch: [extension] `countingFetch(options: { loseResponses: boolean }): { fetch: typeof fetch; requests(): number }` delivers each request and, when it loses responses, throws after the backend has answered (E-15) +- fnHeldWebSocket: [extension] `heldWebSocket(): { WebSocket: typeof WebSocket; holdResponses(): void; pauseConnections(): void; release(): void }`; a socket that closes drops the responses it held (E-15) +- fnWatchQuery: [extension] `watchQuery>(client: ConvexClient, query: Query, args: FunctionArgs): { values: readonly FunctionReturnType[]; error: Error | undefined; until(matches: (value: FunctionReturnType) => boolean, description: string, timeoutMs?: number): Promise>; stop(): void }` (E-15) +- fnFixtureBackend: [extension] `fixtureBackend(options?: { issuer?: string; executable?: Executable; parentDirectory?: string; deploy?: boolean }): Promise` starts a backend, deploys the fixture composition unless `deploy` is `false`, and removes the backend when the test finishes, however it finishes; the executable and parent directory options let the harness's own tests exercise the same cleanup path (E-15, E-13) +- fnMeasure: [extension] `measure(name: string, value: JsonValue): void` stores a measurement on the running test (E-15) +- typeBackendFacts: [extension] `interface BackendFacts { composition: "fixture" | null; installedLayers: string[]; executable: { release: string | null; sha256: string; source: "cache" | "CONVEX_BACKEND_BINARY" }; identitySource: { kind: "fixture issuer"; issuer: string }; environment: string[]; dataset: "empty at start" }` (E-15, E-13) +- typeNativeTestEntry: [extension] `interface NativeTestEntry { name: string; file: string; result: "passed" | "failed" | "skipped"; durationMs: number | null; errors: string[]; backends: BackendFacts[]; measurements: { name: string; value: JsonValue }[] }` (E-15) +- typeNativeRunRecord: [extension] `interface NativeRunRecord { tier: "native"; commit: string; clean: boolean; command: string; startedAt: string; finishedAt: string; result: "passed" | "failed" | "interrupted"; versions: Record; tests: NativeTestEntry[]; unhandledErrors: string[] }`; it has no field for adjusted configuration until a slice adjusts one (E-15, Acceptance scenarios) +- fixtureLayout: [extension] the repository has one `package.json`; the root `convex.json` names `fixture/convex/` as the fixture composition's function directory, and the CLI runs in the repository root (E-15) +- fixtureComponent: [extension] the fixture composition mounts one plain component as `annex`; a fixture context gets its own name when a slice adds it (E-15) +- watchMode: [extension] `npm run dev` starts one backend through the harness and runs `convex dev` against it as a long-lived child, so that the generated files follow a fixture file as it is saved; it is a developer's tool and no test runs through it; it is the one place outside `AdminAccess` that uses the admin key, for that CLI call and to read the backend's site URL, and it prints neither (E-15) +- sweepOwnership: [extension] the sweep reads a backend's ownership record, then the process table, then signals by process id; a process id cannot change hands while its process is alive, so the interval between the read and the signal matters only for a backend that exits during the sweep, and the harness does not close it: no call available to a process that did not spawn the backend does; a process whose command line cannot be read, such as a kernel thread that took a dead backend's process id, keeps that backend's directory and fails that sweep, and the next run starts from a fresh root (E-15) +- observedWriteRate: [extension] the backend holds and then refuses writes past a rate, with `TooManyWrites` and the text "limited to 4 MiB bytes written per 1 second"; one transaction that writes 8.8 MiB commits, so the rate is not a bound on a transaction; this is why a test that writes more than the rate paces its transactions, and why a throughput measured on this backend states the limit (E-15, S13) +- observedFunctionLog: [extension] the backend keeps its last 1000 function log entries in memory and loses them at a restart, and it logs one completion record per top-level call and none for a nested or component call inside it; the harness reads the log by polling `/api/stream_function_logs` from a mark built on the machine clock (E-15) +- observedTableRead: [extension] the backend answers a read of a missing table with an empty page; the harness reads through the system query that `convex data` uses and checks an empty result against the table list (E-15) +- evidenceKept: [extension] every native run writes its record under `evidence/runs/`, which git ignores; the record of the run a Spec or a commit cites is copied to `evidence/` and committed; other runs' records are not kept (E-15, Acceptance scenarios) + +## Verification — reviewed + +- A reviewer confirms that every native example names its composition, and that no native test gives a caller under test the admin key except the one example that shows why not. +- A pure test reads the native tests' sources and fails when a test other than that one example reads the admin key. +- A reviewer confirms that each example which asserts a refusal also shows, on the same deployment, that the refused thing exists. diff --git a/design/specs/platform/transactional-domain-platform.sdp.md b/design/specs/platform/transactional-domain-platform.sdp.md index 1b07855..a1f9997 100644 --- a/design/specs/platform/transactional-domain-platform.sdp.md +++ b/design/specs/platform/transactional-domain-platform.sdp.md @@ -37,7 +37,7 @@ Citations in this corpus use the tokens the plan fixes: D for a decision, Law fo - risk: Every read of context data from the parent is a component call whose cost the first experiment must measure; if it breaks the read budgets, the shape of the context component changes before Layer 3 is designed (D2, Probe 3, OQ1) - risk: Nothing has run yet; the sources support the Convex facts, not the correctness or performance of the design (Sources) - assumption: Mutations are serializable under optimistic concurrency, and component calls commit or roll back with the calling mutation (F1, F2) -- assumption: Four ledger facts are assumed rather than documented; each Spec that rests on one names it and the probe plan names the probe (F14, F15, F16, F17) +- assumption: One ledger fact is still assumed, and three that the doc lists as assumed are rechecked or probed in the corpus; each Spec that rests on one names it and the probe plan names the probe (F14, F15, F16, F17) ### Open questions diff --git a/design/tools/check.py b/design/tools/check.py index cb3fd4c..55c94f8 100644 --- a/design/tools/check.py +++ b/design/tools/check.py @@ -3,7 +3,11 @@ Exits 0 when the corpus, README.md and the review ledger agree; prints each failure and exits 1 otherwise. SESSIONS.md says when to run it. -`python3 design/tools/check.py --slice S1` lists the findings a slice takes and exits. +`python3 design/tools/check.py --slice S1` lists the findings a slice takes and exits. It reads the +ledger only and writes nothing, so a read-only job can run it. + +The Protocol's root is the repository root: the graph holds the Specs under design/specs and the +test anchors that verify them, which live outside design/. Output goes to generated/ at the root. The Protocol's CLI is the pinned dependency in the repository's package.json; run `npm ci` once. What `sdp validate` checks is the Protocol's. What this script adds is this project's policy: @@ -39,19 +43,27 @@ def fail(msg): def run(args): - return subprocess.run(["node", SDP] + args, capture_output=True, text=True, cwd=DESIGN) + return subprocess.run(["node", SDP] + args, capture_output=True, text=True, cwd=ROOT) + + +if "--slice" in sys.argv: + wanted = sys.argv[sys.argv.index("--slice") + 1] + for item in json.load(open(os.path.join(DESIGN, "reviews", "consensus-ledger.json"), encoding="utf-8"))["ledger"]: + if item.get("slice") == wanted and item["status"] in ("open", "partially-fixed", "owner"): + print(f"{item['id']} | {item['severity']} | {item['status']} | caught by: {item.get('detector', '?')} | {', '.join(item['files'])}") + sys.exit(0) # 1. The graph: validate and readiness divergence. out = run(["validate", "."]) text = out.stdout + out.stderr -graph = re.search(r"(\d+) specs · (\d+) packs · \d+ anchors → (\d+) nodes · (\d+) edges", text) +graph = re.search(r"(\d+) specs · (\d+) packs · (\d+) anchors → (\d+) nodes · (\d+) edges", text) verdict = re.search(r"validate: (\d+) errors · (\d+) warnings", text) if not graph or not verdict: fail("validate printed no summary line; run `npm ci` at the repository root, then check " + SDP) print("\n".join(failures)) sys.exit(1) -n_specs, n_packs, n_nodes, n_edges = (int(x) for x in graph.groups()) +n_specs, n_packs, n_anchors, n_nodes, n_edges = (int(x) for x in graph.groups()) n_errors, n_warnings = (int(x) for x in verdict.groups()) warnings = [l for l in text.splitlines() if "[warning]" in l] if n_errors: @@ -115,7 +127,7 @@ def run(args): # 3. README.md against the Specs. readme = open(os.path.join(DESIGN, "README.md"), encoding="utf-8").read() -expected_line = f"{n_specs} specs · {n_packs} packs · 0 anchors → {n_nodes} nodes · {n_edges} edges (0 errors, 0 warnings)" +expected_line = f"{n_specs} specs · {n_packs} packs · {n_anchors} anchors → {n_nodes} nodes · {n_edges} edges (0 errors, 0 warnings)" if expected_line not in readme: fail("README's expected validate output is stale; it should read: " + expected_line) if "validate: 0 errors · 0 warnings" not in readme: @@ -139,12 +151,6 @@ def run(args): fail(f"ledger item {item['id']} has status {item['status']!r}; allowed: {sorted(LEDGER_STATUSES)}") by_status[item["status"]] += 1 -if "--slice" in sys.argv: - wanted = sys.argv[sys.argv.index("--slice") + 1] - for item in ledger: - if item.get("slice") == wanted and item["status"] in ("open", "partially-fixed", "owner"): - print(f"{item['id']} | {item['severity']} | {item['status']} | caught by: {item.get('detector', '?')} | {', '.join(item['files'])}") - sys.exit(0) by_slice = collections.Counter(item.get("slice", "none") for item in ledger if item["status"] == "open") digest = hashlib.sha256() diff --git a/docs/sdp-feedback.md b/docs/sdp-feedback.md index 4a8f885..488eb2b 100644 --- a/docs/sdp-feedback.md +++ b/docs/sdp-feedback.md @@ -4,6 +4,45 @@ A running file, addressed to the Protocol's maintainers. Each `adopt` unit and e Every number here was measured on this corpus on the date of its section. Re-run before relying on one. +## 2026-10-01, slice S0, the first build on the Protocol at `5993da7` + +### A Spec with designed stubs is a maturity the Protocol cannot state + +The owner's direction during this slice: the Protocol should support Specs at any level of maturity, and that includes a Spec whose key abstractions are designed as stubs. The earlier generation of this tooling, architect, did that, and the Protocol is its successor. + +What happened here. Before a build job, a design agent decides the key abstractions of a Spec: the exported interface of a module, the shape of a record, the names. The slice's first design brief was this corpus's own mistake: it asked for a design that left the implementer nothing to decide, with no bound, and the agent wrote toward a long design file outside the repository and then prototyped the code, for about an hour. What the mistake showed is that the corpus had no home for the output of a design pass. A Spec that can hold the stubs is what bounds a design pass: the design is done when the Spec's key abstractions are stated as stubs, and the implementer writes the rest. + +What the Protocol offers today, and where it stops: + +1. **Readiness has no rung for it.** The rungs are `idea`, `scoped`, `defined` and `ready`, and they describe the intent. A Spec at `defined` with no design and a Spec at `defined` whose interfaces are settled look the same in the graph, so the build backlog cannot tell which Specs an implementer can start on. +2. **A stub in code reads as `implemented`.** The only binding from code to a Spec is a `codeAnchor` with `satisfies`, and the Protocol's `CONTEXT.md` defines `implemented` as one or more `satisfies` edges that resolve to the Spec, and the drift alarm as `implemented` without `ready`. A file of signatures with no bodies would claim the Spec is realized, and on a Spec below `ready` it would raise the drift alarm. There is no anchor, or flag on an anchor, that says "this is the designed surface, not the implementation". +3. **A stub in the Spec is one line of prose.** A `## Design` entry is `key: value` on one physical line, and fenced code is refused there. This corpus has 792 Design entries, counted as the bullets under every `## Design` heading on 2026-10-01, and 653 of them hold inline code; a multi-member interface on one line is hard to read and to review. Nothing checks a Design entry's signature against the exported one, so the two drift apart without a finding. Authored order for Design entries, already asked for below, matters more once the entries are stubs a reader walks in order. + +The owner's proposal for item 3: the Markdown carrier would accept a `ts` fenced block in a Spec, holding the design of its key contracts: the interfaces, types and function signatures a reader would otherwise find squeezed into one-line Design entries. Today the carrier accepts two fences, `gwt-vocabulary` under `## Example space` and `gwt` on an example, and refuses every other. + +What that leaves for the maintainers to decide: + +- Where the block is lawful: under `## Design`, under `## Contract`, or both, and whether one block per Spec or one per named contract. +- Whether the graph reads it. Read as data, each declared name could be an entry that entry search (recipe 23) finds, and a later check could compare a stub with the exported signature of the code anchored to the Spec. Read as content only, it is still what a reviewer and an implementer need. +- How a Spec with such a block shows in the graph, which is items 1 and 2: a `designed` fact or rung that the build backlog can filter on, and a code binding for a stub file that does not derive `implemented`. + +What this corpus does until then: the design agent proposes `## Design` entries and rules for the Specs a slice builds, signatures only, one line each, and the main thread writes them into the Spec. Stub files in code carry no anchor until they have bodies. If the carrier comes to accept a `ts` block, the Design entries that are declarations move into blocks, and the design pass writes its stubs there. + +### What the build learned about binding an example to a test + +Slice S0 bound eighteen examples to native tests with `bindExample` from the Vitest adapter. Seven things came up. The first two were read in `dist/adapters/vitest.js` of the pinned package, the others met during the build on 2026-10-01. + +1. **A step is bound by its exact text.** `planExample` looks a step up as `bindings[step.text]`. When a review rewords a Then, the test stops compiling, because the contract's step type changed, and a run that skips the compiler fails with `bindings[step.text] is not a function`. That is the right alarm when the meaning changed. It is noise when the wording was polished and the meaning stayed. This corpus now changes an example's steps and its test in the same commit. A stable key per step, beside its text, would let prose be reworded without touching the test, and the adapter could then say "no binding for step " in place of the type error. +2. **`bindExample`'s `after` runs only when every step passed.** The adapter awaits the plan and then `after`, with no `finally`. A test that owns a process cannot clean up there: a failed step would leave the process running. The harness registers its cleanup with Vitest's `onTestFinished` before the first await, and `after` is unused. A `finally` around the plan, or a sentence in the adapter's documentation, would make this plain. +3. **A test binds only to a Spec under the same root.** The corpus root was `design/`. The tests are outside it, so the root moved to the repository root, and `check.py` and every `sdp` call moved with it. +4. **A parent with no example space gets no registrar.** The harness Spec and the fact Specs have examples and no `## Example space`. Their tests import the example's step contract from `generated/contracts/` and call `bindExample`. That works, and it means `sdp build` has to run before the compiler: `npm run typecheck` and every test command here start with it. +5. **`specTest`'s `verifies` takes one `ref`.** A test that covers two examples needs two anchors. That fits one test per example, which is how every native test here is written, and is worth a line in the authoring skill. + +6. **The slice built code for a day before any of it carried a `codeAnchor`.** The tests were anchored from the first build, because `bindExample` needs the contract. Nothing asked for the other half, so the graph showed the harness Spec with verifiers and no implementation, the drift alarm (recipe 2) returned nothing, and recipe 4 answered `coverageUnknown` for every harness file. A side read of the corpus found it at the close. One anchor on the harness kept validate at 0 warnings and made recipe 2 list the Spec as built and not `ready`, which is this project's queue of Specs waiting for the owner. A warning for "a Spec whose examples are bound and that no code satisfies" would have asked at the right moment. +7. **The build backlog reads stated `ready`.** Here the owner states `ready` after a slice passes, so recipe 1 stays empty for the whole build. The same question at `defined` with a clear floor is a short local body and gave 80 Specs. A parameter for the rung would make recipe 1 serve both ways of working. + +One more, about evidence. Twice in this slice a reviewer's mutation showed a bound test that passed while proving less than its example said: of twenty mutations run on a native backend, nine left their test green, three on the first build and six on the second. The binding says which test claims an example. It cannot say whether the test would fail if the example were false. A corpus that leans on `verified` needs mutation runs beside it, and the Protocol's documentation could say so where it defines the term. + ## 2026-10-01, adopting pull request 26 at `5993da7` The corpus now depends on the Protocol at commit `5993da7b8f61ce00853b790cd9ffb428842b66d0`, the head of `feature/adopter-driven-hardening`. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..d3b541f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,15 @@ +import tseslint from "typescript-eslint"; +import convex from "@convex-dev/eslint-plugin"; +export default tseslint.config( + { ignores: ["**/_generated/**", "generated/**"] }, + ...tseslint.configs.recommended, + ...convex.configs.recommended.map((config) => ({ + ...config, + files: ["fixture/convex/**/*.ts"], + })), + // A composition that runs natively reads its auth provider from the deployment's environment. + { + files: ["fixture/convex/auth.config.ts"], + rules: { "@convex-dev/no-process-env": "off" }, + }, +); diff --git a/evidence/README.md b/evidence/README.md new file mode 100644 index 0000000..86b1135 --- /dev/null +++ b/evidence/README.md @@ -0,0 +1,26 @@ +# Run evidence + +Every native run writes one JSON record to `evidence/runs/`. Git ignores that directory. When a Spec or a commit cites a run, copy that run's record to `evidence/` and commit it. No other record is kept. + +A record that backs a claim says `"clean": true`. It was made on a tree with no uncommitted and no untracked file, at the commit it names. A kept record names the commit it ran on, which is the parent of the commit that adds the record. That commit changes only `evidence/`. A record that says `"clean": false` backs no claim. + +## What a record holds + +- `commit` and `clean`: the commit that was checked out when the run started, and whether the tree was clean then. +- `command`, `startedAt`, `finishedAt` and `result`. +- `versions`: Node and the packages the tiers depend on, read from the installed packages. +- `tests`: one entry per test, with its result, its error messages, its backends and its measurements. + +A test's `backends` has one entry per backend that became ready. The harness writes the entry from the backend that ran: + +- `executable`: the SHA-256 of the executable, where it came from and its release. The release is named only when the hash is the one `harness/backend-release.json` pins. Otherwise it is `null`. +- `identitySource`: the fixture issuer that the backend's environment variables named. The fixture issuer is the one difference from production in how a caller gets an identity. It is not adjusted configuration. +- `composition` and `installedLayers`: what the harness deployed. +- `environment`: the names of the environment variables the harness set. +- `dataset`: every backend starts on empty storage, so a test's data is what the test wrote. + +## Measurements + +A test records a measurement with `measure(name, value)` from `harness/native.ts`. The measurement is stored on the test that was running and appears under that test's entry. A value is plain JSON. A test that records a 64-bit integer or bytes writes it as text. + +A probe records sizes such as timings and counts, and asserts only what its example binds. A size in a record is one machine's reading on one run. diff --git a/evidence/native-20261001T162936Z-ba5eddd.json b/evidence/native-20261001T162936Z-ba5eddd.json new file mode 100644 index 0000000..19f1071 --- /dev/null +++ b/evidence/native-20261001T162936Z-ba5eddd.json @@ -0,0 +1,3539 @@ +{ + "tier": "native", + "commit": "ba5edddbda73ce8d2ecd3dd843c497116ee9a762", + "clean": true, + "command": "vitest run --project native", + "startedAt": "2026-10-01T16:29:36.247Z", + "finishedAt": "2026-10-01T16:29:58.709Z", + "result": "passed", + "versions": { + "node": "v24.21.0", + "convex": "1.46.0", + "convex-helpers": "0.1.124", + "convex-test": "0.0.60", + "vitest": "5.0.3", + "typescript": "7.0.2", + "react": "19.3.0", + "happy-dom": "20.14.5" + }, + "tests": [ + { + "name": "native: stop and kill keep stored rows across restart, and dispose removes storage twice", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 1819.2924580000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://changed-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 4: how deep nested calls may go (spec:facts.f13-transactions-have-limits.probe-4-nesting-depth) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-nesting-depth.test.ts", + "result": "passed", + "durationMs": 1618.2380000000003, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "stackTrial", + "value": { + "stack": 1, + "answer": 1, + "error": null, + "rows": 1 + } + }, + { + "name": "stackTrial", + "value": { + "stack": 2, + "answer": 2, + "error": null, + "rows": 2 + } + }, + { + "name": "stackTrial", + "value": { + "stack": 3, + "answer": 3, + "error": null, + "rows": 3 + } + }, + { + "name": "stackTrial", + "value": { + "stack": 4, + "answer": 4, + "error": null, + "rows": 4 + } + }, + { + "name": "stackTrial", + "value": { + "stack": 5, + "answer": 5, + "error": null, + "rows": 5 + } + }, + { + "name": "stackTrial", + "value": { + "stack": 6, + "answer": 6, + "error": null, + "rows": 6 + } + }, + { + "name": "stackTrial", + "value": { + "stack": 7, + "answer": 7, + "error": null, + "rows": 7 + } + }, + { + "name": "stackTrial", + "value": { + "stack": 8, + "answer": 8, + "error": null, + "rows": 8 + } + }, + { + "name": "stackTrial", + "value": { + "stack": 9, + "answer": 9, + "error": null, + "rows": 9 + } + }, + { + "name": "stackTrial", + "value": { + "stack": 10, + "answer": null, + "error": "Error: [Request ID: 86d42b2304f90e33] Server Error\nUncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Cross component call depth limit exceeded. Do you have an infinite loop in your app?\n at async handler (../fixture/convex/limits.ts:138:12)\n\n at async handler (../fixture/convex/limits.ts:138:12)\n\n at async handler (../fixture/convex/limits.ts:138:12)\n\n at async handler (../fixture/convex/limits.ts:138:12)\n\n at async handler (../fixture/convex/limits.ts:138:12)\n\n at async handler (../fixture/convex/limits.ts:138:12)\n\n at async handler (../fixture/convex/limits.ts:138:12)\n\n at async handler (../fixture/convex/limits.ts:138:12)\n\n at async handler (../fixture/convex/limits.ts:138:12)\n", + "rows": 0 + } + }, + { + "name": "depthBoundary", + "value": { + "deepest": 9, + "firstFailure": 10 + } + } + ] + }, + { + "name": "Probe 5: the client hook splits a page across the boundary and loses rows (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.test.ts", + "result": "passed", + "durationMs": 1786.500959, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "hookBeforeInsert", + "value": { + "rows": 30, + "status": "Exhausted" + } + }, + { + "name": "hookCappedPage", + "value": { + "status": "SplitRequired", + "rows": 20, + "continueCursor": "[4,1790872178299.261,\"jd70p070cnmqgpednbezkgwx118fesvv\"]", + "endCursor": "[9,1790872178299.2622,\"jd76wqytnnw0tg2j0f8aje6whd8ffbns\"]" + } + }, + { + "name": "hookAfterSplit", + "value": { + "rowsInList": 45, + "rowsShown": 40, + "status": "Exhausted", + "missingPositions": [ + 5, + 6, + 7, + 8, + 9 + ] + } + }, + { + "name": "hookMissingCursorRange", + "value": { + "missing": [ + "jd74w5mtgm9cbb73e30rd0vgzn8ffe55", + "jd737rgfne5n8z2bjrnw4kp9558fer1k", + "jd70yzvsr06n37v6r1tr0qy05d8femac", + "jd7bt6jxamm0451qdkh8ws7vqn8fe7ky", + "jd76wqytnnw0tg2j0f8aje6whd8ffbns" + ], + "betweenCursors": [ + "jd74w5mtgm9cbb73e30rd0vgzn8ffe55", + "jd737rgfne5n8z2bjrnw4kp9558fer1k", + "jd70yzvsr06n37v6r1tr0qy05d8femac", + "jd7bt6jxamm0451qdkh8ws7vqn8fe7ky", + "jd76wqytnnw0tg2j0f8aje6whd8ffbns" + ] + } + } + ] + }, + { + "name": "Probe 3: the cost of a component call from a mutation (spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.test.ts", + "result": "passed", + "durationMs": 2953.540167, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "costCall", + "value": { + "round": 0, + "path": "helper", + "nonce": 1000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 0, + "path": "helper", + "nonce": 1000, + "executionTimeSeconds": 0.004024417, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.004024417, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 0, + "path": "nested", + "nonce": 2000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 0, + "path": "nested", + "nonce": 2000, + "executionTimeSeconds": 0.097926958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.097926958, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 0, + "path": "component", + "nonce": 3000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 0, + "path": "component", + "nonce": 3000, + "executionTimeSeconds": 0.142766125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.142766125, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 1, + "path": "helper", + "nonce": 4000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 1, + "path": "helper", + "nonce": 4000, + "executionTimeSeconds": 0.002380875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002380875, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 1, + "path": "nested", + "nonce": 5000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 1, + "path": "nested", + "nonce": 5000, + "executionTimeSeconds": 0.049924292, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.049924292, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 1, + "path": "component", + "nonce": 6000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 1, + "path": "component", + "nonce": 6000, + "executionTimeSeconds": 0.086346541, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.086346541, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 2, + "path": "helper", + "nonce": 7000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 2, + "path": "helper", + "nonce": 7000, + "executionTimeSeconds": 0.002413667, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002413667, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 2, + "path": "nested", + "nonce": 8000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 2, + "path": "nested", + "nonce": 8000, + "executionTimeSeconds": 0.057140208, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.057140208, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 2, + "path": "component", + "nonce": 9000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 2, + "path": "component", + "nonce": 9000, + "executionTimeSeconds": 0.096843584, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.096843584, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 3, + "path": "helper", + "nonce": 10000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 3, + "path": "helper", + "nonce": 10000, + "executionTimeSeconds": 0.002460667, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002460667, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 3, + "path": "nested", + "nonce": 11000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 3, + "path": "nested", + "nonce": 11000, + "executionTimeSeconds": 0.063843583, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.063843583, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 3, + "path": "component", + "nonce": 12000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 3, + "path": "component", + "nonce": 12000, + "executionTimeSeconds": 0.108685792, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.108685792, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 4, + "path": "helper", + "nonce": 13000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 4, + "path": "helper", + "nonce": 13000, + "executionTimeSeconds": 0.002062708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002062708, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 4, + "path": "nested", + "nonce": 14000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 4, + "path": "nested", + "nonce": 14000, + "executionTimeSeconds": 0.051022875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.051022875, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 4, + "path": "component", + "nonce": 15000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 4, + "path": "component", + "nonce": 15000, + "executionTimeSeconds": 0.074557792, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.074557792, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 5, + "path": "helper", + "nonce": 16000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 5, + "path": "helper", + "nonce": 16000, + "executionTimeSeconds": 0.001990542, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.001990542, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 5, + "path": "nested", + "nonce": 17000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 5, + "path": "nested", + "nonce": 17000, + "executionTimeSeconds": 0.050578042, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.050578042, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 5, + "path": "component", + "nonce": 18000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 5, + "path": "component", + "nonce": 18000, + "executionTimeSeconds": 0.076401459, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.076401459, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 6, + "path": "helper", + "nonce": 19000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 6, + "path": "helper", + "nonce": 19000, + "executionTimeSeconds": 0.002159875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002159875, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 6, + "path": "nested", + "nonce": 20000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 6, + "path": "nested", + "nonce": 20000, + "executionTimeSeconds": 0.0577745, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.0577745, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 6, + "path": "component", + "nonce": 21000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 6, + "path": "component", + "nonce": 21000, + "executionTimeSeconds": 0.129952833, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.129952833, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 7, + "path": "helper", + "nonce": 22000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 7, + "path": "helper", + "nonce": 22000, + "executionTimeSeconds": 0.003563042, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.003563042, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 7, + "path": "nested", + "nonce": 23000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 7, + "path": "nested", + "nonce": 23000, + "executionTimeSeconds": 0.070735792, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.070735792, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 7, + "path": "component", + "nonce": 24000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 7, + "path": "component", + "nonce": 24000, + "executionTimeSeconds": 0.093798292, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.093798292, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 8, + "path": "helper", + "nonce": 25000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 8, + "path": "helper", + "nonce": 25000, + "executionTimeSeconds": 0.002043833, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002043833, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 8, + "path": "nested", + "nonce": 26000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 8, + "path": "nested", + "nonce": 26000, + "executionTimeSeconds": 0.055025166, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.055025166, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 8, + "path": "component", + "nonce": 27000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 8, + "path": "component", + "nonce": 27000, + "executionTimeSeconds": 0.083361167, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.083361167, + "cachedResult": false + } + ] + } + }, + { + "name": "costMedians", + "value": { + "helper": 0.002380875, + "nested": 0.057140208, + "component": 0.093798292, + "componentMinusHelperSeconds": 0.091417417, + "componentMinusHelperPerReadSeconds": 0.00182834834 + } + } + ] + }, + { + "name": "Probe 3: the cost of a component call from a query (spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query) > executes the bound example against a fresh world", + "file": "tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.test.ts", + "result": "passed", + "durationMs": 2976.893833, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "costCall", + "value": { + "round": 0, + "path": "helper", + "nonce": 1000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 0, + "path": "helper", + "nonce": 1000, + "executionTimeSeconds": 0.00323125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.00323125, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 0, + "path": "nested", + "nonce": 2000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 0, + "path": "nested", + "nonce": 2000, + "executionTimeSeconds": 0.095424125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.095424125, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 0, + "path": "component", + "nonce": 3000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 0, + "path": "component", + "nonce": 3000, + "executionTimeSeconds": 0.134757167, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.134757167, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 1, + "path": "helper", + "nonce": 4000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 1, + "path": "helper", + "nonce": 4000, + "executionTimeSeconds": 0.002450083, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002450083, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 1, + "path": "nested", + "nonce": 5000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 1, + "path": "nested", + "nonce": 5000, + "executionTimeSeconds": 0.0514345, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.0514345, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 1, + "path": "component", + "nonce": 6000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 1, + "path": "component", + "nonce": 6000, + "executionTimeSeconds": 0.083226, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.083226, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 2, + "path": "helper", + "nonce": 7000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 2, + "path": "helper", + "nonce": 7000, + "executionTimeSeconds": 0.002576833, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002576833, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 2, + "path": "nested", + "nonce": 8000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 2, + "path": "nested", + "nonce": 8000, + "executionTimeSeconds": 0.053611292, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.053611292, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 2, + "path": "component", + "nonce": 9000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 2, + "path": "component", + "nonce": 9000, + "executionTimeSeconds": 0.09331125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.09331125, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 3, + "path": "helper", + "nonce": 10000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 3, + "path": "helper", + "nonce": 10000, + "executionTimeSeconds": 0.002431792, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002431792, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 3, + "path": "nested", + "nonce": 11000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 3, + "path": "nested", + "nonce": 11000, + "executionTimeSeconds": 0.084872166, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.084872166, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 3, + "path": "component", + "nonce": 12000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 3, + "path": "component", + "nonce": 12000, + "executionTimeSeconds": 0.083776917, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.083776917, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 4, + "path": "helper", + "nonce": 13000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 4, + "path": "helper", + "nonce": 13000, + "executionTimeSeconds": 0.00243125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.00243125, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 4, + "path": "nested", + "nonce": 14000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 4, + "path": "nested", + "nonce": 14000, + "executionTimeSeconds": 0.053758, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.053758, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 4, + "path": "component", + "nonce": 15000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 4, + "path": "component", + "nonce": 15000, + "executionTimeSeconds": 0.07368125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.07368125, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 5, + "path": "helper", + "nonce": 16000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 5, + "path": "helper", + "nonce": 16000, + "executionTimeSeconds": 0.002000084, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002000084, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 5, + "path": "nested", + "nonce": 17000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 5, + "path": "nested", + "nonce": 17000, + "executionTimeSeconds": 0.046178083, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.046178083, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 5, + "path": "component", + "nonce": 18000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 5, + "path": "component", + "nonce": 18000, + "executionTimeSeconds": 0.090190875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.090190875, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 6, + "path": "helper", + "nonce": 19000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 6, + "path": "helper", + "nonce": 19000, + "executionTimeSeconds": 0.002573958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002573958, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 6, + "path": "nested", + "nonce": 20000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 6, + "path": "nested", + "nonce": 20000, + "executionTimeSeconds": 0.066395875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.066395875, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 6, + "path": "component", + "nonce": 21000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 6, + "path": "component", + "nonce": 21000, + "executionTimeSeconds": 0.121993208, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.121993208, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 7, + "path": "helper", + "nonce": 22000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 7, + "path": "helper", + "nonce": 22000, + "executionTimeSeconds": 0.003451167, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.003451167, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 7, + "path": "nested", + "nonce": 23000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 7, + "path": "nested", + "nonce": 23000, + "executionTimeSeconds": 0.06384675, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.06384675, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 7, + "path": "component", + "nonce": 24000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 7, + "path": "component", + "nonce": 24000, + "executionTimeSeconds": 0.085675459, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.085675459, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 8, + "path": "helper", + "nonce": 25000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 8, + "path": "helper", + "nonce": 25000, + "executionTimeSeconds": 0.002171375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002171375, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 8, + "path": "nested", + "nonce": 26000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 8, + "path": "nested", + "nonce": 26000, + "executionTimeSeconds": 0.055667542, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.055667542, + "cachedResult": false + } + ] + } + }, + { + "name": "costCall", + "value": { + "round": 8, + "path": "component", + "nonce": 27000, + "error": null + } + }, + { + "name": "costSample", + "value": { + "round": 8, + "path": "component", + "nonce": 27000, + "executionTimeSeconds": 0.079934542, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.079934542, + "cachedResult": false + } + ] + } + }, + { + "name": "costMedians", + "value": { + "helper": 0.002450083, + "nested": 0.055667542, + "component": 0.085675459, + "componentMinusHelperSeconds": 0.08322537599999999, + "componentMinusHelperPerReadSeconds": 0.0016645075199999998 + } + } + ] + }, + { + "name": "Probe 5: pages of a component list stay contiguous under live writes (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.test.ts", + "result": "passed", + "durationMs": 1025.584708, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "the initial pinned page", + "value": { + "rows": 10, + "status": null, + "ids": [ + "jd71thjdvwfwy66650bnas4kxx8ffzds", + "jd71xqrhn54gnh9pv16d7tkqe18ffxc5", + "jd7f3ndwcwswkhpfgqe91ycrv98ffzma", + "jd76x07z0mfvq6snde2gzp0mvn8ff34a", + "jd7ev7ws81hhha2q6pmkqjf4gs8femyx", + "jd7bp82pd7389jznbbx10gjx7n8fe35e", + "jd76977bmxkreg7kmgw1xsr1ks8fepsx", + "jd731n6dz1sv2p1hphrvr0a6xx8ffdc6", + "jd7crhzdrdr8mqs22jq4a18ypx8fehxd", + "jd7db3tf5355hr159rk7ee7xph8ffzpz" + ] + } + }, + { + "name": "the initial pinned page", + "value": { + "rows": 10, + "status": null, + "ids": [ + "jd71zk49xz3x3etxh6972k6mmn8ff319", + "jd7f9qyep5j1ajqt8v6ym3jyv18fey3z", + "jd7apygx6w9fprgrcee3w632m18feztf", + "jd714d56znccas8a762ebcah9h8ff3bp", + "jd76meh22gcx64j5sdd9h8vs7x8feqcd", + "jd7dpyc2s2p7krzs1ryvktynah8ffqv0", + "jd795fh2d81mteh0p1ks8wg7t18ff8g1", + "jd7ezegq5em9q260k9gah2g2an8fe88m", + "jd77g3hdzs3np1ckjz0mxsrfz98ffaf3", + "jd7ebvcqgw02qtyj183jkwjsz98feztq" + ] + } + }, + { + "name": "the initial pinned page", + "value": { + "rows": 10, + "status": null, + "ids": [ + "jd7d89cvg242zjfzdprdr5paf58fe38q", + "jd7870v1dn1bpqh5hbwagkntys8ff921", + "jd76n3n42vcdpa1bc5p3abst758ff13q", + "jd73z5tesrpwtnyz6nfbp9kyz98fedg6", + "jd73bwqqqevw857k30mrybrgz18ff2hs", + "jd7819d83kywwq6cmwe3tbeasn8ffj5p", + "jd724vsfha7keacwajdtt4q7n18feejg", + "jd786kay3733hch0ts1j3pxf318fe6wh", + "jd7ak6wv8aw4b50znc3tm305sn8fect6", + "jd725g5xq8y3n3n99pm6nz5b918fez7t" + ] + } + }, + { + "name": "the initial pinned page", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "initialPinnedPages", + "value": [ + { + "rows": 10, + "isDone": false + }, + { + "rows": 10, + "isDone": false + }, + { + "rows": 10, + "isDone": false + }, + { + "rows": 0, + "isDone": true + } + ] + }, + { + "name": "the pages after the insert", + "value": { + "rows": 10, + "status": null, + "ids": [ + "jd71zk49xz3x3etxh6972k6mmn8ff319", + "jd7f9qyep5j1ajqt8v6ym3jyv18fey3z", + "jd7apygx6w9fprgrcee3w632m18feztf", + "jd714d56znccas8a762ebcah9h8ff3bp", + "jd76meh22gcx64j5sdd9h8vs7x8feqcd", + "jd7dpyc2s2p7krzs1ryvktynah8ffqv0", + "jd795fh2d81mteh0p1ks8wg7t18ff8g1", + "jd7ezegq5em9q260k9gah2g2an8fe88m", + "jd77g3hdzs3np1ckjz0mxsrfz98ffaf3", + "jd7ebvcqgw02qtyj183jkwjsz98feztq" + ] + } + }, + { + "name": "the pages after the insert", + "value": { + "rows": 10, + "status": null, + "ids": [ + "jd7d89cvg242zjfzdprdr5paf58fe38q", + "jd7870v1dn1bpqh5hbwagkntys8ff921", + "jd76n3n42vcdpa1bc5p3abst758ff13q", + "jd73z5tesrpwtnyz6nfbp9kyz98fedg6", + "jd73bwqqqevw857k30mrybrgz18ff2hs", + "jd7819d83kywwq6cmwe3tbeasn8ffj5p", + "jd724vsfha7keacwajdtt4q7n18feejg", + "jd786kay3733hch0ts1j3pxf318fe6wh", + "jd7ak6wv8aw4b50znc3tm305sn8fect6", + "jd725g5xq8y3n3n99pm6nz5b918fez7t" + ] + } + }, + { + "name": "the pages after the insert", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "the pages after the insert", + "value": { + "rows": 15, + "status": "SplitRecommended", + "ids": [ + "jd71thjdvwfwy66650bnas4kxx8ffzds", + "jd76tsv7dpsxb07nmwt2x1vwjs8fevtm", + "jd7dbyjy6f11jmb6sbqc8kxjw58fey0q", + "jd72w1rkw4w705e6jg542stw3h8ff4j6", + "jd7bas5aqtx1jtzhvwxrtc695x8ff9sf", + "jd72fkrtzdm7swsfya99ac9rks8fepfb", + "jd71xqrhn54gnh9pv16d7tkqe18ffxc5", + "jd7f3ndwcwswkhpfgqe91ycrv98ffzma", + "jd76x07z0mfvq6snde2gzp0mvn8ff34a", + "jd7ev7ws81hhha2q6pmkqjf4gs8femyx", + "jd7bp82pd7389jznbbx10gjx7n8fe35e", + "jd76977bmxkreg7kmgw1xsr1ks8fepsx", + "jd731n6dz1sv2p1hphrvr0a6xx8ffdc6", + "jd7crhzdrdr8mqs22jq4a18ypx8fehxd", + "jd7db3tf5355hr159rk7ee7xph8ffzpz" + ] + } + }, + { + "name": "pagesAfterInsert", + "value": [ + [ + "jd71thjdvwfwy66650bnas4kxx8ffzds", + "jd76tsv7dpsxb07nmwt2x1vwjs8fevtm", + "jd7dbyjy6f11jmb6sbqc8kxjw58fey0q", + "jd72w1rkw4w705e6jg542stw3h8ff4j6", + "jd7bas5aqtx1jtzhvwxrtc695x8ff9sf", + "jd72fkrtzdm7swsfya99ac9rks8fepfb", + "jd71xqrhn54gnh9pv16d7tkqe18ffxc5", + "jd7f3ndwcwswkhpfgqe91ycrv98ffzma", + "jd76x07z0mfvq6snde2gzp0mvn8ff34a", + "jd7ev7ws81hhha2q6pmkqjf4gs8femyx", + "jd7bp82pd7389jznbbx10gjx7n8fe35e", + "jd76977bmxkreg7kmgw1xsr1ks8fepsx", + "jd731n6dz1sv2p1hphrvr0a6xx8ffdc6", + "jd7crhzdrdr8mqs22jq4a18ypx8fehxd", + "jd7db3tf5355hr159rk7ee7xph8ffzpz" + ], + [ + "jd71zk49xz3x3etxh6972k6mmn8ff319", + "jd7f9qyep5j1ajqt8v6ym3jyv18fey3z", + "jd7apygx6w9fprgrcee3w632m18feztf", + "jd714d56znccas8a762ebcah9h8ff3bp", + "jd76meh22gcx64j5sdd9h8vs7x8feqcd", + "jd7dpyc2s2p7krzs1ryvktynah8ffqv0", + "jd795fh2d81mteh0p1ks8wg7t18ff8g1", + "jd7ezegq5em9q260k9gah2g2an8fe88m", + "jd77g3hdzs3np1ckjz0mxsrfz98ffaf3", + "jd7ebvcqgw02qtyj183jkwjsz98feztq" + ], + [ + "jd7d89cvg242zjfzdprdr5paf58fe38q", + "jd7870v1dn1bpqh5hbwagkntys8ff921", + "jd76n3n42vcdpa1bc5p3abst758ff13q", + "jd73z5tesrpwtnyz6nfbp9kyz98fedg6", + "jd73bwqqqevw857k30mrybrgz18ff2hs", + "jd7819d83kywwq6cmwe3tbeasn8ffj5p", + "jd724vsfha7keacwajdtt4q7n18feejg", + "jd786kay3733hch0ts1j3pxf318fe6wh", + "jd7ak6wv8aw4b50znc3tm305sn8fect6", + "jd725g5xq8y3n3n99pm6nz5b918fez7t" + ], + [] + ] + }, + { + "name": "contiguityAfterInsert", + "value": { + "actual": [ + "jd71thjdvwfwy66650bnas4kxx8ffzds", + "jd76tsv7dpsxb07nmwt2x1vwjs8fevtm", + "jd7dbyjy6f11jmb6sbqc8kxjw58fey0q", + "jd72w1rkw4w705e6jg542stw3h8ff4j6", + "jd7bas5aqtx1jtzhvwxrtc695x8ff9sf", + "jd72fkrtzdm7swsfya99ac9rks8fepfb", + "jd71xqrhn54gnh9pv16d7tkqe18ffxc5", + "jd7f3ndwcwswkhpfgqe91ycrv98ffzma", + "jd76x07z0mfvq6snde2gzp0mvn8ff34a", + "jd7ev7ws81hhha2q6pmkqjf4gs8femyx", + "jd7bp82pd7389jznbbx10gjx7n8fe35e", + "jd76977bmxkreg7kmgw1xsr1ks8fepsx", + "jd731n6dz1sv2p1hphrvr0a6xx8ffdc6", + "jd7crhzdrdr8mqs22jq4a18ypx8fehxd", + "jd7db3tf5355hr159rk7ee7xph8ffzpz", + "jd71zk49xz3x3etxh6972k6mmn8ff319", + "jd7f9qyep5j1ajqt8v6ym3jyv18fey3z", + "jd7apygx6w9fprgrcee3w632m18feztf", + "jd714d56znccas8a762ebcah9h8ff3bp", + "jd76meh22gcx64j5sdd9h8vs7x8feqcd", + "jd7dpyc2s2p7krzs1ryvktynah8ffqv0", + "jd795fh2d81mteh0p1ks8wg7t18ff8g1", + "jd7ezegq5em9q260k9gah2g2an8fe88m", + "jd77g3hdzs3np1ckjz0mxsrfz98ffaf3", + "jd7ebvcqgw02qtyj183jkwjsz98feztq", + "jd7d89cvg242zjfzdprdr5paf58fe38q", + "jd7870v1dn1bpqh5hbwagkntys8ff921", + "jd76n3n42vcdpa1bc5p3abst758ff13q", + "jd73z5tesrpwtnyz6nfbp9kyz98fedg6", + "jd73bwqqqevw857k30mrybrgz18ff2hs", + "jd7819d83kywwq6cmwe3tbeasn8ffj5p", + "jd724vsfha7keacwajdtt4q7n18feejg", + "jd786kay3733hch0ts1j3pxf318fe6wh", + "jd7ak6wv8aw4b50znc3tm305sn8fect6", + "jd725g5xq8y3n3n99pm6nz5b918fez7t" + ], + "expected": [ + "jd71thjdvwfwy66650bnas4kxx8ffzds", + "jd76tsv7dpsxb07nmwt2x1vwjs8fevtm", + "jd7dbyjy6f11jmb6sbqc8kxjw58fey0q", + "jd72w1rkw4w705e6jg542stw3h8ff4j6", + "jd7bas5aqtx1jtzhvwxrtc695x8ff9sf", + "jd72fkrtzdm7swsfya99ac9rks8fepfb", + "jd71xqrhn54gnh9pv16d7tkqe18ffxc5", + "jd7f3ndwcwswkhpfgqe91ycrv98ffzma", + "jd76x07z0mfvq6snde2gzp0mvn8ff34a", + "jd7ev7ws81hhha2q6pmkqjf4gs8femyx", + "jd7bp82pd7389jznbbx10gjx7n8fe35e", + "jd76977bmxkreg7kmgw1xsr1ks8fepsx", + "jd731n6dz1sv2p1hphrvr0a6xx8ffdc6", + "jd7crhzdrdr8mqs22jq4a18ypx8fehxd", + "jd7db3tf5355hr159rk7ee7xph8ffzpz", + "jd71zk49xz3x3etxh6972k6mmn8ff319", + "jd7f9qyep5j1ajqt8v6ym3jyv18fey3z", + "jd7apygx6w9fprgrcee3w632m18feztf", + "jd714d56znccas8a762ebcah9h8ff3bp", + "jd76meh22gcx64j5sdd9h8vs7x8feqcd", + "jd7dpyc2s2p7krzs1ryvktynah8ffqv0", + "jd795fh2d81mteh0p1ks8wg7t18ff8g1", + "jd7ezegq5em9q260k9gah2g2an8fe88m", + "jd77g3hdzs3np1ckjz0mxsrfz98ffaf3", + "jd7ebvcqgw02qtyj183jkwjsz98feztq", + "jd7d89cvg242zjfzdprdr5paf58fe38q", + "jd7870v1dn1bpqh5hbwagkntys8ff921", + "jd76n3n42vcdpa1bc5p3abst758ff13q", + "jd73z5tesrpwtnyz6nfbp9kyz98fedg6", + "jd73bwqqqevw857k30mrybrgz18ff2hs", + "jd7819d83kywwq6cmwe3tbeasn8ffj5p", + "jd724vsfha7keacwajdtt4q7n18feejg", + "jd786kay3733hch0ts1j3pxf318fe6wh", + "jd7ak6wv8aw4b50znc3tm305sn8fect6", + "jd725g5xq8y3n3n99pm6nz5b918fez7t" + ] + } + }, + { + "name": "the pages after the deletion", + "value": { + "rows": 15, + "status": "SplitRecommended", + "ids": [ + "jd71thjdvwfwy66650bnas4kxx8ffzds", + "jd76tsv7dpsxb07nmwt2x1vwjs8fevtm", + "jd7dbyjy6f11jmb6sbqc8kxjw58fey0q", + "jd72w1rkw4w705e6jg542stw3h8ff4j6", + "jd7bas5aqtx1jtzhvwxrtc695x8ff9sf", + "jd72fkrtzdm7swsfya99ac9rks8fepfb", + "jd71xqrhn54gnh9pv16d7tkqe18ffxc5", + "jd7f3ndwcwswkhpfgqe91ycrv98ffzma", + "jd76x07z0mfvq6snde2gzp0mvn8ff34a", + "jd7ev7ws81hhha2q6pmkqjf4gs8femyx", + "jd7bp82pd7389jznbbx10gjx7n8fe35e", + "jd76977bmxkreg7kmgw1xsr1ks8fepsx", + "jd731n6dz1sv2p1hphrvr0a6xx8ffdc6", + "jd7crhzdrdr8mqs22jq4a18ypx8fehxd", + "jd7db3tf5355hr159rk7ee7xph8ffzpz" + ] + } + }, + { + "name": "the pages after the deletion", + "value": { + "rows": 10, + "status": null, + "ids": [ + "jd7d89cvg242zjfzdprdr5paf58fe38q", + "jd7870v1dn1bpqh5hbwagkntys8ff921", + "jd76n3n42vcdpa1bc5p3abst758ff13q", + "jd73z5tesrpwtnyz6nfbp9kyz98fedg6", + "jd73bwqqqevw857k30mrybrgz18ff2hs", + "jd7819d83kywwq6cmwe3tbeasn8ffj5p", + "jd724vsfha7keacwajdtt4q7n18feejg", + "jd786kay3733hch0ts1j3pxf318fe6wh", + "jd7ak6wv8aw4b50znc3tm305sn8fect6", + "jd725g5xq8y3n3n99pm6nz5b918fez7t" + ] + } + }, + { + "name": "the pages after the deletion", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "the pages after the deletion", + "value": { + "rows": 9, + "status": null, + "ids": [ + "jd71zk49xz3x3etxh6972k6mmn8ff319", + "jd7apygx6w9fprgrcee3w632m18feztf", + "jd714d56znccas8a762ebcah9h8ff3bp", + "jd76meh22gcx64j5sdd9h8vs7x8feqcd", + "jd7dpyc2s2p7krzs1ryvktynah8ffqv0", + "jd795fh2d81mteh0p1ks8wg7t18ff8g1", + "jd7ezegq5em9q260k9gah2g2an8fe88m", + "jd77g3hdzs3np1ckjz0mxsrfz98ffaf3", + "jd7ebvcqgw02qtyj183jkwjsz98feztq" + ] + } + }, + { + "name": "contiguityAfterDelete", + "value": { + "removed": "jd7f9qyep5j1ajqt8v6ym3jyv18fey3z", + "actual": [ + "jd71thjdvwfwy66650bnas4kxx8ffzds", + "jd76tsv7dpsxb07nmwt2x1vwjs8fevtm", + "jd7dbyjy6f11jmb6sbqc8kxjw58fey0q", + "jd72w1rkw4w705e6jg542stw3h8ff4j6", + "jd7bas5aqtx1jtzhvwxrtc695x8ff9sf", + "jd72fkrtzdm7swsfya99ac9rks8fepfb", + "jd71xqrhn54gnh9pv16d7tkqe18ffxc5", + "jd7f3ndwcwswkhpfgqe91ycrv98ffzma", + "jd76x07z0mfvq6snde2gzp0mvn8ff34a", + "jd7ev7ws81hhha2q6pmkqjf4gs8femyx", + "jd7bp82pd7389jznbbx10gjx7n8fe35e", + "jd76977bmxkreg7kmgw1xsr1ks8fepsx", + "jd731n6dz1sv2p1hphrvr0a6xx8ffdc6", + "jd7crhzdrdr8mqs22jq4a18ypx8fehxd", + "jd7db3tf5355hr159rk7ee7xph8ffzpz", + "jd71zk49xz3x3etxh6972k6mmn8ff319", + "jd7apygx6w9fprgrcee3w632m18feztf", + "jd714d56znccas8a762ebcah9h8ff3bp", + "jd76meh22gcx64j5sdd9h8vs7x8feqcd", + "jd7dpyc2s2p7krzs1ryvktynah8ffqv0", + "jd795fh2d81mteh0p1ks8wg7t18ff8g1", + "jd7ezegq5em9q260k9gah2g2an8fe88m", + "jd77g3hdzs3np1ckjz0mxsrfz98ffaf3", + "jd7ebvcqgw02qtyj183jkwjsz98feztq", + "jd7d89cvg242zjfzdprdr5paf58fe38q", + "jd7870v1dn1bpqh5hbwagkntys8ff921", + "jd76n3n42vcdpa1bc5p3abst758ff13q", + "jd73z5tesrpwtnyz6nfbp9kyz98fedg6", + "jd73bwqqqevw857k30mrybrgz18ff2hs", + "jd7819d83kywwq6cmwe3tbeasn8ffj5p", + "jd724vsfha7keacwajdtt4q7n18feejg", + "jd786kay3733hch0ts1j3pxf318fe6wh", + "jd7ak6wv8aw4b50znc3tm305sn8fect6", + "jd725g5xq8y3n3n99pm6nz5b918fez7t" + ], + "expected": [ + "jd71thjdvwfwy66650bnas4kxx8ffzds", + "jd76tsv7dpsxb07nmwt2x1vwjs8fevtm", + "jd7dbyjy6f11jmb6sbqc8kxjw58fey0q", + "jd72w1rkw4w705e6jg542stw3h8ff4j6", + "jd7bas5aqtx1jtzhvwxrtc695x8ff9sf", + "jd72fkrtzdm7swsfya99ac9rks8fepfb", + "jd71xqrhn54gnh9pv16d7tkqe18ffxc5", + "jd7f3ndwcwswkhpfgqe91ycrv98ffzma", + "jd76x07z0mfvq6snde2gzp0mvn8ff34a", + "jd7ev7ws81hhha2q6pmkqjf4gs8femyx", + "jd7bp82pd7389jznbbx10gjx7n8fe35e", + "jd76977bmxkreg7kmgw1xsr1ks8fepsx", + "jd731n6dz1sv2p1hphrvr0a6xx8ffdc6", + "jd7crhzdrdr8mqs22jq4a18ypx8fehxd", + "jd7db3tf5355hr159rk7ee7xph8ffzpz", + "jd71zk49xz3x3etxh6972k6mmn8ff319", + "jd7apygx6w9fprgrcee3w632m18feztf", + "jd714d56znccas8a762ebcah9h8ff3bp", + "jd76meh22gcx64j5sdd9h8vs7x8feqcd", + "jd7dpyc2s2p7krzs1ryvktynah8ffqv0", + "jd795fh2d81mteh0p1ks8wg7t18ff8g1", + "jd7ezegq5em9q260k9gah2g2an8fe88m", + "jd77g3hdzs3np1ckjz0mxsrfz98ffaf3", + "jd7ebvcqgw02qtyj183jkwjsz98feztq", + "jd7d89cvg242zjfzdprdr5paf58fe38q", + "jd7870v1dn1bpqh5hbwagkntys8ff921", + "jd76n3n42vcdpa1bc5p3abst758ff13q", + "jd73z5tesrpwtnyz6nfbp9kyz98fedg6", + "jd73bwqqqevw857k30mrybrgz18ff2hs", + "jd7819d83kywwq6cmwe3tbeasn8ffj5p", + "jd724vsfha7keacwajdtt4q7n18feejg", + "jd786kay3733hch0ts1j3pxf318fe6wh", + "jd7ak6wv8aw4b50znc3tm305sn8fect6", + "jd725g5xq8y3n3n99pm6nz5b918fez7t" + ] + } + } + ] + }, + { + "name": "Probe 5: a page that outgrows its read cap comes back through the parent as a split (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.test.ts", + "result": "passed", + "durationMs": 892.4807079999998, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "the initial pinned page", + "value": { + "rows": 10, + "status": null, + "ids": [ + "jd70wretkydckfygtnqpxqsbyh8ffckj", + "jd7bgq7drc4xjzz03ekpj8py2x8fevxq", + "jd79jckq534pjdmmqjjaa5j3498fe3kw", + "jd70xsv5fa1p2sqnwy8a519afd8fezpa", + "jd7a3xey57022d95s775e4acbs8ffa8w", + "jd7678wy6p5k4gethrgbz8q0td8ff6zr", + "jd78evxc2j4snrzjc2ajs5wwr18fftn0", + "jd7fx2mt8ae3ja76d43xss2kn98ff74z", + "jd7dn2wxq2x45sh99v75t27yex8ffrwe", + "jd71msmk2hm4n3092mhjt2vz918ffkag" + ] + } + }, + { + "name": "a page with split status", + "value": { + "rows": 20, + "status": "SplitRequired", + "ids": [ + "jd70wretkydckfygtnqpxqsbyh8ffckj", + "jd7em53da4yfdy0atxg39vgbkn8fe6cz", + "jd716ttx59rja0hpqq99nsyxp18ff67e", + "jd727fcvr3fzj66p0hp97rm6818ffn7x", + "jd771v6sc1veca9t8fgw3v86gd8ffny2", + "jd71xr2cx9jwt4n8sq5nykkv998fempb", + "jd763tqw8xsq5y3713fm42fkrn8ffs0a", + "jd75y1gg0wv3g6arcgm7rjmm8x8ffc2s", + "jd7f58wvg9k4r4y81vvm6wad5n8ff6k1", + "jd7ce0bpn8xg8t1906trk8bv6s8few85", + "jd707ccak14vt7bxc5szsz9w9x8ffxq3", + "jd711gsrxkjkg7r85e30jwqjg18fe0fg", + "jd75pynky493q4dvse77c4d0798ffx0q", + "jd76z7r8zekr8jafb86m97zans8ffqhw", + "jd77rn3z2grmq27vdy1f1c911h8fej98", + "jd7at2vhps5k9gk57s4jr1vx098ffsqh", + "jd7bgq7drc4xjzz03ekpj8py2x8fevxq", + "jd79jckq534pjdmmqjjaa5j3498fe3kw", + "jd70xsv5fa1p2sqnwy8a519afd8fezpa", + "jd7a3xey57022d95s775e4acbs8ffa8w" + ] + } + }, + { + "name": "relayedSplit", + "value": { + "status": "SplitRequired", + "splitCursor": "[0.5625,1790872179911.534,\"jd7ce0bpn8xg8t1906trk8bv6s8few85\"]", + "continueCursor": "[4,1790872179859.042,\"jd7a3xey57022d95s775e4acbs8ffa8w\"]", + "endCursor": "[9,1790872179859.0432,\"jd71msmk2hm4n3092mhjt2vz918ffkag\"]", + "rows": 20 + } + }, + { + "name": "splitHalves", + "value": { + "left": [ + "jd70wretkydckfygtnqpxqsbyh8ffckj", + "jd7em53da4yfdy0atxg39vgbkn8fe6cz", + "jd716ttx59rja0hpqq99nsyxp18ff67e", + "jd727fcvr3fzj66p0hp97rm6818ffn7x", + "jd771v6sc1veca9t8fgw3v86gd8ffny2", + "jd71xr2cx9jwt4n8sq5nykkv998fempb", + "jd763tqw8xsq5y3713fm42fkrn8ffs0a", + "jd75y1gg0wv3g6arcgm7rjmm8x8ffc2s", + "jd7f58wvg9k4r4y81vvm6wad5n8ff6k1", + "jd7ce0bpn8xg8t1906trk8bv6s8few85" + ], + "right": [ + "jd707ccak14vt7bxc5szsz9w9x8ffxq3", + "jd711gsrxkjkg7r85e30jwqjg18fe0fg", + "jd75pynky493q4dvse77c4d0798ffx0q", + "jd76z7r8zekr8jafb86m97zans8ffqhw", + "jd77rn3z2grmq27vdy1f1c911h8fej98", + "jd7at2vhps5k9gk57s4jr1vx098ffsqh", + "jd7bgq7drc4xjzz03ekpj8py2x8fevxq", + "jd79jckq534pjdmmqjjaa5j3498fe3kw", + "jd70xsv5fa1p2sqnwy8a519afd8fezpa", + "jd7a3xey57022d95s775e4acbs8ffa8w", + "jd7678wy6p5k4gethrgbz8q0td8ff6zr", + "jd78evxc2j4snrzjc2ajs5wwr18fftn0", + "jd7fx2mt8ae3ja76d43xss2kn98ff74z", + "jd7dn2wxq2x45sh99v75t27yex8ffrwe", + "jd71msmk2hm4n3092mhjt2vz918ffkag" + ], + "wholeRange": [ + "jd70wretkydckfygtnqpxqsbyh8ffckj", + "jd7em53da4yfdy0atxg39vgbkn8fe6cz", + "jd716ttx59rja0hpqq99nsyxp18ff67e", + "jd727fcvr3fzj66p0hp97rm6818ffn7x", + "jd771v6sc1veca9t8fgw3v86gd8ffny2", + "jd71xr2cx9jwt4n8sq5nykkv998fempb", + "jd763tqw8xsq5y3713fm42fkrn8ffs0a", + "jd75y1gg0wv3g6arcgm7rjmm8x8ffc2s", + "jd7f58wvg9k4r4y81vvm6wad5n8ff6k1", + "jd7ce0bpn8xg8t1906trk8bv6s8few85", + "jd707ccak14vt7bxc5szsz9w9x8ffxq3", + "jd711gsrxkjkg7r85e30jwqjg18fe0fg", + "jd75pynky493q4dvse77c4d0798ffx0q", + "jd76z7r8zekr8jafb86m97zans8ffqhw", + "jd77rn3z2grmq27vdy1f1c911h8fej98", + "jd7at2vhps5k9gk57s4jr1vx098ffsqh", + "jd7bgq7drc4xjzz03ekpj8py2x8fevxq", + "jd79jckq534pjdmmqjjaa5j3498fe3kw", + "jd70xsv5fa1p2sqnwy8a519afd8fezpa", + "jd7a3xey57022d95s775e4acbs8ffa8w", + "jd7678wy6p5k4gethrgbz8q0td8ff6zr", + "jd78evxc2j4snrzjc2ajs5wwr18fftn0", + "jd7fx2mt8ae3ja76d43xss2kn98ff74z", + "jd7dn2wxq2x45sh99v75t27yex8ffrwe", + "jd71msmk2hm4n3092mhjt2vz918ffkag" + ] + } + } + ] + }, + { + "name": "Probe 2: ConvexError data through a component boundary (spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary) > executes the bound example against a fresh world", + "file": "tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.test.ts", + "result": "passed", + "durationMs": 934.3645419999999, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "catchingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + } + } + }, + { + "name": "rowsAfterCatching", + "value": { + "child": 0, + "parent": 1 + } + }, + { + "name": "passingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + }, + "text": "ConvexError: [Request ID: 7e06a239bf7d0feb] Server Error\nUncaught ConvexError: Uncaught ConvexError: {\"array\":[\"one\",2,false,null],\"boolean\":true,\"code\":\"staleVersion\",\"int64\":\"9007199254740993n\",\"nested\":{\"count\":3,\"label\":\"nested\"},\"nil\":null,\"number\":17.25}\n at handler (../fixture/convex/annex/failures.ts:8:11)\n\n" + } + }, + { + "name": "ordinaryErrorAtParent", + "value": { + "property": null, + "message": "Error: Uncaught Error: plain failure\n at handler (../fixture/convex/annex/failures.ts:12:23)\n" + } + }, + { + "name": "ordinaryErrorAtClient", + "value": { + "property": null, + "message": "Error: [Request ID: c97518d6d37904b5] Server Error\nUncaught Error: Uncaught Error: plain failure\n at handler (../fixture/convex/annex/failures.ts:12:23)\n\n" + } + } + ] + }, + { + "name": "Probe 2: ConvexError data through a nested mutation (spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.test.ts", + "result": "passed", + "durationMs": 923.2637500000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "catchingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + } + } + }, + { + "name": "rowsAfterCatching", + "value": { + "child": 0, + "parent": 1 + } + }, + { + "name": "passingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + }, + "text": "ConvexError: [Request ID: 7683ffbddcc32d12] Server Error\nUncaught ConvexError: Uncaught ConvexError: {\"array\":[\"one\",2,false,null],\"boolean\":true,\"code\":\"staleVersion\",\"int64\":\"9007199254740993n\",\"nested\":{\"count\":3,\"label\":\"nested\"},\"nil\":null,\"number\":17.25}\n at handler (../fixture/convex/failures.ts:21:11)\n\n" + } + }, + { + "name": "ordinaryErrorAtParent", + "value": { + "property": null, + "message": "Error: Uncaught Error: plain failure\n at handler (../fixture/convex/failures.ts:25:23)\n" + } + }, + { + "name": "ordinaryErrorAtClient", + "value": { + "property": null, + "message": "Error: [Request ID: 7445ace0b933ccfa] Server Error\nUncaught Error: Uncaught Error: plain failure\n at handler (../fixture/convex/failures.ts:25:23)\n\n" + } + } + ] + }, + { + "name": "Probe 5: a subscription to a parent query follows the component's data (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.test.ts", + "result": "passed", + "durationMs": 950.781833, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "subscriptionUpdates", + "value": { + "waitMs": 5000, + "observationAfterDeliveryMs": 100, + "values": [ + { + "id": "jd7fwe7v2a54peqysrtbbndfdn8fe80g", + "label": "changed" + } + ], + "error": null + } + } + ] + }, + { + "name": "native: a taken --port causes fresh port pairs and the third start succeeds", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 2748.502667, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "f097e9b7b3004f7c9c85667e251bf2bcb03144cc0f85939c01583d98a4323352", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "An ordinary client cannot call a component's function (spec:platform.native-harness.ordinary-client-refused-component-function) > executes the bound example against a fresh world", + "file": "tests/native/ordinary-client-refused-component-function.test.ts", + "result": "passed", + "durationMs": 4429.579791, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 1: a client closed with a pending mutation (spec:facts.f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.test.ts", + "result": "passed", + "durationMs": 1666.901708, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "trials", + "value": [ + { + "trial": "closed 0", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 1", + "gapMs": 1, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 2", + "gapMs": 2, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 3", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 4", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 5", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 6", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 7", + "gapMs": 1, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 8", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 9", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 10", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 11", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 12", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 13", + "gapMs": 1, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 14", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 15", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 16", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 17", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 18", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 19", + "gapMs": 1, + "confirmed": false, + "rows": 0 + } + ] + }, + { + "name": "trialsThatLeftNoRow", + "value": 8 + }, + { + "name": "trialsThatLeftOneRow", + "value": 12 + } + ] + }, + { + "name": "native: a taken --site-proxy-port causes fresh port pairs and the third start succeeds", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 751.1032500000001, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "f60b845e3c2ed2acbd58ff9ff528e3f86f1a4b2556112893a01ddcf42652eafa", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "The admin key acting as an identity reaches an internal function (spec:platform.native-harness.admin-key-acting-as-identity-reaches-internal-function) > executes the bound example against a fresh world", + "file": "tests/native/admin-key-acting-as-identity-reaches-internal-function.test.ts", + "result": "passed", + "durationMs": 931.221708, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 1: the HTTP client and a caller's retry (spec:facts.f05-react-client-retries-until-confirmed.probe-1-http-client-retry) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.test.ts", + "result": "passed", + "durationMs": 953.8106250000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "requestsAfterEachCall", + "value": [ + 1, + 2 + ] + }, + { + "name": "rowsAfterTheRetry", + "value": 2 + } + ] + }, + { + "name": "An ordinary client cannot call an internal function (spec:platform.native-harness.ordinary-client-refused-internal-function) > executes the bound example against a fresh world", + "file": "tests/native/ordinary-client-refused-internal-function.test.ts", + "result": "passed", + "durationMs": 916.171458, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "A token signed by the fixture issuer yields the caller's identity (spec:platform.native-harness.fixture-issuer-token-yields-identity) > executes the bound example against a fresh world", + "file": "tests/native/fixture-issuer-token-yields-identity.test.ts", + "result": "passed", + "durationMs": 900.284792, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: a component query that calls ctx.auth.getUserIdentity() completes, and its answer is recorded", + "file": "tests/native/component-auth.test.ts", + "result": "passed", + "durationMs": 797.608125, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "identitySeenInsideComponent", + "value": { + "answered": "an identity", + "issuer": "https://fixture-issuer.test", + "subject": "user-1" + } + } + ] + }, + { + "name": "native: three taken ports fail clearly and remove the failed start's storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 3686.9814579999993, + "errors": [], + "backends": [], + "measurements": [] + }, + { + "name": "Probe 1: a backend restart with a mutation in flight (spec:facts.f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.test.ts", + "result": "passed", + "durationMs": 10908.941083, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "trials", + "value": [ + { + "trial": "restart 0", + "delayMs": 0, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 1", + "delayMs": 0, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 2", + "delayMs": 0, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 3", + "delayMs": 1, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 4", + "delayMs": 2, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 5", + "delayMs": 4, + "committedBeforeKill": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 6", + "delayMs": 8, + "committedBeforeKill": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 7", + "delayMs": 16, + "committedBeforeKill": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 8", + "delayMs": 32, + "committedBeforeKill": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 9", + "delayMs": 64, + "committedBeforeKill": true, + "resolved": true, + "rows": 1 + } + ] + }, + { + "name": "trialsKilledBeforeCommit", + "value": 5 + }, + { + "name": "trialsKilledAfterCommit", + "value": 5 + } + ] + }, + { + "name": "native: stop escalates to SIGKILL when the owned process ignores SIGINT and keeps its storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 5358.005041, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "a5228305041678f250852956a81d9eeb38a047b98f134be65ae6cc55eb3130a1", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 4: a nested call draws on the caller's transaction limits (spec:facts.f13-transactions-have-limits.probe-4-nested-call-shares-limits) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-nested-call-shares-limits.test.ts", + "result": "passed", + "durationMs": 21579.266750000003, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "seededPayloads", + "value": { + "size": 921600, + "parentCount": 10, + "childCount": 10 + } + }, + { + "name": "functionOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: f7e4d72f01165636] Server Error\nUncaught Error: Uncaught Error: Too many bytes read in a single function execution (limit: 16777216 bytes). Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\n at async handler (../fixture/convex/limits.ts:48:11)\n\n at async handler (../fixture/convex/limits.ts:79:34)\n" + } + }, + { + "name": "readTogether", + "value": "fails on the bytes-read limit" + }, + { + "name": "functionOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "functionOutcome", + "value": { + "outcome": "returns", + "error": null + } + }, + { + "name": "readsAlone", + "value": { + "parent": "commits", + "child": "returns", + "parentDocuments": 10, + "childDocuments": 10, + "childPayloadBytes": 9216000 + } + }, + { + "name": "sharedReadMetrics", + "value": { + "byteGrowth": 921684, + "documentGrowth": 1, + "childBytes": 921684, + "childDocuments": 1, + "payloadBytes": 921600 + } + }, + { + "name": "functionOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 7fa290356516e6ba] Server Error\nUncaught Error: Uncaught Error: Too many bytes written in a single function execution (limit: 16 MiB)\n at async handler (../fixture/convex/limits.ts:24:8)\n\n at async handler (../fixture/convex/limits.ts:104:8)\n" + } + }, + { + "name": "writesTogether", + "value": "fails on the bytes-written limit" + }, + { + "name": "failedWriteRows", + "value": { + "count": 0, + "actualRows": 0 + } + }, + { + "name": "functionOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "writeHalf", + "value": { + "half": "parent", + "outcome": "commits", + "rows": 10, + "actualRows": 10 + } + }, + { + "name": "functionOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "writeHalf", + "value": { + "half": "child", + "outcome": "commits", + "rows": 20, + "actualRows": 20 + } + } + ] + }, + { + "name": "Probe 4: a component call draws on the caller's transaction limits (spec:facts.f13-transactions-have-limits.probe-4-component-call-shares-limits) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-component-call-shares-limits.test.ts", + "result": "passed", + "durationMs": 21747.446624999997, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "seededPayloads", + "value": { + "size": 921600, + "parentCount": 10, + "childCount": 10 + } + }, + { + "name": "functionOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: d838c7cbe6eaa630] Server Error\nUncaught Error: Uncaught Error: Too many bytes read in a single function execution (limit: 16777216 bytes). Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\n at async handler (../fixture/convex/annex/limits.ts:35:11)\n\n at async handler (../fixture/convex/limits.ts:79:34)\n" + } + }, + { + "name": "readTogether", + "value": "fails on the bytes-read limit" + }, + { + "name": "functionOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "functionOutcome", + "value": { + "outcome": "returns", + "error": null + } + }, + { + "name": "readsAlone", + "value": { + "parent": "commits", + "child": "returns", + "parentDocuments": 10, + "childDocuments": 10, + "childPayloadBytes": 9216000 + } + }, + { + "name": "sharedReadMetrics", + "value": { + "byteGrowth": 921684, + "documentGrowth": 1, + "childBytes": 921684, + "childDocuments": 1, + "payloadBytes": 921600 + } + }, + { + "name": "functionOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: ffa812624100a78e] Server Error\nUncaught Error: Uncaught Error: Too many bytes written in a single function execution (limit: 16 MiB)\n at async handler (../fixture/convex/annex/limits.ts:9:8)\n\n at async handler (../fixture/convex/limits.ts:104:8)\n" + } + }, + { + "name": "writesTogether", + "value": "fails on the bytes-written limit" + }, + { + "name": "failedWriteRows", + "value": { + "count": 0, + "actualRows": 0 + } + }, + { + "name": "functionOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "writeHalf", + "value": { + "half": "parent", + "outcome": "commits", + "rows": 10, + "actualRows": 10 + } + }, + { + "name": "functionOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "writeHalf", + "value": { + "half": "child", + "outcome": "commits", + "rows": 20, + "actualRows": 20 + } + } + ] + } + ], + "unhandledErrors": [] +} diff --git a/evidence/native-20261001T175157Z-5f63f97-cda79359-9825-4d43-af3d-4c271931c2a7.json b/evidence/native-20261001T175157Z-5f63f97-cda79359-9825-4d43-af3d-4c271931c2a7.json new file mode 100644 index 0000000..a598e3d --- /dev/null +++ b/evidence/native-20261001T175157Z-5f63f97-cda79359-9825-4d43-af3d-4c271931c2a7.json @@ -0,0 +1,3462 @@ +{ + "tier": "native", + "commit": "5f63f9767e6d5574284e5f3381461420384ac26a", + "clean": true, + "command": "vitest run --project native", + "startedAt": "2026-10-01T17:51:57.805Z", + "finishedAt": "2026-10-01T17:52:45.360Z", + "result": "passed", + "versions": { + "node": "v24.21.0", + "convex": "1.46.0", + "convex-helpers": "0.1.124", + "convex-test": "0.0.60", + "vitest": "5.0.3", + "typescript": "7.0.2", + "react": "19.3.0", + "happy-dom": "20.14.5" + }, + "tests": [ + { + "name": "Probe 2: ConvexError data through a nested mutation (spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.test.ts", + "result": "passed", + "durationMs": 1408.98625, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "catchingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + } + } + }, + { + "name": "rowsAfterCatching", + "value": { + "child": 0, + "parent": 1 + } + }, + { + "name": "passingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + }, + "text": "ConvexError: [Request ID: e6e87952c623c1b1] Server Error\nUncaught ConvexError: Uncaught ConvexError: {\"array\":[\"one\",2,false,null],\"boolean\":true,\"code\":\"staleVersion\",\"int64\":\"9007199254740993n\",\"nested\":{\"count\":3,\"label\":\"nested\"},\"nil\":null,\"number\":17.25}\n at handler (../fixture/convex/failures.ts:21:11)\n\n" + } + }, + { + "name": "ordinaryErrorAtParent", + "value": { + "property": null, + "message": "Error: Uncaught Error: plain failure: thrower-nested-9bcdc170-07b2-4da1-83b6-6d8cd45427f8\n at handler (../fixture/convex/failures.ts:39:23)\n" + } + }, + { + "name": "ordinaryErrorAtClient", + "value": { + "property": null, + "message": "Error: [Request ID: 676920ffab2f6c8f] Server Error\nUncaught Error: Uncaught Error: plain failure: thrower-nested-9bcdc170-07b2-4da1-83b6-6d8cd45427f8\n at handler (../fixture/convex/failures.ts:39:23)\n\n" + } + }, + { + "name": "throwBoundarySummary", + "value": { + "boundary": "nested", + "caughtDataUnchanged": true, + "passedDataUnchanged": true, + "childRows": 0, + "parentRows": 1, + "plainErrorFromThrower": true, + "writeGuardReached": true + } + } + ] + }, + { + "name": "Probe 2: ConvexError data through a component boundary (spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary) > executes the bound example against a fresh world", + "file": "tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.test.ts", + "result": "passed", + "durationMs": 1457.6299589999999, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "catchingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + } + } + }, + { + "name": "rowsAfterCatching", + "value": { + "child": 0, + "parent": 1 + } + }, + { + "name": "passingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + }, + "text": "ConvexError: [Request ID: 2d069dfc6cfb0f2d] Server Error\nUncaught ConvexError: Uncaught ConvexError: {\"array\":[\"one\",2,false,null],\"boolean\":true,\"code\":\"staleVersion\",\"int64\":\"9007199254740993n\",\"nested\":{\"count\":3,\"label\":\"nested\"},\"nil\":null,\"number\":17.25}\n at handler (../fixture/convex/annex/failures.ts:8:11)\n\n" + } + }, + { + "name": "ordinaryErrorAtParent", + "value": { + "property": null, + "message": "Error: Uncaught Error: plain failure: thrower-annex-b33dad71-4c4d-41b2-a5f8-537bb7806423\n at handler (../fixture/convex/annex/failures.ts:26:23)\n" + } + }, + { + "name": "ordinaryErrorAtClient", + "value": { + "property": null, + "message": "Error: [Request ID: ada801e31a7a4881] Server Error\nUncaught Error: Uncaught Error: plain failure: thrower-annex-b33dad71-4c4d-41b2-a5f8-537bb7806423\n at handler (../fixture/convex/annex/failures.ts:26:23)\n\n" + } + }, + { + "name": "throwBoundarySummary", + "value": { + "boundary": "annex", + "caughtDataUnchanged": true, + "passedDataUnchanged": true, + "childRows": 0, + "parentRows": 1, + "plainErrorFromThrower": true, + "writeGuardReached": true + } + } + ] + }, + { + "name": "Probe 4: how deep nested calls may go (spec:facts.f13-transactions-have-limits.probe-4-nesting-depth) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-nesting-depth.test.ts", + "result": "passed", + "durationMs": 1576.496292, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "stack1Trial", + "value": { + "stack": 1, + "answer": 1, + "error": null, + "rows": 1 + } + }, + { + "name": "stack2Trial", + "value": { + "stack": 2, + "answer": 2, + "error": null, + "rows": 2 + } + }, + { + "name": "stack3Trial", + "value": { + "stack": 3, + "answer": 3, + "error": null, + "rows": 3 + } + }, + { + "name": "stack4Trial", + "value": { + "stack": 4, + "answer": 4, + "error": null, + "rows": 4 + } + }, + { + "name": "stack5Trial", + "value": { + "stack": 5, + "answer": 5, + "error": null, + "rows": 5 + } + }, + { + "name": "stack6Trial", + "value": { + "stack": 6, + "answer": 6, + "error": null, + "rows": 6 + } + }, + { + "name": "stack7Trial", + "value": { + "stack": 7, + "answer": 7, + "error": null, + "rows": 7 + } + }, + { + "name": "stack8Trial", + "value": { + "stack": 8, + "answer": 8, + "error": null, + "rows": 8 + } + }, + { + "name": "stack9Trial", + "value": { + "stack": 9, + "answer": 9, + "error": null, + "rows": 9 + } + }, + { + "name": "stack10Trial", + "value": { + "stack": 10, + "answer": null, + "error": "Error: [Request ID: 0b330ebec2185017] Server Error\nUncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Cross component call depth limit exceeded. Do you have an infinite loop in your app?\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n", + "rows": 0 + } + }, + { + "name": "depthBoundary", + "value": { + "deepest": 9, + "firstFailure": 10, + "refusal": "Error: [Request ID: 0b330ebec2185017] Server Error\nUncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Cross component call depth limit exceeded. Do you have an infinite loop in your app?\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n" + } + } + ] + }, + { + "name": "native: stop and kill keep stored rows across restart, and dispose removes storage twice", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 1845.457333, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://changed-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 3: the cost of a component call from a mutation (spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.test.ts", + "result": "passed", + "durationMs": 2986.443916, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "costSamples", + "value": [ + { + "round": 0, + "path": "helper", + "cacheBuster": 1000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.00313075, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.00313075, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "nested", + "cacheBuster": 2000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.079000584, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.079000584, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "component", + "cacheBuster": 3000, + "result": { + "id": "jn7c3am9r0q9gr97rba6kygfkd8ffkwx", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.118236791, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.118236791, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "helper", + "cacheBuster": 4000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002242583, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002242583, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "nested", + "cacheBuster": 5000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.061711917, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.061711917, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "component", + "cacheBuster": 6000, + "result": { + "id": "jn7c3am9r0q9gr97rba6kygfkd8ffkwx", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.104061125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.104061125, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "helper", + "cacheBuster": 7000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002452792, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002452792, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "nested", + "cacheBuster": 8000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.058311791, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.058311791, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "component", + "cacheBuster": 9000, + "result": { + "id": "jn7c3am9r0q9gr97rba6kygfkd8ffkwx", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.09055625, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.09055625, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "helper", + "cacheBuster": 10000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.001995208, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.001995208, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "nested", + "cacheBuster": 11000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.047283208, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.047283208, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "component", + "cacheBuster": 12000, + "result": { + "id": "jn7c3am9r0q9gr97rba6kygfkd8ffkwx", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.078584375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.078584375, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "helper", + "cacheBuster": 13000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002190459, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002190459, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "nested", + "cacheBuster": 14000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.06189425, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.06189425, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "component", + "cacheBuster": 15000, + "result": { + "id": "jn7c3am9r0q9gr97rba6kygfkd8ffkwx", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.163176458, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.163176458, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "helper", + "cacheBuster": 16000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.00239025, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.00239025, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "nested", + "cacheBuster": 17000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.09270825, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.09270825, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "component", + "cacheBuster": 18000, + "result": { + "id": "jn7c3am9r0q9gr97rba6kygfkd8ffkwx", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.122233667, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.122233667, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "helper", + "cacheBuster": 19000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002496375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002496375, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "nested", + "cacheBuster": 20000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.066752291, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.066752291, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "component", + "cacheBuster": 21000, + "result": { + "id": "jn7c3am9r0q9gr97rba6kygfkd8ffkwx", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.0956695, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.0956695, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "helper", + "cacheBuster": 22000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.001975958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.001975958, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "nested", + "cacheBuster": 23000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.050992708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.050992708, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "component", + "cacheBuster": 24000, + "result": { + "id": "jn7c3am9r0q9gr97rba6kygfkd8ffkwx", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.076372916, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.076372916, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "helper", + "cacheBuster": 25000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002481416, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002481416, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "nested", + "cacheBuster": 26000, + "result": { + "id": "jx77hgecta0p5eg1rctpjfwx8d8fe88c", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.048779959, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.048779959, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "component", + "cacheBuster": 27000, + "result": { + "id": "jn7c3am9r0q9gr97rba6kygfkd8ffkwx", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.073903917, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.073903917, + "cachedResult": false + } + ] + } + ] + }, + { + "name": "readCostSummary", + "value": { + "kind": "mutation", + "readsPerSample": 50, + "acceptedSamples": { + "helper": 9, + "nested": 9, + "component": 9 + }, + "componentCompletionRecords": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "helper": 0.00239025, + "nested": 0.061711917, + "component": 0.0956695, + "componentMinusHelperSeconds": 0.09327925000000001, + "componentMinusHelperPerReadSeconds": 0.0018655850000000001 + } + } + ] + }, + { + "name": "Probe 3: the cost of a component call from a query (spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query) > executes the bound example against a fresh world", + "file": "tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.test.ts", + "result": "passed", + "durationMs": 3008.862709, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "costSamples", + "value": [ + { + "round": 0, + "path": "helper", + "cacheBuster": 1000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.003274292, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.003274292, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "nested", + "cacheBuster": 2000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.082732375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.082732375, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "component", + "cacheBuster": 3000, + "result": { + "id": "jn7csew6778pht5zd5ttfyps5h8ffn37", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.110256666, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.110256666, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "helper", + "cacheBuster": 4000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002505625, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002505625, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "nested", + "cacheBuster": 5000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.057720625, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.057720625, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "component", + "cacheBuster": 6000, + "result": { + "id": "jn7csew6778pht5zd5ttfyps5h8ffn37", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.10199125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.10199125, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "helper", + "cacheBuster": 7000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002689875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002689875, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "nested", + "cacheBuster": 8000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.058103958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.058103958, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "component", + "cacheBuster": 9000, + "result": { + "id": "jn7csew6778pht5zd5ttfyps5h8ffn37", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.087511875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.087511875, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "helper", + "cacheBuster": 10000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.001993875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.001993875, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "nested", + "cacheBuster": 11000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.052683625, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.052683625, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "component", + "cacheBuster": 12000, + "result": { + "id": "jn7csew6778pht5zd5ttfyps5h8ffn37", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.07883725, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.07883725, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "helper", + "cacheBuster": 13000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002293125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002293125, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "nested", + "cacheBuster": 14000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.063160625, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.063160625, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "component", + "cacheBuster": 15000, + "result": { + "id": "jn7csew6778pht5zd5ttfyps5h8ffn37", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.176115583, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.176115583, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "helper", + "cacheBuster": 16000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002280875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002280875, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "nested", + "cacheBuster": 17000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.091811292, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.091811292, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "component", + "cacheBuster": 18000, + "result": { + "id": "jn7csew6778pht5zd5ttfyps5h8ffn37", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.112191959, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.112191959, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "helper", + "cacheBuster": 19000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002043958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002043958, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "nested", + "cacheBuster": 20000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.076300375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.076300375, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "component", + "cacheBuster": 21000, + "result": { + "id": "jn7csew6778pht5zd5ttfyps5h8ffn37", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.077640583, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.077640583, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "helper", + "cacheBuster": 22000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.002099958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002099958, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "nested", + "cacheBuster": 23000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.04717475, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.04717475, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "component", + "cacheBuster": 24000, + "result": { + "id": "jn7csew6778pht5zd5ttfyps5h8ffn37", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.07836875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.07836875, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "helper", + "cacheBuster": 25000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.001938625, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.001938625, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "nested", + "cacheBuster": 26000, + "result": { + "id": "jx7e769j7cpc7prm95pz943nwd8ff1a0", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.049151583, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.049151583, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "component", + "cacheBuster": 27000, + "result": { + "id": "jn7csew6778pht5zd5ttfyps5h8ffn37", + "reads": 50, + "valueSum": 350 + }, + "callError": null, + "executionTimeSeconds": 0.074448125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.074448125, + "cachedResult": false + } + ] + } + ] + }, + { + "name": "readCostSummary", + "value": { + "kind": "query", + "readsPerSample": 50, + "acceptedSamples": { + "helper": 9, + "nested": 9, + "component": 9 + }, + "componentCompletionRecords": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "helper": 0.002280875, + "nested": 0.058103958, + "component": 0.087511875, + "componentMinusHelperSeconds": 0.085231, + "componentMinusHelperPerReadSeconds": 0.00170462 + } + } + ] + }, + { + "name": "Probe 5: the client hook splits a page across the boundary and loses rows (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.test.ts", + "result": "passed", + "durationMs": 1129.550417, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "hookBeforeInsert", + "value": { + "rows": 30, + "status": "Exhausted" + } + }, + { + "name": "hookCappedPage", + "value": { + "status": "SplitRequired", + "rows": 20, + "continueCursor": "[4,1790877121145.046,\"j970r14x3zcb0bqhhmaf4crssx8fej4k\"]", + "endCursor": "[9,1790877121145.047,\"j973w46rtc2md055bz08w5wbbd8feen7\"]" + } + }, + { + "name": "hookSplitSummary", + "value": { + "rowsInList": 45, + "rowsShown": 40, + "status": "Exhausted", + "missingPositions": [ + 5, + 6, + 7, + 8, + 9 + ] + } + }, + { + "name": "hookMissingCursorRange", + "value": { + "missing": [ + "j97f7ctj389xsnxedfxzv7bmxx8fe41h", + "j979g73tnpjg48370w0w5rxcgd8fe366", + "j9755w63g84ndnjh07mtv5gz1s8ff3ps", + "j97c1vs2my4ea9z67phttwcpts8ff0xe", + "j973w46rtc2md055bz08w5wbbd8feen7" + ], + "betweenCursors": [ + "j97f7ctj389xsnxedfxzv7bmxx8fe41h", + "j979g73tnpjg48370w0w5rxcgd8fe366", + "j9755w63g84ndnjh07mtv5gz1s8ff3ps", + "j97c1vs2my4ea9z67phttwcpts8ff0xe", + "j973w46rtc2md055bz08w5wbbd8feen7" + ] + } + } + ] + }, + { + "name": "Probe 1: a client closed with a pending mutation (spec:facts.f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.test.ts", + "result": "passed", + "durationMs": 1720.723375, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "trials", + "value": [ + { + "trial": "closed 0", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 1", + "gapMs": 1, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 2", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 3", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 4", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 5", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 6", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 7", + "gapMs": 1, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 8", + "gapMs": 2, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 9", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 10", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 11", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 12", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 13", + "gapMs": 1, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 14", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 15", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 16", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 17", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 18", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 19", + "gapMs": 1, + "confirmed": false, + "rows": 0 + } + ] + }, + { + "name": "closedClientSummary", + "value": { + "trials": 20, + "confirmed": 0, + "noRow": 8, + "oneRow": 12 + } + } + ] + }, + { + "name": "Probe 5: a subscription to a parent query follows the component's data (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.test.ts", + "result": "passed", + "durationMs": 906.5265829999998, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "subscriptionSummary", + "value": { + "waitMs": 5000, + "observationAfterDeliveryMs": 100, + "values": [ + { + "id": "j97b2pahx3p9kp9nd5ybe7qe8s8fftwp", + "label": "changed" + } + ], + "error": null + } + } + ] + }, + { + "name": "Probe 5: a page that outgrows its read cap comes back through the parent as a split (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.test.ts", + "result": "passed", + "durationMs": 881.638709, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "initialPinnedPage", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j975y2tz6h1d4f0wxn75v538tn8ffyd1", + "j97d8c4zhdxa65pmfacs5cdhw18fegs7", + "j974hrg02habn2b641qx2tkkz98ff6b4", + "j977gfrbe3q4zja2x1kzbmn5eh8ffm0p", + "j97aqdf5xps8f318eseah5nx4d8fedjq", + "j974gy518ss60v039jseh9xwkn8ffxbg", + "j97df2f4vfesnehtvphyf0sf7n8fe1s8", + "j97an48775s6z7xpmam7q3es7h8fefwb", + "j97cs54bvr9q6efbpnychex4fh8fevge", + "j97ex6esj8883rbn7rzxmx8jx58ffhvb" + ] + } + }, + { + "name": "splitPage", + "value": { + "rows": 20, + "status": "SplitRequired", + "ids": [ + "j975y2tz6h1d4f0wxn75v538tn8ffyd1", + "j971avdk2sn87az4rtf79edt1h8fehej", + "j979ttcpg4j1gq5chkfkgenc9n8feqmz", + "j977yqykqsjnnagc76kp0vfvcn8fe61s", + "j97ea0dtpa9m4t5pqxc40cpswn8ffcxv", + "j974626rdr3fbctjkbe41vb8eh8fedxq", + "j97cyaxrwdm9njgmwamxhh4r298fevam", + "j9789dhjqq0xgxn2h03005z84n8ffcyp", + "j9743m8fx3dsa5b8hn615c0jfn8fehse", + "j979e61xx4x0sh61j2pe84z2tn8ffcdb", + "j972yd7wd90jb9vem8ft99mkp58ffemm", + "j975e6246wyrfz35bcnhmtjv418ffezh", + "j97d7pz8dbpr1y5dwy76nq7fj18fewyf", + "j973h60km1tn4qf72b45d19nen8fe51p", + "j97abt1xfn1sk6c7w1qzkzgmvs8ff8pf", + "j976j908xr6sa6qff5q56c1hjh8ff16n", + "j97d8c4zhdxa65pmfacs5cdhw18fegs7", + "j974hrg02habn2b641qx2tkkz98ff6b4", + "j977gfrbe3q4zja2x1kzbmn5eh8ffm0p", + "j97aqdf5xps8f318eseah5nx4d8fedjq" + ] + } + }, + { + "name": "relayedSplit", + "value": { + "status": "SplitRequired", + "splitCursor": "[0.5625,1790877122616.305,\"j979e61xx4x0sh61j2pe84z2tn8ffcdb\"]", + "continueCursor": "[4,1790877122581.24,\"j97aqdf5xps8f318eseah5nx4d8fedjq\"]", + "endCursor": "[9,1790877122581.2412,\"j97ex6esj8883rbn7rzxmx8jx58ffhvb\"]", + "rows": 20 + } + }, + { + "name": "splitSummary", + "value": { + "pageStatus": "SplitRequired", + "left": [ + "j975y2tz6h1d4f0wxn75v538tn8ffyd1", + "j971avdk2sn87az4rtf79edt1h8fehej", + "j979ttcpg4j1gq5chkfkgenc9n8feqmz", + "j977yqykqsjnnagc76kp0vfvcn8fe61s", + "j97ea0dtpa9m4t5pqxc40cpswn8ffcxv", + "j974626rdr3fbctjkbe41vb8eh8fedxq", + "j97cyaxrwdm9njgmwamxhh4r298fevam", + "j9789dhjqq0xgxn2h03005z84n8ffcyp", + "j9743m8fx3dsa5b8hn615c0jfn8fehse", + "j979e61xx4x0sh61j2pe84z2tn8ffcdb" + ], + "right": [ + "j972yd7wd90jb9vem8ft99mkp58ffemm", + "j975e6246wyrfz35bcnhmtjv418ffezh", + "j97d7pz8dbpr1y5dwy76nq7fj18fewyf", + "j973h60km1tn4qf72b45d19nen8fe51p", + "j97abt1xfn1sk6c7w1qzkzgmvs8ff8pf", + "j976j908xr6sa6qff5q56c1hjh8ff16n", + "j97d8c4zhdxa65pmfacs5cdhw18fegs7", + "j974hrg02habn2b641qx2tkkz98ff6b4", + "j977gfrbe3q4zja2x1kzbmn5eh8ffm0p", + "j97aqdf5xps8f318eseah5nx4d8fedjq", + "j974gy518ss60v039jseh9xwkn8ffxbg", + "j97df2f4vfesnehtvphyf0sf7n8fe1s8", + "j97an48775s6z7xpmam7q3es7h8fefwb", + "j97cs54bvr9q6efbpnychex4fh8fevge", + "j97ex6esj8883rbn7rzxmx8jx58ffhvb" + ], + "wholeRange": [ + "j975y2tz6h1d4f0wxn75v538tn8ffyd1", + "j971avdk2sn87az4rtf79edt1h8fehej", + "j979ttcpg4j1gq5chkfkgenc9n8feqmz", + "j977yqykqsjnnagc76kp0vfvcn8fe61s", + "j97ea0dtpa9m4t5pqxc40cpswn8ffcxv", + "j974626rdr3fbctjkbe41vb8eh8fedxq", + "j97cyaxrwdm9njgmwamxhh4r298fevam", + "j9789dhjqq0xgxn2h03005z84n8ffcyp", + "j9743m8fx3dsa5b8hn615c0jfn8fehse", + "j979e61xx4x0sh61j2pe84z2tn8ffcdb", + "j972yd7wd90jb9vem8ft99mkp58ffemm", + "j975e6246wyrfz35bcnhmtjv418ffezh", + "j97d7pz8dbpr1y5dwy76nq7fj18fewyf", + "j973h60km1tn4qf72b45d19nen8fe51p", + "j97abt1xfn1sk6c7w1qzkzgmvs8ff8pf", + "j976j908xr6sa6qff5q56c1hjh8ff16n", + "j97d8c4zhdxa65pmfacs5cdhw18fegs7", + "j974hrg02habn2b641qx2tkkz98ff6b4", + "j977gfrbe3q4zja2x1kzbmn5eh8ffm0p", + "j97aqdf5xps8f318eseah5nx4d8fedjq", + "j974gy518ss60v039jseh9xwkn8ffxbg", + "j97df2f4vfesnehtvphyf0sf7n8fe1s8", + "j97an48775s6z7xpmam7q3es7h8fefwb", + "j97cs54bvr9q6efbpnychex4fh8fevge", + "j97ex6esj8883rbn7rzxmx8jx58ffhvb" + ] + } + } + ] + }, + { + "name": "Probe 5: pages of a component list stay contiguous under live writes (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.test.ts", + "result": "passed", + "durationMs": 941.795292, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "initialPinnedPage0", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j973jstgxkh7sz5gt6hehscx2s8fe047", + "j97252kngyse1ph0ejgcsttew18fen2k", + "j979ay0352ce0xc70f848nh7458ff25h", + "j97b1d803j9m2znr4qd8w97r198fesa7", + "j977q3nfsg1btgstrav26gcje18ffhch", + "j976t5ze3r4gtbcb80ed9d930d8ff41p", + "j9727snedd33rg47pcn210ksm98feb4k", + "j97enmd26hwrpcxqvta8x92f058ff6hx", + "j979v25nep3yges2st7wg8n5ns8ffq65", + "j9796jqr722hf1x380avcxg3j18fe3gj" + ] + } + }, + { + "name": "initialPinnedPage1", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j971h1hx96zefha95eqqb489ad8feejq", + "j973ra0akpfm4bh06465np9mpd8ffe7t", + "j9776zqksyvw46ha7933x8zk0h8ffvr5", + "j970rq2qta78mwk6ftdt60kd4n8ffy35", + "j977qebymykf5pdsvpsygsxp6x8fexpn", + "j97215ck8vvjan5vzhgm42cvw18fe0xe", + "j9793j7jm7hnx6s41ha8njhmn98ff9f2", + "j97bbdjjhsagatgb7vak639n4n8ffvq8", + "j97fwc8cmwcsv8bk0jmjwyca098ffcw9", + "j97ftskjk3494rad875kjythp58feqmn" + ] + } + }, + { + "name": "initialPinnedPage2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j979hrfx577xxtx2jng662pg4d8fen9p", + "j978bhn1a08zepck244t1v75b58feex1", + "j972h41k9618kxfsjarfpj50518ff8b8", + "j97csrnpehxt2jegq6ps96d1dn8ffnbv", + "j9769xpnfm99pfg44rd8t5kdk98ffb77", + "j974kcpv1sb7dfxfrc6q698p618ffwsm", + "j97fbx0bybms8c2say3zhknxg58ffxc8", + "j975krc3haj8kx9s44ta3pgtzx8ffcqv", + "j974xn559br80nbs84e28gqs4d8fendb", + "j9786j4ygzh80z84a1pxc60agd8fecnp" + ] + } + }, + { + "name": "initialPinnedPage3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "initialPinnedPages", + "value": [ + { + "rows": 10, + "isDone": false + }, + { + "rows": 10, + "isDone": false + }, + { + "rows": 10, + "isDone": false + }, + { + "rows": 0, + "isDone": true + } + ] + }, + { + "name": "pinnedPageAfterInsert1", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j971h1hx96zefha95eqqb489ad8feejq", + "j973ra0akpfm4bh06465np9mpd8ffe7t", + "j9776zqksyvw46ha7933x8zk0h8ffvr5", + "j970rq2qta78mwk6ftdt60kd4n8ffy35", + "j977qebymykf5pdsvpsygsxp6x8fexpn", + "j97215ck8vvjan5vzhgm42cvw18fe0xe", + "j9793j7jm7hnx6s41ha8njhmn98ff9f2", + "j97bbdjjhsagatgb7vak639n4n8ffvq8", + "j97fwc8cmwcsv8bk0jmjwyca098ffcw9", + "j97ftskjk3494rad875kjythp58feqmn" + ] + } + }, + { + "name": "pinnedPageAfterInsert2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j979hrfx577xxtx2jng662pg4d8fen9p", + "j978bhn1a08zepck244t1v75b58feex1", + "j972h41k9618kxfsjarfpj50518ff8b8", + "j97csrnpehxt2jegq6ps96d1dn8ffnbv", + "j9769xpnfm99pfg44rd8t5kdk98ffb77", + "j974kcpv1sb7dfxfrc6q698p618ffwsm", + "j97fbx0bybms8c2say3zhknxg58ffxc8", + "j975krc3haj8kx9s44ta3pgtzx8ffcqv", + "j974xn559br80nbs84e28gqs4d8fendb", + "j9786j4ygzh80z84a1pxc60agd8fecnp" + ] + } + }, + { + "name": "pinnedPageAfterInsert3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "pinnedPageAfterInsert0", + "value": { + "rows": 15, + "status": "SplitRecommended", + "ids": [ + "j973jstgxkh7sz5gt6hehscx2s8fe047", + "j97bmnv6b31cstarv1jndvb51x8ffee5", + "j97epkbw203hz0mez8t89g9sj98feqt2", + "j976njz0jbcbx3hwztsnakk6218ffy2b", + "j97ezggrvcd5wrgjgtdmqyny558fe102", + "j97b8rkty7g9atgkydr8d39kqd8ffzhg", + "j97252kngyse1ph0ejgcsttew18fen2k", + "j979ay0352ce0xc70f848nh7458ff25h", + "j97b1d803j9m2znr4qd8w97r198fesa7", + "j977q3nfsg1btgstrav26gcje18ffhch", + "j976t5ze3r4gtbcb80ed9d930d8ff41p", + "j9727snedd33rg47pcn210ksm98feb4k", + "j97enmd26hwrpcxqvta8x92f058ff6hx", + "j979v25nep3yges2st7wg8n5ns8ffq65", + "j9796jqr722hf1x380avcxg3j18fe3gj" + ] + } + }, + { + "name": "pagesAfterInsert", + "value": [ + [ + "j973jstgxkh7sz5gt6hehscx2s8fe047", + "j97bmnv6b31cstarv1jndvb51x8ffee5", + "j97epkbw203hz0mez8t89g9sj98feqt2", + "j976njz0jbcbx3hwztsnakk6218ffy2b", + "j97ezggrvcd5wrgjgtdmqyny558fe102", + "j97b8rkty7g9atgkydr8d39kqd8ffzhg", + "j97252kngyse1ph0ejgcsttew18fen2k", + "j979ay0352ce0xc70f848nh7458ff25h", + "j97b1d803j9m2znr4qd8w97r198fesa7", + "j977q3nfsg1btgstrav26gcje18ffhch", + "j976t5ze3r4gtbcb80ed9d930d8ff41p", + "j9727snedd33rg47pcn210ksm98feb4k", + "j97enmd26hwrpcxqvta8x92f058ff6hx", + "j979v25nep3yges2st7wg8n5ns8ffq65", + "j9796jqr722hf1x380avcxg3j18fe3gj" + ], + [ + "j971h1hx96zefha95eqqb489ad8feejq", + "j973ra0akpfm4bh06465np9mpd8ffe7t", + "j9776zqksyvw46ha7933x8zk0h8ffvr5", + "j970rq2qta78mwk6ftdt60kd4n8ffy35", + "j977qebymykf5pdsvpsygsxp6x8fexpn", + "j97215ck8vvjan5vzhgm42cvw18fe0xe", + "j9793j7jm7hnx6s41ha8njhmn98ff9f2", + "j97bbdjjhsagatgb7vak639n4n8ffvq8", + "j97fwc8cmwcsv8bk0jmjwyca098ffcw9", + "j97ftskjk3494rad875kjythp58feqmn" + ], + [ + "j979hrfx577xxtx2jng662pg4d8fen9p", + "j978bhn1a08zepck244t1v75b58feex1", + "j972h41k9618kxfsjarfpj50518ff8b8", + "j97csrnpehxt2jegq6ps96d1dn8ffnbv", + "j9769xpnfm99pfg44rd8t5kdk98ffb77", + "j974kcpv1sb7dfxfrc6q698p618ffwsm", + "j97fbx0bybms8c2say3zhknxg58ffxc8", + "j975krc3haj8kx9s44ta3pgtzx8ffcqv", + "j974xn559br80nbs84e28gqs4d8fendb", + "j9786j4ygzh80z84a1pxc60agd8fecnp" + ], + [] + ] + }, + { + "name": "contiguityAfterInsert", + "value": { + "actual": [ + "j973jstgxkh7sz5gt6hehscx2s8fe047", + "j97bmnv6b31cstarv1jndvb51x8ffee5", + "j97epkbw203hz0mez8t89g9sj98feqt2", + "j976njz0jbcbx3hwztsnakk6218ffy2b", + "j97ezggrvcd5wrgjgtdmqyny558fe102", + "j97b8rkty7g9atgkydr8d39kqd8ffzhg", + "j97252kngyse1ph0ejgcsttew18fen2k", + "j979ay0352ce0xc70f848nh7458ff25h", + "j97b1d803j9m2znr4qd8w97r198fesa7", + "j977q3nfsg1btgstrav26gcje18ffhch", + "j976t5ze3r4gtbcb80ed9d930d8ff41p", + "j9727snedd33rg47pcn210ksm98feb4k", + "j97enmd26hwrpcxqvta8x92f058ff6hx", + "j979v25nep3yges2st7wg8n5ns8ffq65", + "j9796jqr722hf1x380avcxg3j18fe3gj", + "j971h1hx96zefha95eqqb489ad8feejq", + "j973ra0akpfm4bh06465np9mpd8ffe7t", + "j9776zqksyvw46ha7933x8zk0h8ffvr5", + "j970rq2qta78mwk6ftdt60kd4n8ffy35", + "j977qebymykf5pdsvpsygsxp6x8fexpn", + "j97215ck8vvjan5vzhgm42cvw18fe0xe", + "j9793j7jm7hnx6s41ha8njhmn98ff9f2", + "j97bbdjjhsagatgb7vak639n4n8ffvq8", + "j97fwc8cmwcsv8bk0jmjwyca098ffcw9", + "j97ftskjk3494rad875kjythp58feqmn", + "j979hrfx577xxtx2jng662pg4d8fen9p", + "j978bhn1a08zepck244t1v75b58feex1", + "j972h41k9618kxfsjarfpj50518ff8b8", + "j97csrnpehxt2jegq6ps96d1dn8ffnbv", + "j9769xpnfm99pfg44rd8t5kdk98ffb77", + "j974kcpv1sb7dfxfrc6q698p618ffwsm", + "j97fbx0bybms8c2say3zhknxg58ffxc8", + "j975krc3haj8kx9s44ta3pgtzx8ffcqv", + "j974xn559br80nbs84e28gqs4d8fendb", + "j9786j4ygzh80z84a1pxc60agd8fecnp" + ], + "expected": [ + "j973jstgxkh7sz5gt6hehscx2s8fe047", + "j97bmnv6b31cstarv1jndvb51x8ffee5", + "j97epkbw203hz0mez8t89g9sj98feqt2", + "j976njz0jbcbx3hwztsnakk6218ffy2b", + "j97ezggrvcd5wrgjgtdmqyny558fe102", + "j97b8rkty7g9atgkydr8d39kqd8ffzhg", + "j97252kngyse1ph0ejgcsttew18fen2k", + "j979ay0352ce0xc70f848nh7458ff25h", + "j97b1d803j9m2znr4qd8w97r198fesa7", + "j977q3nfsg1btgstrav26gcje18ffhch", + "j976t5ze3r4gtbcb80ed9d930d8ff41p", + "j9727snedd33rg47pcn210ksm98feb4k", + "j97enmd26hwrpcxqvta8x92f058ff6hx", + "j979v25nep3yges2st7wg8n5ns8ffq65", + "j9796jqr722hf1x380avcxg3j18fe3gj", + "j971h1hx96zefha95eqqb489ad8feejq", + "j973ra0akpfm4bh06465np9mpd8ffe7t", + "j9776zqksyvw46ha7933x8zk0h8ffvr5", + "j970rq2qta78mwk6ftdt60kd4n8ffy35", + "j977qebymykf5pdsvpsygsxp6x8fexpn", + "j97215ck8vvjan5vzhgm42cvw18fe0xe", + "j9793j7jm7hnx6s41ha8njhmn98ff9f2", + "j97bbdjjhsagatgb7vak639n4n8ffvq8", + "j97fwc8cmwcsv8bk0jmjwyca098ffcw9", + "j97ftskjk3494rad875kjythp58feqmn", + "j979hrfx577xxtx2jng662pg4d8fen9p", + "j978bhn1a08zepck244t1v75b58feex1", + "j972h41k9618kxfsjarfpj50518ff8b8", + "j97csrnpehxt2jegq6ps96d1dn8ffnbv", + "j9769xpnfm99pfg44rd8t5kdk98ffb77", + "j974kcpv1sb7dfxfrc6q698p618ffwsm", + "j97fbx0bybms8c2say3zhknxg58ffxc8", + "j975krc3haj8kx9s44ta3pgtzx8ffcqv", + "j974xn559br80nbs84e28gqs4d8fendb", + "j9786j4ygzh80z84a1pxc60agd8fecnp" + ] + } + }, + { + "name": "pinnedPageAfterDelete0", + "value": { + "rows": 15, + "status": "SplitRecommended", + "ids": [ + "j973jstgxkh7sz5gt6hehscx2s8fe047", + "j97bmnv6b31cstarv1jndvb51x8ffee5", + "j97epkbw203hz0mez8t89g9sj98feqt2", + "j976njz0jbcbx3hwztsnakk6218ffy2b", + "j97ezggrvcd5wrgjgtdmqyny558fe102", + "j97b8rkty7g9atgkydr8d39kqd8ffzhg", + "j97252kngyse1ph0ejgcsttew18fen2k", + "j979ay0352ce0xc70f848nh7458ff25h", + "j97b1d803j9m2znr4qd8w97r198fesa7", + "j977q3nfsg1btgstrav26gcje18ffhch", + "j976t5ze3r4gtbcb80ed9d930d8ff41p", + "j9727snedd33rg47pcn210ksm98feb4k", + "j97enmd26hwrpcxqvta8x92f058ff6hx", + "j979v25nep3yges2st7wg8n5ns8ffq65", + "j9796jqr722hf1x380avcxg3j18fe3gj" + ] + } + }, + { + "name": "pinnedPageAfterDelete2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j979hrfx577xxtx2jng662pg4d8fen9p", + "j978bhn1a08zepck244t1v75b58feex1", + "j972h41k9618kxfsjarfpj50518ff8b8", + "j97csrnpehxt2jegq6ps96d1dn8ffnbv", + "j9769xpnfm99pfg44rd8t5kdk98ffb77", + "j974kcpv1sb7dfxfrc6q698p618ffwsm", + "j97fbx0bybms8c2say3zhknxg58ffxc8", + "j975krc3haj8kx9s44ta3pgtzx8ffcqv", + "j974xn559br80nbs84e28gqs4d8fendb", + "j9786j4ygzh80z84a1pxc60agd8fecnp" + ] + } + }, + { + "name": "pinnedPageAfterDelete3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "pinnedPageAfterDelete1", + "value": { + "rows": 9, + "status": null, + "ids": [ + "j971h1hx96zefha95eqqb489ad8feejq", + "j9776zqksyvw46ha7933x8zk0h8ffvr5", + "j970rq2qta78mwk6ftdt60kd4n8ffy35", + "j977qebymykf5pdsvpsygsxp6x8fexpn", + "j97215ck8vvjan5vzhgm42cvw18fe0xe", + "j9793j7jm7hnx6s41ha8njhmn98ff9f2", + "j97bbdjjhsagatgb7vak639n4n8ffvq8", + "j97fwc8cmwcsv8bk0jmjwyca098ffcw9", + "j97ftskjk3494rad875kjythp58feqmn" + ] + } + }, + { + "name": "contiguitySummary", + "value": { + "pages": 4, + "rowsAfterInsert": 35, + "rowsAfterDelete": 34, + "removed": "j973ra0akpfm4bh06465np9mpd8ffe7t", + "actual": [ + "j973jstgxkh7sz5gt6hehscx2s8fe047", + "j97bmnv6b31cstarv1jndvb51x8ffee5", + "j97epkbw203hz0mez8t89g9sj98feqt2", + "j976njz0jbcbx3hwztsnakk6218ffy2b", + "j97ezggrvcd5wrgjgtdmqyny558fe102", + "j97b8rkty7g9atgkydr8d39kqd8ffzhg", + "j97252kngyse1ph0ejgcsttew18fen2k", + "j979ay0352ce0xc70f848nh7458ff25h", + "j97b1d803j9m2znr4qd8w97r198fesa7", + "j977q3nfsg1btgstrav26gcje18ffhch", + "j976t5ze3r4gtbcb80ed9d930d8ff41p", + "j9727snedd33rg47pcn210ksm98feb4k", + "j97enmd26hwrpcxqvta8x92f058ff6hx", + "j979v25nep3yges2st7wg8n5ns8ffq65", + "j9796jqr722hf1x380avcxg3j18fe3gj", + "j971h1hx96zefha95eqqb489ad8feejq", + "j9776zqksyvw46ha7933x8zk0h8ffvr5", + "j970rq2qta78mwk6ftdt60kd4n8ffy35", + "j977qebymykf5pdsvpsygsxp6x8fexpn", + "j97215ck8vvjan5vzhgm42cvw18fe0xe", + "j9793j7jm7hnx6s41ha8njhmn98ff9f2", + "j97bbdjjhsagatgb7vak639n4n8ffvq8", + "j97fwc8cmwcsv8bk0jmjwyca098ffcw9", + "j97ftskjk3494rad875kjythp58feqmn", + "j979hrfx577xxtx2jng662pg4d8fen9p", + "j978bhn1a08zepck244t1v75b58feex1", + "j972h41k9618kxfsjarfpj50518ff8b8", + "j97csrnpehxt2jegq6ps96d1dn8ffnbv", + "j9769xpnfm99pfg44rd8t5kdk98ffb77", + "j974kcpv1sb7dfxfrc6q698p618ffwsm", + "j97fbx0bybms8c2say3zhknxg58ffxc8", + "j975krc3haj8kx9s44ta3pgtzx8ffcqv", + "j974xn559br80nbs84e28gqs4d8fendb", + "j9786j4ygzh80z84a1pxc60agd8fecnp" + ], + "expected": [ + "j973jstgxkh7sz5gt6hehscx2s8fe047", + "j97bmnv6b31cstarv1jndvb51x8ffee5", + "j97epkbw203hz0mez8t89g9sj98feqt2", + "j976njz0jbcbx3hwztsnakk6218ffy2b", + "j97ezggrvcd5wrgjgtdmqyny558fe102", + "j97b8rkty7g9atgkydr8d39kqd8ffzhg", + "j97252kngyse1ph0ejgcsttew18fen2k", + "j979ay0352ce0xc70f848nh7458ff25h", + "j97b1d803j9m2znr4qd8w97r198fesa7", + "j977q3nfsg1btgstrav26gcje18ffhch", + "j976t5ze3r4gtbcb80ed9d930d8ff41p", + "j9727snedd33rg47pcn210ksm98feb4k", + "j97enmd26hwrpcxqvta8x92f058ff6hx", + "j979v25nep3yges2st7wg8n5ns8ffq65", + "j9796jqr722hf1x380avcxg3j18fe3gj", + "j971h1hx96zefha95eqqb489ad8feejq", + "j9776zqksyvw46ha7933x8zk0h8ffvr5", + "j970rq2qta78mwk6ftdt60kd4n8ffy35", + "j977qebymykf5pdsvpsygsxp6x8fexpn", + "j97215ck8vvjan5vzhgm42cvw18fe0xe", + "j9793j7jm7hnx6s41ha8njhmn98ff9f2", + "j97bbdjjhsagatgb7vak639n4n8ffvq8", + "j97fwc8cmwcsv8bk0jmjwyca098ffcw9", + "j97ftskjk3494rad875kjythp58feqmn", + "j979hrfx577xxtx2jng662pg4d8fen9p", + "j978bhn1a08zepck244t1v75b58feex1", + "j972h41k9618kxfsjarfpj50518ff8b8", + "j97csrnpehxt2jegq6ps96d1dn8ffnbv", + "j9769xpnfm99pfg44rd8t5kdk98ffb77", + "j974kcpv1sb7dfxfrc6q698p618ffwsm", + "j97fbx0bybms8c2say3zhknxg58ffxc8", + "j975krc3haj8kx9s44ta3pgtzx8ffcqv", + "j974xn559br80nbs84e28gqs4d8fendb", + "j9786j4ygzh80z84a1pxc60agd8fecnp" + ] + } + } + ] + }, + { + "name": "An ordinary client cannot call a component's function (spec:platform.native-harness.ordinary-client-refused-component-function) > executes the bound example against a fresh world", + "file": "tests/native/ordinary-client-refused-component-function.test.ts", + "result": "passed", + "durationMs": 4421.186125, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "componentSocketSummary", + "value": { + "sends": 3, + "backendCloses": 3, + "answer": "no answer", + "socketWaitMs": 3000 + } + } + ] + }, + { + "name": "native: a taken --port causes fresh port pairs and the third start succeeds", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 2996.93825, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "ecd9bddb23742fe5a9aa12bd6e80839542c1eb56dbe7026691aa474bbedbb3a9", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "Probe 1: the HTTP client and a caller's retry (spec:facts.f05-react-client-retries-until-confirmed.probe-1-http-client-retry) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.test.ts", + "result": "passed", + "durationMs": 877.342958, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "requestsAfterEachCall", + "value": [ + 1, + 2 + ] + }, + { + "name": "httpRetrySummary", + "value": { + "rows": 2, + "requestsAfterCall": [ + 1, + 2 + ] + } + } + ] + }, + { + "name": "native: the parent sweep verifies and kills two real owned backend processes", + "file": "tests/native/harness-sweep.test.ts", + "result": "passed", + "durationMs": 329.102375, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + }, + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: a component query that calls ctx.auth.getUserIdentity() completes, and its answer is recorded", + "file": "tests/native/component-auth.test.ts", + "result": "passed", + "durationMs": 935.8024170000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "identitySeenInsideComponent", + "value": { + "answered": "an identity", + "issuer": "https://fixture-issuer.test", + "subject": "user-1" + } + } + ] + }, + { + "name": "A token signed by the fixture issuer yields the caller's identity (spec:platform.native-harness.fixture-issuer-token-yields-identity) > executes the bound example against a fresh world", + "file": "tests/native/fixture-issuer-token-yields-identity.test.ts", + "result": "passed", + "durationMs": 903.280542, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "The admin key acting as an identity reaches an internal function (spec:platform.native-harness.admin-key-acting-as-identity-reaches-internal-function) > executes the bound example against a fresh world", + "file": "tests/native/admin-key-acting-as-identity-reaches-internal-function.test.ts", + "result": "passed", + "durationMs": 945.915375, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "An ordinary client cannot call an internal function (spec:platform.native-harness.ordinary-client-refused-internal-function) > executes the bound example against a fresh world", + "file": "tests/native/ordinary-client-refused-internal-function.test.ts", + "result": "passed", + "durationMs": 968.193959, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: a taken --site-proxy-port causes fresh port pairs and the third start succeeds", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 936.0304590000005, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "6a705c3ca6530828190993aef5093614f92e3578aed5f0fa40aef686e2a13f0b", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "native: three taken ports fail clearly and remove the failed start's storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 3812.8176249999988, + "errors": [], + "backends": [], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "Probe 1: a backend restart with a mutation in flight (spec:facts.f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.test.ts", + "result": "passed", + "durationMs": 10688.744958000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "trials", + "value": [ + { + "trial": "restart 0", + "delayMs": 0, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 1", + "delayMs": 0, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 2", + "delayMs": 0, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 3", + "delayMs": 1, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 4", + "delayMs": 2, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 5", + "delayMs": 4, + "committedBeforeKill": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 6", + "delayMs": 8, + "committedBeforeKill": false, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 7", + "delayMs": 16, + "committedBeforeKill": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 8", + "delayMs": 32, + "committedBeforeKill": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 9", + "delayMs": 64, + "committedBeforeKill": true, + "resolved": true, + "rows": 1 + } + ] + }, + { + "name": "restartSummary", + "value": { + "trials": 10, + "resolved": 10, + "killedBeforeCommit": 6, + "killedAfterCommit": 4, + "rowCounts": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + } + ] + }, + { + "name": "native: stop escalates to SIGKILL when the owned process ignores SIGINT and keeps its storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 5351.905083, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "6fc2367687c5b0c547d178df4352fffef5d86ed97bfd56af25feb2342509ada4", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: a log mark from before a kill cannot be read after restart", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 765.4902919999986, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 4: a nested call draws on the caller's transaction limits (spec:facts.f13-transactions-have-limits.probe-4-nested-call-shares-limits) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-nested-call-shares-limits.test.ts", + "result": "passed", + "durationMs": 21671.735458, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "seededPayloads", + "value": { + "size": 921600, + "parentCount": 10, + "childCount": 10 + } + }, + { + "name": "combinedReadOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 41131521315d3f1a] Server Error\nUncaught Error: Uncaught Error: Too many bytes read in a single function execution (limit: 16777216 bytes). Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\n at async handler (../fixture/convex/limits.ts:47:11)\n\n at async handler (../fixture/convex/limits.ts:78:34)\n" + } + }, + { + "name": "readTogether", + "value": "fails on the bytes-read limit" + }, + { + "name": "parentReadOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childReadOutcome", + "value": { + "outcome": "returns", + "error": null + } + }, + { + "name": "readsAlone", + "value": { + "parent": "commits", + "child": "returns", + "parentDocuments": 10, + "childDocuments": 10, + "childPayloadBytes": 9216000 + } + }, + { + "name": "sharedReadMetrics", + "value": { + "byteGrowth": 921684, + "documentGrowth": 1, + "childBytes": 921684, + "childDocuments": 1, + "payloadBytes": 921600 + } + }, + { + "name": "combinedWriteOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 9305696dc2472536] Server Error\nUncaught Error: Uncaught Error: Too many bytes written in a single function execution (limit: 16 MiB)\n at async handler (../fixture/convex/limits.ts:23:35)\n\n at async handler (../fixture/convex/limits.ts:102:8)\n" + } + }, + { + "name": "writesTogether", + "value": "fails on the bytes-written limit" + }, + { + "name": "failedWriteRows", + "value": 0 + }, + { + "name": "parentWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "parentWriteRows", + "value": 10 + }, + { + "name": "childWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childWriteRows", + "value": 20 + }, + { + "name": "limitSummary", + "value": { + "boundary": "nested", + "combinedRead": "fails on the bytes-read limit", + "combinedWrite": "fails on the bytes-written limit", + "failedWriteRows": 0, + "committedRows": 20 + } + } + ] + }, + { + "name": "Probe 4: a component call draws on the caller's transaction limits (spec:facts.f13-transactions-have-limits.probe-4-component-call-shares-limits) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-component-call-shares-limits.test.ts", + "result": "passed", + "durationMs": 21852.233375, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "seededPayloads", + "value": { + "size": 921600, + "parentCount": 10, + "childCount": 10 + } + }, + { + "name": "combinedReadOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 7a4df03f2219085c] Server Error\nUncaught Error: Uncaught Error: Too many bytes read in a single function execution (limit: 16777216 bytes). Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\n at async handler (../fixture/convex/annex/limits.ts:34:11)\n\n at async handler (../fixture/convex/limits.ts:78:34)\n" + } + }, + { + "name": "readTogether", + "value": "fails on the bytes-read limit" + }, + { + "name": "parentReadOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childReadOutcome", + "value": { + "outcome": "returns", + "error": null + } + }, + { + "name": "readsAlone", + "value": { + "parent": "commits", + "child": "returns", + "parentDocuments": 10, + "childDocuments": 10, + "childPayloadBytes": 9216000 + } + }, + { + "name": "sharedReadMetrics", + "value": { + "byteGrowth": 921684, + "documentGrowth": 1, + "childBytes": 921684, + "childDocuments": 1, + "payloadBytes": 921600 + } + }, + { + "name": "combinedWriteOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 59a446f57d040690] Server Error\nUncaught Error: Uncaught Error: Too many bytes written in a single function execution (limit: 16 MiB)\n at async handler (../fixture/convex/annex/limits.ts:8:35)\n\n at async handler (../fixture/convex/limits.ts:102:8)\n" + } + }, + { + "name": "writesTogether", + "value": "fails on the bytes-written limit" + }, + { + "name": "failedWriteRows", + "value": 0 + }, + { + "name": "parentWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "parentWriteRows", + "value": 10 + }, + { + "name": "childWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childWriteRows", + "value": 20 + }, + { + "name": "limitSummary", + "value": { + "boundary": "annex", + "combinedRead": "fails on the bytes-read limit", + "combinedWrite": "fails on the bytes-written limit", + "failedWriteRows": 0, + "committedRows": 20 + } + } + ] + }, + { + "name": "native: a restart that stalls after spawning reaps its process", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 30377.708166999997, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "9e5cc661841b1c8f1f4526a13b092152107ad242c4e06bfbccf4a19ac7cfd365", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: codegen interrupted by SIGINT removes its starting backend and storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 348.77554200000304, + "errors": [], + "backends": [], + "measurements": [] + }, + { + "name": "native: codegen interrupted by SIGTERM removes its starting backend and storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 290.20262500000536, + "errors": [], + "backends": [], + "measurements": [] + }, + { + "name": "native: backend output split across chunks is redacted before the rolling buffer clips it", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 363.60520800000086, + "errors": [], + "backends": [], + "measurements": [] + } + ], + "unhandledErrors": [] +} diff --git a/evidence/native-20261001T185851Z-45abf7a-166f856c-f28f-4334-ad32-444c76074b9e.json b/evidence/native-20261001T185851Z-45abf7a-166f856c-f28f-4334-ad32-444c76074b9e.json new file mode 100644 index 0000000..7c821f5 --- /dev/null +++ b/evidence/native-20261001T185851Z-45abf7a-166f856c-f28f-4334-ad32-444c76074b9e.json @@ -0,0 +1,3458 @@ +{ + "tier": "native", + "commit": "45abf7ac107b23b495071c78b88b9f4fec496c68", + "clean": true, + "command": "vitest run --project native", + "startedAt": "2026-10-01T18:58:51.166Z", + "finishedAt": "2026-10-01T18:59:39.167Z", + "result": "passed", + "versions": { + "node": "v24.21.0", + "convex": "1.46.0", + "convex-helpers": "0.1.124", + "convex-test": "0.0.60", + "vitest": "5.0.3", + "typescript": "7.0.2", + "react": "19.3.0", + "happy-dom": "20.14.5" + }, + "tests": [ + { + "name": "native: stop and kill keep stored rows across restart, and dispose removes storage twice", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 1790.6810839999998, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://changed-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 5: the client hook splits a page across the boundary and loses rows (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.test.ts", + "result": "passed", + "durationMs": 1899.533667, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "hookBeforeInsert", + "value": { + "rows": 30, + "status": "Exhausted" + } + }, + { + "name": "hookCappedPage", + "value": { + "status": "SplitRequired", + "rows": 20, + "continueCursor": "[4,1790881133295.863,\"j970gvkse7gag7vvdz6e1wce9d8ff3nh\"]", + "endCursor": "[9,1790881133295.8643,\"j9749smfeta3sp6tss3pg755yh8fe2qy\"]" + } + }, + { + "name": "hookSplitSummary", + "value": { + "rowsInList": 45, + "rowsShown": 40, + "status": "Exhausted", + "missingPositions": [ + 5, + 6, + 7, + 8, + 9 + ] + } + }, + { + "name": "hookMissingCursorRange", + "value": { + "missing": [ + "j977jke44zfrg2vmt7zp3qm1ex8ffh93", + "j9719mvdct8dxekwcre2vg3nb98ffgtr", + "j971gs5x9q2vrndvr3skx1yzyh8ff2fd", + "j973stnm3qy70zr1rmxv5mz46x8ffanm", + "j9749smfeta3sp6tss3pg755yh8fe2qy" + ], + "betweenCursors": [ + "j977jke44zfrg2vmt7zp3qm1ex8ffh93", + "j9719mvdct8dxekwcre2vg3nb98ffgtr", + "j971gs5x9q2vrndvr3skx1yzyh8ff2fd", + "j973stnm3qy70zr1rmxv5mz46x8ffanm", + "j9749smfeta3sp6tss3pg755yh8fe2qy" + ] + } + } + ] + }, + { + "name": "Probe 1: a client closed with a pending mutation (spec:facts.f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.test.ts", + "result": "passed", + "durationMs": 2287.133708, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "trials", + "value": [ + { + "trial": "closed 0", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 1", + "gapMs": 1, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 2", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 3", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 4", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 5", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 6", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 7", + "gapMs": 1, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 8", + "gapMs": 2, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 9", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 10", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 11", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 12", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 13", + "gapMs": 1, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 14", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 15", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 16", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 17", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 18", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 19", + "gapMs": 1, + "confirmed": false, + "rows": 0 + } + ] + }, + { + "name": "closedClientSummary", + "value": { + "trials": 20, + "confirmed": 0, + "noRow": 7, + "oneRow": 13 + } + } + ] + }, + { + "name": "Probe 3: the cost of a component call from a query (spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query) > executes the bound example against a fresh world", + "file": "tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.test.ts", + "result": "passed", + "durationMs": 2842.1014579999996, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "costSamples", + "value": [ + { + "round": 0, + "path": "helper", + "cacheBuster": 1000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.014059167, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.014059167, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "nested", + "cacheBuster": 2000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.127796083, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.127796083, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "component", + "cacheBuster": 3000, + "result": { + "documentsRead": 50, + "id": "jn7caf0y284t718n6p20hp8k1n8feps1" + }, + "callError": null, + "executionTimeSeconds": 0.132434666, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.132434666, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "helper", + "cacheBuster": 4000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.002292041, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002292041, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "nested", + "cacheBuster": 5000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.053991916, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.053991916, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "component", + "cacheBuster": 6000, + "result": { + "documentsRead": 50, + "id": "jn7caf0y284t718n6p20hp8k1n8feps1" + }, + "callError": null, + "executionTimeSeconds": 0.0825035, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.0825035, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "helper", + "cacheBuster": 7000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.002180209, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002180209, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "nested", + "cacheBuster": 8000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.048631083, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.048631083, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "component", + "cacheBuster": 9000, + "result": { + "documentsRead": 50, + "id": "jn7caf0y284t718n6p20hp8k1n8feps1" + }, + "callError": null, + "executionTimeSeconds": 0.082625834, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.082625834, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "helper", + "cacheBuster": 10000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.002206708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002206708, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "nested", + "cacheBuster": 11000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.057526, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.057526, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "component", + "cacheBuster": 12000, + "result": { + "documentsRead": 50, + "id": "jn7caf0y284t718n6p20hp8k1n8feps1" + }, + "callError": null, + "executionTimeSeconds": 0.094146, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.094146, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "helper", + "cacheBuster": 13000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.002463792, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002463792, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "nested", + "cacheBuster": 14000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.055585125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.055585125, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "component", + "cacheBuster": 15000, + "result": { + "documentsRead": 50, + "id": "jn7caf0y284t718n6p20hp8k1n8feps1" + }, + "callError": null, + "executionTimeSeconds": 0.081516209, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.081516209, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "helper", + "cacheBuster": 16000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.002277541, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002277541, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "nested", + "cacheBuster": 17000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.051055667, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.051055667, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "component", + "cacheBuster": 18000, + "result": { + "documentsRead": 50, + "id": "jn7caf0y284t718n6p20hp8k1n8feps1" + }, + "callError": null, + "executionTimeSeconds": 0.093731167, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.093731167, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "helper", + "cacheBuster": 19000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.00218825, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.00218825, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "nested", + "cacheBuster": 20000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.052364542, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.052364542, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "component", + "cacheBuster": 21000, + "result": { + "documentsRead": 50, + "id": "jn7caf0y284t718n6p20hp8k1n8feps1" + }, + "callError": null, + "executionTimeSeconds": 0.088563, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.088563, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "helper", + "cacheBuster": 22000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.002058166, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002058166, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "nested", + "cacheBuster": 23000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.047839625, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.047839625, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "component", + "cacheBuster": 24000, + "result": { + "documentsRead": 50, + "id": "jn7caf0y284t718n6p20hp8k1n8feps1" + }, + "callError": null, + "executionTimeSeconds": 0.093287167, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.093287167, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "helper", + "cacheBuster": 25000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.00216775, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.00216775, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "nested", + "cacheBuster": 26000, + "result": { + "documentsRead": 50, + "id": "jx777cxa0mnze0xe9rrxtpj2rs8ff4mb" + }, + "callError": null, + "executionTimeSeconds": 0.054195041, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.054195041, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "component", + "cacheBuster": 27000, + "result": { + "documentsRead": 50, + "id": "jn7caf0y284t718n6p20hp8k1n8feps1" + }, + "callError": null, + "executionTimeSeconds": 0.092239583, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.092239583, + "cachedResult": false + } + ] + } + ] + }, + { + "name": "readCostSummary", + "value": { + "kind": "query", + "readsPerSample": 50, + "acceptedSamples": { + "helper": 9, + "nested": 9, + "component": 9 + }, + "componentCompletionRecords": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "helper": 0.002206708, + "nested": 0.053991916, + "component": 0.092239583, + "componentMinusHelperSeconds": 0.090032875, + "componentMinusHelperPerReadSeconds": 0.0018006575 + } + } + ] + }, + { + "name": "Probe 3: the cost of a component call from a mutation (spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.test.ts", + "result": "passed", + "durationMs": 2915.6865, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "costSamples", + "value": [ + { + "round": 0, + "path": "helper", + "cacheBuster": 1000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.003846375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.003846375, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "nested", + "cacheBuster": 2000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.072606833, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.072606833, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "component", + "cacheBuster": 3000, + "result": { + "documentsRead": 50, + "id": "jn7enf64ex9dgqm21s90hdd8fx8fepc8" + }, + "callError": null, + "executionTimeSeconds": 0.091508042, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.091508042, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "helper", + "cacheBuster": 4000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.002440416, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002440416, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "nested", + "cacheBuster": 5000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.055854, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.055854, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "component", + "cacheBuster": 6000, + "result": { + "documentsRead": 50, + "id": "jn7enf64ex9dgqm21s90hdd8fx8fepc8" + }, + "callError": null, + "executionTimeSeconds": 0.078373084, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.078373084, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "helper", + "cacheBuster": 7000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.002099333, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002099333, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "nested", + "cacheBuster": 8000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.052761916, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.052761916, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "component", + "cacheBuster": 9000, + "result": { + "documentsRead": 50, + "id": "jn7enf64ex9dgqm21s90hdd8fx8fepc8" + }, + "callError": null, + "executionTimeSeconds": 0.090199666, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.090199666, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "helper", + "cacheBuster": 10000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.002381416, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002381416, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "nested", + "cacheBuster": 11000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.055668958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.055668958, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "component", + "cacheBuster": 12000, + "result": { + "documentsRead": 50, + "id": "jn7enf64ex9dgqm21s90hdd8fx8fepc8" + }, + "callError": null, + "executionTimeSeconds": 0.096426875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.096426875, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "helper", + "cacheBuster": 13000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.002465042, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002465042, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "nested", + "cacheBuster": 14000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.048264791, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.048264791, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "component", + "cacheBuster": 15000, + "result": { + "documentsRead": 50, + "id": "jn7enf64ex9dgqm21s90hdd8fx8fepc8" + }, + "callError": null, + "executionTimeSeconds": 0.086975291, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.086975291, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "helper", + "cacheBuster": 16000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.002207084, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002207084, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "nested", + "cacheBuster": 17000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.061846708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.061846708, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "component", + "cacheBuster": 18000, + "result": { + "documentsRead": 50, + "id": "jn7enf64ex9dgqm21s90hdd8fx8fepc8" + }, + "callError": null, + "executionTimeSeconds": 0.081910708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.081910708, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "helper", + "cacheBuster": 19000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.002369584, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002369584, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "nested", + "cacheBuster": 20000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.058838458, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.058838458, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "component", + "cacheBuster": 21000, + "result": { + "documentsRead": 50, + "id": "jn7enf64ex9dgqm21s90hdd8fx8fepc8" + }, + "callError": null, + "executionTimeSeconds": 0.079233959, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.079233959, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "helper", + "cacheBuster": 22000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.002139375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002139375, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "nested", + "cacheBuster": 23000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.057750458, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.057750458, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "component", + "cacheBuster": 24000, + "result": { + "documentsRead": 50, + "id": "jn7enf64ex9dgqm21s90hdd8fx8fepc8" + }, + "callError": null, + "executionTimeSeconds": 0.085687208, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.085687208, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "helper", + "cacheBuster": 25000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.003060875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.003060875, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "nested", + "cacheBuster": 26000, + "result": { + "documentsRead": 50, + "id": "jx765fvhbcnwt821hfy77k44ad8fe3yg" + }, + "callError": null, + "executionTimeSeconds": 0.057554333, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.057554333, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "component", + "cacheBuster": 27000, + "result": { + "documentsRead": 50, + "id": "jn7enf64ex9dgqm21s90hdd8fx8fepc8" + }, + "callError": null, + "executionTimeSeconds": 0.08413975, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.08413975, + "cachedResult": false + } + ] + } + ] + }, + { + "name": "readCostSummary", + "value": { + "kind": "mutation", + "readsPerSample": 50, + "acceptedSamples": { + "helper": 9, + "nested": 9, + "component": 9 + }, + "componentCompletionRecords": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "helper": 0.002381416, + "nested": 0.057554333, + "component": 0.085687208, + "componentMinusHelperSeconds": 0.083305792, + "componentMinusHelperPerReadSeconds": 0.00166611584 + } + } + ] + }, + { + "name": "An ordinary client cannot call an internal function (spec:platform.native-harness.ordinary-client-refused-internal-function) > executes the bound example against a fresh world", + "file": "tests/native/ordinary-client-refused-internal-function.test.ts", + "result": "passed", + "durationMs": 807.5302090000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 2: ConvexError data through a nested mutation (spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.test.ts", + "result": "passed", + "durationMs": 859.7404999999999, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "catchingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + } + } + }, + { + "name": "rowsAfterCatching", + "value": { + "child": 0, + "parent": 1 + } + }, + { + "name": "passingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + }, + "text": "ConvexError: [Request ID: b02e15afca5a30f0] Server Error\nUncaught ConvexError: Uncaught ConvexError: {\"array\":[\"one\",2,false,null],\"boolean\":true,\"code\":\"staleVersion\",\"int64\":\"9007199254740993n\",\"nested\":{\"count\":3,\"label\":\"nested\"},\"nil\":null,\"number\":17.25}\n at handler (../fixture/convex/failures.ts:21:11)\n\n" + } + }, + { + "name": "ordinaryErrorAtParent", + "value": { + "property": null, + "message": "Error: Uncaught Error: plain failure: thrower-nested-01881302-0b7f-4e44-811d-04f17be71708\n at handler (../fixture/convex/failures.ts:39:23)\n" + } + }, + { + "name": "ordinaryErrorAtClient", + "value": { + "property": null, + "message": "Error: [Request ID: 96c71823c92531f2] Server Error\nUncaught Error: Uncaught Error: plain failure: thrower-nested-01881302-0b7f-4e44-811d-04f17be71708\n at handler (../fixture/convex/failures.ts:39:23)\n\n" + } + }, + { + "name": "throwBoundarySummary", + "value": { + "boundary": "nested", + "caughtDataUnchanged": true, + "passedDataUnchanged": true, + "childRows": 0, + "parentRows": 1, + "plainErrorFromThrower": true, + "writeGuardReached": true + } + } + ] + }, + { + "name": "Probe 1: the HTTP client and a caller's retry (spec:facts.f05-react-client-retries-until-confirmed.probe-1-http-client-retry) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.test.ts", + "result": "passed", + "durationMs": 863.4625840000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "requestsAfterEachCall", + "value": [ + 1, + 2 + ] + }, + { + "name": "httpRetrySummary", + "value": { + "rows": 2, + "requestsAfterCall": [ + 1, + 2 + ] + } + } + ] + }, + { + "name": "Probe 4: how deep nested calls may go (spec:facts.f13-transactions-have-limits.probe-4-nesting-depth) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-nesting-depth.test.ts", + "result": "passed", + "durationMs": 1032.043666, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "stack1Trial", + "value": { + "stack": 1, + "answer": 1, + "error": null, + "rows": 1 + } + }, + { + "name": "stack2Trial", + "value": { + "stack": 2, + "answer": 2, + "error": null, + "rows": 2 + } + }, + { + "name": "stack3Trial", + "value": { + "stack": 3, + "answer": 3, + "error": null, + "rows": 3 + } + }, + { + "name": "stack4Trial", + "value": { + "stack": 4, + "answer": 4, + "error": null, + "rows": 4 + } + }, + { + "name": "stack5Trial", + "value": { + "stack": 5, + "answer": 5, + "error": null, + "rows": 5 + } + }, + { + "name": "stack6Trial", + "value": { + "stack": 6, + "answer": 6, + "error": null, + "rows": 6 + } + }, + { + "name": "stack7Trial", + "value": { + "stack": 7, + "answer": 7, + "error": null, + "rows": 7 + } + }, + { + "name": "stack8Trial", + "value": { + "stack": 8, + "answer": 8, + "error": null, + "rows": 8 + } + }, + { + "name": "stack9Trial", + "value": { + "stack": 9, + "answer": 9, + "error": null, + "rows": 9 + } + }, + { + "name": "stack10Trial", + "value": { + "stack": 10, + "answer": null, + "error": "Error: [Request ID: 58ba89714b961ace] Server Error\nUncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Cross component call depth limit exceeded. Do you have an infinite loop in your app?\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n", + "rows": 0 + } + }, + { + "name": "depthBoundary", + "value": { + "deepest": 9, + "firstFailure": 10, + "refusal": "Error: [Request ID: 58ba89714b961ace] Server Error\nUncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Cross component call depth limit exceeded. Do you have an infinite loop in your app?\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n" + } + } + ] + }, + { + "name": "Probe 2: ConvexError data through a component boundary (spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary) > executes the bound example against a fresh world", + "file": "tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.test.ts", + "result": "passed", + "durationMs": 906.557208, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "catchingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + } + } + }, + { + "name": "rowsAfterCatching", + "value": { + "child": 0, + "parent": 1 + } + }, + { + "name": "passingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + }, + "text": "ConvexError: [Request ID: de2334b24799dba0] Server Error\nUncaught ConvexError: Uncaught ConvexError: {\"array\":[\"one\",2,false,null],\"boolean\":true,\"code\":\"staleVersion\",\"int64\":\"9007199254740993n\",\"nested\":{\"count\":3,\"label\":\"nested\"},\"nil\":null,\"number\":17.25}\n at handler (../fixture/convex/annex/failures.ts:8:11)\n\n" + } + }, + { + "name": "ordinaryErrorAtParent", + "value": { + "property": null, + "message": "Error: Uncaught Error: plain failure: thrower-annex-c0f9aa04-2a64-4194-9633-00680259c459\n at handler (../fixture/convex/annex/failures.ts:26:23)\n" + } + }, + { + "name": "ordinaryErrorAtClient", + "value": { + "property": null, + "message": "Error: [Request ID: 6d0568ed11aeba52] Server Error\nUncaught Error: Uncaught Error: plain failure: thrower-annex-c0f9aa04-2a64-4194-9633-00680259c459\n at handler (../fixture/convex/annex/failures.ts:26:23)\n\n" + } + }, + { + "name": "throwBoundarySummary", + "value": { + "boundary": "annex", + "caughtDataUnchanged": true, + "passedDataUnchanged": true, + "childRows": 0, + "parentRows": 1, + "plainErrorFromThrower": true, + "writeGuardReached": true + } + } + ] + }, + { + "name": "An ordinary client cannot call a component's function (spec:platform.native-harness.ordinary-client-refused-component-function) > executes the bound example against a fresh world", + "file": "tests/native/ordinary-client-refused-component-function.test.ts", + "result": "passed", + "durationMs": 4491.679666, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "componentSocketSummary", + "value": { + "sends": 3, + "backendCloses": 3, + "answer": "no answer", + "socketWaitMs": 3000 + } + } + ] + }, + { + "name": "native: a component query that calls ctx.auth.getUserIdentity() completes, and its answer is recorded", + "file": "tests/native/component-auth.test.ts", + "result": "passed", + "durationMs": 947.820875, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "identitySeenInsideComponent", + "value": { + "answered": "an identity", + "issuer": "https://fixture-issuer.test", + "subject": "user-1" + } + } + ] + }, + { + "name": "native: a taken --port causes fresh port pairs and the third start succeeds", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 3036.615875, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "88dce2b5bedfc6dfa1ccbcb9cd644cf854f3fedc222a40dfac95b9c411be5abf", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "Probe 5: a subscription to a parent query follows the component's data (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.test.ts", + "result": "passed", + "durationMs": 1144.064292, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "subscriptionSummary", + "value": { + "waitMs": 5000, + "observationAfterDeliveryMs": 100, + "values": [ + { + "id": "j970as4vrct1k8yjnrdg738kyd8fe3zy", + "label": "changed" + } + ], + "error": null + } + } + ] + }, + { + "name": "Probe 5: a page that outgrows its read cap comes back through the parent as a split (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.test.ts", + "result": "passed", + "durationMs": 1097.793667, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "initialPinnedPage", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j97c0cj5sttm4gx0g6xt64k6s58fe9g7", + "j972s92qz45a9cka7zkx8hxvrs8ffmz0", + "j97b3gcvb1qczv4qzvnpf7ewe98ff1wc", + "j979k5tp98cps7mehggbehjsgs8fec60", + "j9790hh9c9pyayst3m2htv9qpn8fe9zv", + "j973pdqe263r7yq1s60wvg98sx8fejyn", + "j97f7zy8erk565xy83814aaxt98fe7yr", + "j972xshbaw4q7hx9ws8yv3x5ad8fen49", + "j97c7t1xw3nre3vvat2m1j5tmx8febys", + "j973dk6f05gryttgndd0v6m9cd8fef6p" + ] + } + }, + { + "name": "splitPage", + "value": { + "rows": 20, + "status": "SplitRequired", + "ids": [ + "j97c0cj5sttm4gx0g6xt64k6s58fe9g7", + "j971hqzjaf1tnzmg3qgwxrcb2n8ffase", + "j97fsmf80bz6j120dgtt1n917x8ffzx2", + "j978z07h5ngzc51g6qvpsswprs8ffjn4", + "j976j99dh5af94jkq6fw9mxsph8ffx0j", + "j974t686cfna3w8nc4k8ng78618few7k", + "j97f6h9f7kaqgtc9mxkx0pebtd8ffqhw", + "j97c5qp7zqfkjsmb4g9vw7xygs8ffv7a", + "j97dwxr8vtcavsexnyhhmhqp518fee8n", + "j9724kmwbzratk5eazpnmwyz9x8fet6j", + "j9710n09n6g5g0n7nw4rte8kyd8ferma", + "j97bqa0gks3t5rvhe2ajycmaxh8fem9b", + "j971rt8dms23a6ekqms7rq1spd8fexz7", + "j97e7c9pg7ed7zqhhywzwyqg9d8fe2tx", + "j975bxwgxxmq8ryrvdkgqx3cd98fe8eg", + "j9760h01xzgmvpvms626rtby1h8ffs28", + "j972s92qz45a9cka7zkx8hxvrs8ffmz0", + "j97b3gcvb1qczv4qzvnpf7ewe98ff1wc", + "j979k5tp98cps7mehggbehjsgs8fec60", + "j9790hh9c9pyayst3m2htv9qpn8fe9zv" + ] + } + }, + { + "name": "relayedSplit", + "value": { + "status": "SplitRequired", + "splitCursor": "[0.5625,1790881137478.338,\"j9724kmwbzratk5eazpnmwyz9x8fet6j\"]", + "continueCursor": "[4,1790881137430.764,\"j9790hh9c9pyayst3m2htv9qpn8fe9zv\"]", + "endCursor": "[9,1790881137430.7651,\"j973dk6f05gryttgndd0v6m9cd8fef6p\"]", + "rows": 20 + } + }, + { + "name": "splitSummary", + "value": { + "pageStatus": "SplitRequired", + "left": [ + "j97c0cj5sttm4gx0g6xt64k6s58fe9g7", + "j971hqzjaf1tnzmg3qgwxrcb2n8ffase", + "j97fsmf80bz6j120dgtt1n917x8ffzx2", + "j978z07h5ngzc51g6qvpsswprs8ffjn4", + "j976j99dh5af94jkq6fw9mxsph8ffx0j", + "j974t686cfna3w8nc4k8ng78618few7k", + "j97f6h9f7kaqgtc9mxkx0pebtd8ffqhw", + "j97c5qp7zqfkjsmb4g9vw7xygs8ffv7a", + "j97dwxr8vtcavsexnyhhmhqp518fee8n", + "j9724kmwbzratk5eazpnmwyz9x8fet6j" + ], + "right": [ + "j9710n09n6g5g0n7nw4rte8kyd8ferma", + "j97bqa0gks3t5rvhe2ajycmaxh8fem9b", + "j971rt8dms23a6ekqms7rq1spd8fexz7", + "j97e7c9pg7ed7zqhhywzwyqg9d8fe2tx", + "j975bxwgxxmq8ryrvdkgqx3cd98fe8eg", + "j9760h01xzgmvpvms626rtby1h8ffs28", + "j972s92qz45a9cka7zkx8hxvrs8ffmz0", + "j97b3gcvb1qczv4qzvnpf7ewe98ff1wc", + "j979k5tp98cps7mehggbehjsgs8fec60", + "j9790hh9c9pyayst3m2htv9qpn8fe9zv", + "j973pdqe263r7yq1s60wvg98sx8fejyn", + "j97f7zy8erk565xy83814aaxt98fe7yr", + "j972xshbaw4q7hx9ws8yv3x5ad8fen49", + "j97c7t1xw3nre3vvat2m1j5tmx8febys", + "j973dk6f05gryttgndd0v6m9cd8fef6p" + ], + "wholeRange": [ + "j97c0cj5sttm4gx0g6xt64k6s58fe9g7", + "j971hqzjaf1tnzmg3qgwxrcb2n8ffase", + "j97fsmf80bz6j120dgtt1n917x8ffzx2", + "j978z07h5ngzc51g6qvpsswprs8ffjn4", + "j976j99dh5af94jkq6fw9mxsph8ffx0j", + "j974t686cfna3w8nc4k8ng78618few7k", + "j97f6h9f7kaqgtc9mxkx0pebtd8ffqhw", + "j97c5qp7zqfkjsmb4g9vw7xygs8ffv7a", + "j97dwxr8vtcavsexnyhhmhqp518fee8n", + "j9724kmwbzratk5eazpnmwyz9x8fet6j", + "j9710n09n6g5g0n7nw4rte8kyd8ferma", + "j97bqa0gks3t5rvhe2ajycmaxh8fem9b", + "j971rt8dms23a6ekqms7rq1spd8fexz7", + "j97e7c9pg7ed7zqhhywzwyqg9d8fe2tx", + "j975bxwgxxmq8ryrvdkgqx3cd98fe8eg", + "j9760h01xzgmvpvms626rtby1h8ffs28", + "j972s92qz45a9cka7zkx8hxvrs8ffmz0", + "j97b3gcvb1qczv4qzvnpf7ewe98ff1wc", + "j979k5tp98cps7mehggbehjsgs8fec60", + "j9790hh9c9pyayst3m2htv9qpn8fe9zv", + "j973pdqe263r7yq1s60wvg98sx8fejyn", + "j97f7zy8erk565xy83814aaxt98fe7yr", + "j972xshbaw4q7hx9ws8yv3x5ad8fen49", + "j97c7t1xw3nre3vvat2m1j5tmx8febys", + "j973dk6f05gryttgndd0v6m9cd8fef6p" + ] + } + } + ] + }, + { + "name": "A token signed by the fixture issuer yields the caller's identity (spec:platform.native-harness.fixture-issuer-token-yields-identity) > executes the bound example against a fresh world", + "file": "tests/native/fixture-issuer-token-yields-identity.test.ts", + "result": "passed", + "durationMs": 936.4521669999999, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 5: pages of a component list stay contiguous under live writes (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.test.ts", + "result": "passed", + "durationMs": 1108.9535420000002, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "initialPinnedPage0", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j97cefgq5aea6g8zjeqtfzdbb18ffxb1", + "j972174da26eqjpjas7v69y8es8ff41j", + "j97113rvsattab0tzxecpc2rb18femfh", + "j971svqd0zw0h0719tg3atxwps8ff1h4", + "j97aqtzf72pz5340qk9aew5eh98fev7g", + "j977nx5sb3rn9s689m45q4khy58feewp", + "j97ddcvvn8namt018v6733zph98ff92c", + "j97efszv9d4tq5hv0kw0s2rqtx8ff087", + "j97f9mwz2fphhv5m5ry8xse5v18fffvw", + "j975kdz0ecdhznz5w1brhy8rvh8feb6q" + ] + } + }, + { + "name": "initialPinnedPage1", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j9723zpr6kpmpme4snq8q3djnh8fem0b", + "j9799wrfgxxc2ahem8ajbgmacd8fergm", + "j974xfzft6zt2ds31q63yw81ad8ff2xh", + "j979mnm639cdwbbpyqay6df3mh8ffy9s", + "j9729cx4cfd69tkve3ptzxqmp98fem49", + "j974ypk2cxyekprm2brvgbwams8fe1v7", + "j974q3w50yvtp8gzp4z739604h8fe64k", + "j972cgv8ds3w95k00snvysrt3x8fedzq", + "j975b8xqse756ygmahc4mxptq98fe44g", + "j973mhbp2hjm0evqc94zd39jx18ffkss" + ] + } + }, + { + "name": "initialPinnedPage2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j979avfpr74ct20gw392psbq318feqq9", + "j9780dgh70ejqd1aean2qrer018fek5d", + "j97ayks6pypr6069n4frc8ebdn8ff8h4", + "j973z6hkee66bnm5gx2swb6n618ff38z", + "j97fw7kgs9e0wmvhgqvtfbgrnh8feg6b", + "j976706x3674yxvkrenw2x3zhh8fex81", + "j97d1f9f1p4mvjfsra3j5r2ted8ff82v", + "j973n0erpwgep7tb1se6q20cqh8fe2t7", + "j9703zcny1fbnnv148t3a54gsh8ffx9s", + "j97a1dh6q7z63wvwdq3193zz9h8fejre" + ] + } + }, + { + "name": "initialPinnedPage3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "initialPinnedPages", + "value": [ + { + "rows": 10, + "isDone": false + }, + { + "rows": 10, + "isDone": false + }, + { + "rows": 10, + "isDone": false + }, + { + "rows": 0, + "isDone": true + } + ] + }, + { + "name": "pinnedPageAfterInsert1", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j9723zpr6kpmpme4snq8q3djnh8fem0b", + "j9799wrfgxxc2ahem8ajbgmacd8fergm", + "j974xfzft6zt2ds31q63yw81ad8ff2xh", + "j979mnm639cdwbbpyqay6df3mh8ffy9s", + "j9729cx4cfd69tkve3ptzxqmp98fem49", + "j974ypk2cxyekprm2brvgbwams8fe1v7", + "j974q3w50yvtp8gzp4z739604h8fe64k", + "j972cgv8ds3w95k00snvysrt3x8fedzq", + "j975b8xqse756ygmahc4mxptq98fe44g", + "j973mhbp2hjm0evqc94zd39jx18ffkss" + ] + } + }, + { + "name": "pinnedPageAfterInsert2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j979avfpr74ct20gw392psbq318feqq9", + "j9780dgh70ejqd1aean2qrer018fek5d", + "j97ayks6pypr6069n4frc8ebdn8ff8h4", + "j973z6hkee66bnm5gx2swb6n618ff38z", + "j97fw7kgs9e0wmvhgqvtfbgrnh8feg6b", + "j976706x3674yxvkrenw2x3zhh8fex81", + "j97d1f9f1p4mvjfsra3j5r2ted8ff82v", + "j973n0erpwgep7tb1se6q20cqh8fe2t7", + "j9703zcny1fbnnv148t3a54gsh8ffx9s", + "j97a1dh6q7z63wvwdq3193zz9h8fejre" + ] + } + }, + { + "name": "pinnedPageAfterInsert3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "pinnedPageAfterInsert0", + "value": { + "rows": 15, + "status": "SplitRecommended", + "ids": [ + "j97cefgq5aea6g8zjeqtfzdbb18ffxb1", + "j976m98rn9dghkekf2j599sjrn8ff2ax", + "j971essvtshr49z1trd0y2magd8ffef6", + "j97cxc2zz4tjtce40dyc6pcr6d8ffegd", + "j972cbzy6dahcxz4gexb692xy98ffnmv", + "j975jyyy3xh92mm44vbrhjf3qn8ffbs1", + "j972174da26eqjpjas7v69y8es8ff41j", + "j97113rvsattab0tzxecpc2rb18femfh", + "j971svqd0zw0h0719tg3atxwps8ff1h4", + "j97aqtzf72pz5340qk9aew5eh98fev7g", + "j977nx5sb3rn9s689m45q4khy58feewp", + "j97ddcvvn8namt018v6733zph98ff92c", + "j97efszv9d4tq5hv0kw0s2rqtx8ff087", + "j97f9mwz2fphhv5m5ry8xse5v18fffvw", + "j975kdz0ecdhznz5w1brhy8rvh8feb6q" + ] + } + }, + { + "name": "pagesAfterInsert", + "value": [ + [ + "j97cefgq5aea6g8zjeqtfzdbb18ffxb1", + "j976m98rn9dghkekf2j599sjrn8ff2ax", + "j971essvtshr49z1trd0y2magd8ffef6", + "j97cxc2zz4tjtce40dyc6pcr6d8ffegd", + "j972cbzy6dahcxz4gexb692xy98ffnmv", + "j975jyyy3xh92mm44vbrhjf3qn8ffbs1", + "j972174da26eqjpjas7v69y8es8ff41j", + "j97113rvsattab0tzxecpc2rb18femfh", + "j971svqd0zw0h0719tg3atxwps8ff1h4", + "j97aqtzf72pz5340qk9aew5eh98fev7g", + "j977nx5sb3rn9s689m45q4khy58feewp", + "j97ddcvvn8namt018v6733zph98ff92c", + "j97efszv9d4tq5hv0kw0s2rqtx8ff087", + "j97f9mwz2fphhv5m5ry8xse5v18fffvw", + "j975kdz0ecdhznz5w1brhy8rvh8feb6q" + ], + [ + "j9723zpr6kpmpme4snq8q3djnh8fem0b", + "j9799wrfgxxc2ahem8ajbgmacd8fergm", + "j974xfzft6zt2ds31q63yw81ad8ff2xh", + "j979mnm639cdwbbpyqay6df3mh8ffy9s", + "j9729cx4cfd69tkve3ptzxqmp98fem49", + "j974ypk2cxyekprm2brvgbwams8fe1v7", + "j974q3w50yvtp8gzp4z739604h8fe64k", + "j972cgv8ds3w95k00snvysrt3x8fedzq", + "j975b8xqse756ygmahc4mxptq98fe44g", + "j973mhbp2hjm0evqc94zd39jx18ffkss" + ], + [ + "j979avfpr74ct20gw392psbq318feqq9", + "j9780dgh70ejqd1aean2qrer018fek5d", + "j97ayks6pypr6069n4frc8ebdn8ff8h4", + "j973z6hkee66bnm5gx2swb6n618ff38z", + "j97fw7kgs9e0wmvhgqvtfbgrnh8feg6b", + "j976706x3674yxvkrenw2x3zhh8fex81", + "j97d1f9f1p4mvjfsra3j5r2ted8ff82v", + "j973n0erpwgep7tb1se6q20cqh8fe2t7", + "j9703zcny1fbnnv148t3a54gsh8ffx9s", + "j97a1dh6q7z63wvwdq3193zz9h8fejre" + ], + [] + ] + }, + { + "name": "contiguityAfterInsert", + "value": { + "actual": [ + "j97cefgq5aea6g8zjeqtfzdbb18ffxb1", + "j976m98rn9dghkekf2j599sjrn8ff2ax", + "j971essvtshr49z1trd0y2magd8ffef6", + "j97cxc2zz4tjtce40dyc6pcr6d8ffegd", + "j972cbzy6dahcxz4gexb692xy98ffnmv", + "j975jyyy3xh92mm44vbrhjf3qn8ffbs1", + "j972174da26eqjpjas7v69y8es8ff41j", + "j97113rvsattab0tzxecpc2rb18femfh", + "j971svqd0zw0h0719tg3atxwps8ff1h4", + "j97aqtzf72pz5340qk9aew5eh98fev7g", + "j977nx5sb3rn9s689m45q4khy58feewp", + "j97ddcvvn8namt018v6733zph98ff92c", + "j97efszv9d4tq5hv0kw0s2rqtx8ff087", + "j97f9mwz2fphhv5m5ry8xse5v18fffvw", + "j975kdz0ecdhznz5w1brhy8rvh8feb6q", + "j9723zpr6kpmpme4snq8q3djnh8fem0b", + "j9799wrfgxxc2ahem8ajbgmacd8fergm", + "j974xfzft6zt2ds31q63yw81ad8ff2xh", + "j979mnm639cdwbbpyqay6df3mh8ffy9s", + "j9729cx4cfd69tkve3ptzxqmp98fem49", + "j974ypk2cxyekprm2brvgbwams8fe1v7", + "j974q3w50yvtp8gzp4z739604h8fe64k", + "j972cgv8ds3w95k00snvysrt3x8fedzq", + "j975b8xqse756ygmahc4mxptq98fe44g", + "j973mhbp2hjm0evqc94zd39jx18ffkss", + "j979avfpr74ct20gw392psbq318feqq9", + "j9780dgh70ejqd1aean2qrer018fek5d", + "j97ayks6pypr6069n4frc8ebdn8ff8h4", + "j973z6hkee66bnm5gx2swb6n618ff38z", + "j97fw7kgs9e0wmvhgqvtfbgrnh8feg6b", + "j976706x3674yxvkrenw2x3zhh8fex81", + "j97d1f9f1p4mvjfsra3j5r2ted8ff82v", + "j973n0erpwgep7tb1se6q20cqh8fe2t7", + "j9703zcny1fbnnv148t3a54gsh8ffx9s", + "j97a1dh6q7z63wvwdq3193zz9h8fejre" + ], + "expected": [ + "j97cefgq5aea6g8zjeqtfzdbb18ffxb1", + "j976m98rn9dghkekf2j599sjrn8ff2ax", + "j971essvtshr49z1trd0y2magd8ffef6", + "j97cxc2zz4tjtce40dyc6pcr6d8ffegd", + "j972cbzy6dahcxz4gexb692xy98ffnmv", + "j975jyyy3xh92mm44vbrhjf3qn8ffbs1", + "j972174da26eqjpjas7v69y8es8ff41j", + "j97113rvsattab0tzxecpc2rb18femfh", + "j971svqd0zw0h0719tg3atxwps8ff1h4", + "j97aqtzf72pz5340qk9aew5eh98fev7g", + "j977nx5sb3rn9s689m45q4khy58feewp", + "j97ddcvvn8namt018v6733zph98ff92c", + "j97efszv9d4tq5hv0kw0s2rqtx8ff087", + "j97f9mwz2fphhv5m5ry8xse5v18fffvw", + "j975kdz0ecdhznz5w1brhy8rvh8feb6q", + "j9723zpr6kpmpme4snq8q3djnh8fem0b", + "j9799wrfgxxc2ahem8ajbgmacd8fergm", + "j974xfzft6zt2ds31q63yw81ad8ff2xh", + "j979mnm639cdwbbpyqay6df3mh8ffy9s", + "j9729cx4cfd69tkve3ptzxqmp98fem49", + "j974ypk2cxyekprm2brvgbwams8fe1v7", + "j974q3w50yvtp8gzp4z739604h8fe64k", + "j972cgv8ds3w95k00snvysrt3x8fedzq", + "j975b8xqse756ygmahc4mxptq98fe44g", + "j973mhbp2hjm0evqc94zd39jx18ffkss", + "j979avfpr74ct20gw392psbq318feqq9", + "j9780dgh70ejqd1aean2qrer018fek5d", + "j97ayks6pypr6069n4frc8ebdn8ff8h4", + "j973z6hkee66bnm5gx2swb6n618ff38z", + "j97fw7kgs9e0wmvhgqvtfbgrnh8feg6b", + "j976706x3674yxvkrenw2x3zhh8fex81", + "j97d1f9f1p4mvjfsra3j5r2ted8ff82v", + "j973n0erpwgep7tb1se6q20cqh8fe2t7", + "j9703zcny1fbnnv148t3a54gsh8ffx9s", + "j97a1dh6q7z63wvwdq3193zz9h8fejre" + ] + } + }, + { + "name": "pinnedPageAfterDelete0", + "value": { + "rows": 15, + "status": "SplitRecommended", + "ids": [ + "j97cefgq5aea6g8zjeqtfzdbb18ffxb1", + "j976m98rn9dghkekf2j599sjrn8ff2ax", + "j971essvtshr49z1trd0y2magd8ffef6", + "j97cxc2zz4tjtce40dyc6pcr6d8ffegd", + "j972cbzy6dahcxz4gexb692xy98ffnmv", + "j975jyyy3xh92mm44vbrhjf3qn8ffbs1", + "j972174da26eqjpjas7v69y8es8ff41j", + "j97113rvsattab0tzxecpc2rb18femfh", + "j971svqd0zw0h0719tg3atxwps8ff1h4", + "j97aqtzf72pz5340qk9aew5eh98fev7g", + "j977nx5sb3rn9s689m45q4khy58feewp", + "j97ddcvvn8namt018v6733zph98ff92c", + "j97efszv9d4tq5hv0kw0s2rqtx8ff087", + "j97f9mwz2fphhv5m5ry8xse5v18fffvw", + "j975kdz0ecdhznz5w1brhy8rvh8feb6q" + ] + } + }, + { + "name": "pinnedPageAfterDelete2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j979avfpr74ct20gw392psbq318feqq9", + "j9780dgh70ejqd1aean2qrer018fek5d", + "j97ayks6pypr6069n4frc8ebdn8ff8h4", + "j973z6hkee66bnm5gx2swb6n618ff38z", + "j97fw7kgs9e0wmvhgqvtfbgrnh8feg6b", + "j976706x3674yxvkrenw2x3zhh8fex81", + "j97d1f9f1p4mvjfsra3j5r2ted8ff82v", + "j973n0erpwgep7tb1se6q20cqh8fe2t7", + "j9703zcny1fbnnv148t3a54gsh8ffx9s", + "j97a1dh6q7z63wvwdq3193zz9h8fejre" + ] + } + }, + { + "name": "pinnedPageAfterDelete3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "pinnedPageAfterDelete1", + "value": { + "rows": 9, + "status": null, + "ids": [ + "j9723zpr6kpmpme4snq8q3djnh8fem0b", + "j974xfzft6zt2ds31q63yw81ad8ff2xh", + "j979mnm639cdwbbpyqay6df3mh8ffy9s", + "j9729cx4cfd69tkve3ptzxqmp98fem49", + "j974ypk2cxyekprm2brvgbwams8fe1v7", + "j974q3w50yvtp8gzp4z739604h8fe64k", + "j972cgv8ds3w95k00snvysrt3x8fedzq", + "j975b8xqse756ygmahc4mxptq98fe44g", + "j973mhbp2hjm0evqc94zd39jx18ffkss" + ] + } + }, + { + "name": "contiguitySummary", + "value": { + "pages": 4, + "rowsAfterInsert": 35, + "rowsAfterDelete": 34, + "removed": "j9799wrfgxxc2ahem8ajbgmacd8fergm", + "actual": [ + "j97cefgq5aea6g8zjeqtfzdbb18ffxb1", + "j976m98rn9dghkekf2j599sjrn8ff2ax", + "j971essvtshr49z1trd0y2magd8ffef6", + "j97cxc2zz4tjtce40dyc6pcr6d8ffegd", + "j972cbzy6dahcxz4gexb692xy98ffnmv", + "j975jyyy3xh92mm44vbrhjf3qn8ffbs1", + "j972174da26eqjpjas7v69y8es8ff41j", + "j97113rvsattab0tzxecpc2rb18femfh", + "j971svqd0zw0h0719tg3atxwps8ff1h4", + "j97aqtzf72pz5340qk9aew5eh98fev7g", + "j977nx5sb3rn9s689m45q4khy58feewp", + "j97ddcvvn8namt018v6733zph98ff92c", + "j97efszv9d4tq5hv0kw0s2rqtx8ff087", + "j97f9mwz2fphhv5m5ry8xse5v18fffvw", + "j975kdz0ecdhznz5w1brhy8rvh8feb6q", + "j9723zpr6kpmpme4snq8q3djnh8fem0b", + "j974xfzft6zt2ds31q63yw81ad8ff2xh", + "j979mnm639cdwbbpyqay6df3mh8ffy9s", + "j9729cx4cfd69tkve3ptzxqmp98fem49", + "j974ypk2cxyekprm2brvgbwams8fe1v7", + "j974q3w50yvtp8gzp4z739604h8fe64k", + "j972cgv8ds3w95k00snvysrt3x8fedzq", + "j975b8xqse756ygmahc4mxptq98fe44g", + "j973mhbp2hjm0evqc94zd39jx18ffkss", + "j979avfpr74ct20gw392psbq318feqq9", + "j9780dgh70ejqd1aean2qrer018fek5d", + "j97ayks6pypr6069n4frc8ebdn8ff8h4", + "j973z6hkee66bnm5gx2swb6n618ff38z", + "j97fw7kgs9e0wmvhgqvtfbgrnh8feg6b", + "j976706x3674yxvkrenw2x3zhh8fex81", + "j97d1f9f1p4mvjfsra3j5r2ted8ff82v", + "j973n0erpwgep7tb1se6q20cqh8fe2t7", + "j9703zcny1fbnnv148t3a54gsh8ffx9s", + "j97a1dh6q7z63wvwdq3193zz9h8fejre" + ], + "expected": [ + "j97cefgq5aea6g8zjeqtfzdbb18ffxb1", + "j976m98rn9dghkekf2j599sjrn8ff2ax", + "j971essvtshr49z1trd0y2magd8ffef6", + "j97cxc2zz4tjtce40dyc6pcr6d8ffegd", + "j972cbzy6dahcxz4gexb692xy98ffnmv", + "j975jyyy3xh92mm44vbrhjf3qn8ffbs1", + "j972174da26eqjpjas7v69y8es8ff41j", + "j97113rvsattab0tzxecpc2rb18femfh", + "j971svqd0zw0h0719tg3atxwps8ff1h4", + "j97aqtzf72pz5340qk9aew5eh98fev7g", + "j977nx5sb3rn9s689m45q4khy58feewp", + "j97ddcvvn8namt018v6733zph98ff92c", + "j97efszv9d4tq5hv0kw0s2rqtx8ff087", + "j97f9mwz2fphhv5m5ry8xse5v18fffvw", + "j975kdz0ecdhznz5w1brhy8rvh8feb6q", + "j9723zpr6kpmpme4snq8q3djnh8fem0b", + "j974xfzft6zt2ds31q63yw81ad8ff2xh", + "j979mnm639cdwbbpyqay6df3mh8ffy9s", + "j9729cx4cfd69tkve3ptzxqmp98fem49", + "j974ypk2cxyekprm2brvgbwams8fe1v7", + "j974q3w50yvtp8gzp4z739604h8fe64k", + "j972cgv8ds3w95k00snvysrt3x8fedzq", + "j975b8xqse756ygmahc4mxptq98fe44g", + "j973mhbp2hjm0evqc94zd39jx18ffkss", + "j979avfpr74ct20gw392psbq318feqq9", + "j9780dgh70ejqd1aean2qrer018fek5d", + "j97ayks6pypr6069n4frc8ebdn8ff8h4", + "j973z6hkee66bnm5gx2swb6n618ff38z", + "j97fw7kgs9e0wmvhgqvtfbgrnh8feg6b", + "j976706x3674yxvkrenw2x3zhh8fex81", + "j97d1f9f1p4mvjfsra3j5r2ted8ff82v", + "j973n0erpwgep7tb1se6q20cqh8fe2t7", + "j9703zcny1fbnnv148t3a54gsh8ffx9s", + "j97a1dh6q7z63wvwdq3193zz9h8fejre" + ] + } + } + ] + }, + { + "name": "native: a taken --site-proxy-port causes fresh port pairs and the third start succeeds", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 1115.15575, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "c66f40897f83a65c46217580902894faa0d4ed9502785cfdf3cb0515d76c01a8", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "The admin key acting as an identity reaches an internal function (spec:platform.native-harness.admin-key-acting-as-identity-reaches-internal-function) > executes the bound example against a fresh world", + "file": "tests/native/admin-key-acting-as-identity-reaches-internal-function.test.ts", + "result": "passed", + "durationMs": 877.906542, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: the parent sweep verifies and kills two real owned backend processes", + "file": "tests/native/harness-sweep.test.ts", + "result": "passed", + "durationMs": 306.362875, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + }, + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native review: failed inspection must not silently discard ownership", + "file": "tests/native/harness-sweep.test.ts", + "result": "passed", + "durationMs": 155.35133299999995, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: three taken ports fail clearly and remove the failed start's storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 3789.479708, + "errors": [], + "backends": [], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "Probe 1: a backend restart with a mutation in flight (spec:facts.f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.test.ts", + "result": "passed", + "durationMs": 12411.142667, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "trials", + "value": [ + { + "trial": "restart 0", + "delayMs": 0, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 1", + "delayMs": 0, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 2", + "delayMs": 0, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 3", + "delayMs": 1, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 4", + "delayMs": 2, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 5", + "delayMs": 4, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 6", + "delayMs": 8, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 7", + "delayMs": 16, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 8", + "delayMs": 32, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 9", + "delayMs": 64, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + } + ] + }, + { + "name": "restartSummary", + "value": { + "trials": 10, + "resolvedAfterRestart": 10, + "resolvedBeforeRelease": 0, + "killedBeforeCommit": 6, + "killedAfterCommit": 4, + "rowCounts": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + } + ] + }, + { + "name": "native: stop escalates to SIGKILL when the owned process ignores SIGINT and keeps its storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 5341.831709, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "038831accc179db753c2c5ed68bf1487eaefc0e51743bb686a13cb2630a160fa", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: a log mark from before a kill cannot be read after restart", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 798.4422080000004, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 4: a nested call draws on the caller's transaction limits (spec:facts.f13-transactions-have-limits.probe-4-nested-call-shares-limits) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-nested-call-shares-limits.test.ts", + "result": "passed", + "durationMs": 21877.047791999998, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "seededPayloads", + "value": { + "size": 921600, + "parentCount": 10, + "childCount": 10 + } + }, + { + "name": "combinedReadOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 5b8101e6e1c3cc48] Server Error\nUncaught Error: Uncaught Error: Too many bytes read in a single function execution (limit: 16777216 bytes). Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\n at async handler (../fixture/convex/limits.ts:47:11)\n\n at async handler (../fixture/convex/limits.ts:78:34)\n" + } + }, + { + "name": "readTogether", + "value": "fails on the bytes-read limit" + }, + { + "name": "parentReadOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childReadOutcome", + "value": { + "outcome": "returns", + "error": null + } + }, + { + "name": "readsAlone", + "value": { + "parent": "commits", + "child": "returns", + "parentDocuments": 10, + "childDocuments": 10, + "childPayloadBytes": 9216000 + } + }, + { + "name": "sharedReadMetrics", + "value": { + "byteGrowth": 921684, + "documentGrowth": 1, + "childBytes": 921684, + "childDocuments": 1, + "payloadBytes": 921600 + } + }, + { + "name": "combinedWriteOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: b43ab5a2b447b5ee] Server Error\nUncaught Error: Uncaught Error: Too many bytes written in a single function execution (limit: 16 MiB)\n at async handler (../fixture/convex/limits.ts:23:35)\n\n at async handler (../fixture/convex/limits.ts:102:8)\n" + } + }, + { + "name": "writesTogether", + "value": "fails on the bytes-written limit" + }, + { + "name": "failedWriteRows", + "value": 0 + }, + { + "name": "parentWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "parentWriteRows", + "value": 10 + }, + { + "name": "childWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childWriteRows", + "value": 20 + }, + { + "name": "limitSummary", + "value": { + "boundary": "nested", + "combinedRead": "fails on the bytes-read limit", + "combinedWrite": "fails on the bytes-written limit", + "failedWriteRows": 0, + "committedRows": 20 + } + } + ] + }, + { + "name": "Probe 4: a component call draws on the caller's transaction limits (spec:facts.f13-transactions-have-limits.probe-4-component-call-shares-limits) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-component-call-shares-limits.test.ts", + "result": "passed", + "durationMs": 22473.841875000002, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "seededPayloads", + "value": { + "size": 921600, + "parentCount": 10, + "childCount": 10 + } + }, + { + "name": "combinedReadOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 693f7d29af4bfb55] Server Error\nUncaught Error: Uncaught Error: Too many bytes read in a single function execution (limit: 16777216 bytes). Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\n at async handler (../fixture/convex/annex/limits.ts:34:11)\n\n at async handler (../fixture/convex/limits.ts:78:34)\n" + } + }, + { + "name": "readTogether", + "value": "fails on the bytes-read limit" + }, + { + "name": "parentReadOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childReadOutcome", + "value": { + "outcome": "returns", + "error": null + } + }, + { + "name": "readsAlone", + "value": { + "parent": "commits", + "child": "returns", + "parentDocuments": 10, + "childDocuments": 10, + "childPayloadBytes": 9216000 + } + }, + { + "name": "sharedReadMetrics", + "value": { + "byteGrowth": 921684, + "documentGrowth": 1, + "childBytes": 921684, + "childDocuments": 1, + "payloadBytes": 921600 + } + }, + { + "name": "combinedWriteOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 7d31d2d9d0a70287] Server Error\nUncaught Error: Uncaught Error: Too many bytes written in a single function execution (limit: 16 MiB)\n at async handler (../fixture/convex/annex/limits.ts:8:35)\n\n at async handler (../fixture/convex/limits.ts:102:8)\n" + } + }, + { + "name": "writesTogether", + "value": "fails on the bytes-written limit" + }, + { + "name": "failedWriteRows", + "value": 0 + }, + { + "name": "parentWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "parentWriteRows", + "value": 10 + }, + { + "name": "childWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childWriteRows", + "value": 20 + }, + { + "name": "limitSummary", + "value": { + "boundary": "annex", + "combinedRead": "fails on the bytes-read limit", + "combinedWrite": "fails on the bytes-written limit", + "failedWriteRows": 0, + "committedRows": 20 + } + } + ] + }, + { + "name": "native: a restart that stalls after spawning reaps its process", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 30340.660542, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "3d106c81f25ed282641a51610bd0918f05500daf7c8798c7b8ff1bfb803cac99", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: codegen interrupted by SIGINT removes its starting backend and storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 374.1884579999969, + "errors": [], + "backends": [], + "measurements": [] + }, + { + "name": "native: codegen interrupted by SIGTERM removes its starting backend and storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 295.0494169999947, + "errors": [], + "backends": [], + "measurements": [] + }, + { + "name": "native: backend output split across chunks is redacted before the rolling buffer clips it", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 369.5974580000038, + "errors": [], + "backends": [], + "measurements": [] + } + ], + "unhandledErrors": [] +} diff --git a/evidence/native-20261001T193002Z-5c8e616-34093e99-c2af-4f91-a42b-fa7ac5b413fc.json b/evidence/native-20261001T193002Z-5c8e616-34093e99-c2af-4f91-a42b-fa7ac5b413fc.json new file mode 100644 index 0000000..a34791e --- /dev/null +++ b/evidence/native-20261001T193002Z-5c8e616-34093e99-c2af-4f91-a42b-fa7ac5b413fc.json @@ -0,0 +1,3458 @@ +{ + "tier": "native", + "commit": "5c8e6162508dadee12c1c895786f5b76bfbc6e19", + "clean": true, + "command": "vitest run --project native", + "startedAt": "2026-10-01T19:30:02.306Z", + "finishedAt": "2026-10-01T19:30:50.612Z", + "result": "passed", + "versions": { + "node": "v24.21.0", + "convex": "1.46.0", + "convex-helpers": "0.1.124", + "convex-test": "0.0.60", + "vitest": "5.0.3", + "typescript": "7.0.2", + "react": "19.3.0", + "happy-dom": "20.14.5" + }, + "tests": [ + { + "name": "Probe 4: how deep nested calls may go (spec:facts.f13-transactions-have-limits.probe-4-nesting-depth) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-nesting-depth.test.ts", + "result": "passed", + "durationMs": 1803.7860409999998, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "stack1Trial", + "value": { + "stack": 1, + "answer": 1, + "error": null, + "rows": 1 + } + }, + { + "name": "stack2Trial", + "value": { + "stack": 2, + "answer": 2, + "error": null, + "rows": 2 + } + }, + { + "name": "stack3Trial", + "value": { + "stack": 3, + "answer": 3, + "error": null, + "rows": 3 + } + }, + { + "name": "stack4Trial", + "value": { + "stack": 4, + "answer": 4, + "error": null, + "rows": 4 + } + }, + { + "name": "stack5Trial", + "value": { + "stack": 5, + "answer": 5, + "error": null, + "rows": 5 + } + }, + { + "name": "stack6Trial", + "value": { + "stack": 6, + "answer": 6, + "error": null, + "rows": 6 + } + }, + { + "name": "stack7Trial", + "value": { + "stack": 7, + "answer": 7, + "error": null, + "rows": 7 + } + }, + { + "name": "stack8Trial", + "value": { + "stack": 8, + "answer": 8, + "error": null, + "rows": 8 + } + }, + { + "name": "stack9Trial", + "value": { + "stack": 9, + "answer": 9, + "error": null, + "rows": 9 + } + }, + { + "name": "stack10Trial", + "value": { + "stack": 10, + "answer": null, + "error": "Error: [Request ID: 22a9fce17b5328fe] Server Error\nUncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Cross component call depth limit exceeded. Do you have an infinite loop in your app?\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n", + "rows": 0 + } + }, + { + "name": "depthBoundary", + "value": { + "deepest": 9, + "firstFailure": 10, + "refusal": "Error: [Request ID: 22a9fce17b5328fe] Server Error\nUncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Cross component call depth limit exceeded. Do you have an infinite loop in your app?\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n" + } + } + ] + }, + { + "name": "native: stop and kill keep stored rows across restart, and dispose removes storage twice", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 1973.641958, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://changed-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 1: a client closed with a pending mutation (spec:facts.f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.test.ts", + "result": "passed", + "durationMs": 2442.534958, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "trials", + "value": [ + { + "trial": "closed 0", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 1", + "gapMs": 1, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 2", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 3", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 4", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 5", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 6", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 7", + "gapMs": 1, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 8", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 9", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 10", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 11", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 12", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 13", + "gapMs": 1, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 14", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 15", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 16", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 17", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 18", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 19", + "gapMs": 1, + "confirmed": false, + "rows": 0 + } + ] + }, + { + "name": "closedClientSummary", + "value": { + "trials": 20, + "confirmed": 0, + "noRow": 7, + "oneRow": 13 + } + } + ] + }, + { + "name": "Probe 3: the cost of a component call from a mutation (spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.test.ts", + "result": "passed", + "durationMs": 3111.033166, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "costSamples", + "value": [ + { + "round": 0, + "path": "helper", + "cacheBuster": 1000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.005172542, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.005172542, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "nested", + "cacheBuster": 2000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.174677708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.174677708, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "component", + "cacheBuster": 3000, + "result": { + "documentsRead": 50, + "id": "jn707xh0g41a2s5gaqp7289wa58ffpwb" + }, + "callError": null, + "executionTimeSeconds": 0.190583042, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.190583042, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "helper", + "cacheBuster": 4000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.003042542, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.003042542, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "nested", + "cacheBuster": 5000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.075134667, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.075134667, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "component", + "cacheBuster": 6000, + "result": { + "documentsRead": 50, + "id": "jn707xh0g41a2s5gaqp7289wa58ffpwb" + }, + "callError": null, + "executionTimeSeconds": 0.097401167, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.097401167, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "helper", + "cacheBuster": 7000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.002071458, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002071458, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "nested", + "cacheBuster": 8000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.054287958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.054287958, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "component", + "cacheBuster": 9000, + "result": { + "documentsRead": 50, + "id": "jn707xh0g41a2s5gaqp7289wa58ffpwb" + }, + "callError": null, + "executionTimeSeconds": 0.103319, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.103319, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "helper", + "cacheBuster": 10000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.00328375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.00328375, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "nested", + "cacheBuster": 11000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.076133416, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.076133416, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "component", + "cacheBuster": 12000, + "result": { + "documentsRead": 50, + "id": "jn707xh0g41a2s5gaqp7289wa58ffpwb" + }, + "callError": null, + "executionTimeSeconds": 0.111397583, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.111397583, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "helper", + "cacheBuster": 13000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.002263416, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002263416, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "nested", + "cacheBuster": 14000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.055716417, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.055716417, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "component", + "cacheBuster": 15000, + "result": { + "documentsRead": 50, + "id": "jn707xh0g41a2s5gaqp7289wa58ffpwb" + }, + "callError": null, + "executionTimeSeconds": 0.076372959, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.076372959, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "helper", + "cacheBuster": 16000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.003426209, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.003426209, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "nested", + "cacheBuster": 17000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.053565542, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.053565542, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "component", + "cacheBuster": 18000, + "result": { + "documentsRead": 50, + "id": "jn707xh0g41a2s5gaqp7289wa58ffpwb" + }, + "callError": null, + "executionTimeSeconds": 0.095136375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.095136375, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "helper", + "cacheBuster": 19000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.002920959, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002920959, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "nested", + "cacheBuster": 20000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.056550125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.056550125, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "component", + "cacheBuster": 21000, + "result": { + "documentsRead": 50, + "id": "jn707xh0g41a2s5gaqp7289wa58ffpwb" + }, + "callError": null, + "executionTimeSeconds": 0.11463225, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.11463225, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "helper", + "cacheBuster": 22000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.002355458, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002355458, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "nested", + "cacheBuster": 23000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.064747125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.064747125, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "component", + "cacheBuster": 24000, + "result": { + "documentsRead": 50, + "id": "jn707xh0g41a2s5gaqp7289wa58ffpwb" + }, + "callError": null, + "executionTimeSeconds": 0.090297959, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.090297959, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "helper", + "cacheBuster": 25000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.002131708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002131708, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "nested", + "cacheBuster": 26000, + "result": { + "documentsRead": 50, + "id": "jx70d9gh5emfpmj1bnbqqn31vn8ff2sa" + }, + "callError": null, + "executionTimeSeconds": 0.056460166, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.056460166, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "component", + "cacheBuster": 27000, + "result": { + "documentsRead": 50, + "id": "jn707xh0g41a2s5gaqp7289wa58ffpwb" + }, + "callError": null, + "executionTimeSeconds": 0.079940459, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.079940459, + "cachedResult": false + } + ] + } + ] + }, + { + "name": "readCostSummary", + "value": { + "kind": "mutation", + "readsPerSample": 50, + "acceptedSamples": { + "helper": 9, + "nested": 9, + "component": 9 + }, + "componentCompletionRecords": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "helper": 0.002920959, + "nested": 0.056550125, + "component": 0.097401167, + "componentMinusHelperSeconds": 0.094480208, + "componentMinusHelperPerReadSeconds": 0.0018896041599999998 + } + } + ] + }, + { + "name": "Probe 2: ConvexError data through a nested mutation (spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.test.ts", + "result": "passed", + "durationMs": 925.608708, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "catchingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + } + } + }, + { + "name": "rowsAfterCatching", + "value": { + "child": 0, + "parent": 1 + } + }, + { + "name": "passingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + }, + "text": "ConvexError: [Request ID: d1b7317e2faaa8a3] Server Error\nUncaught ConvexError: Uncaught ConvexError: {\"array\":[\"one\",2,false,null],\"boolean\":true,\"code\":\"staleVersion\",\"int64\":\"9007199254740993n\",\"nested\":{\"count\":3,\"label\":\"nested\"},\"nil\":null,\"number\":17.25}\n at handler (../fixture/convex/failures.ts:21:11)\n\n" + } + }, + { + "name": "ordinaryErrorAtParent", + "value": { + "property": null, + "message": "Error: Uncaught Error: plain failure: thrower-nested-97480696-2012-45e1-91cd-5cdd61150a5d\n at handler (../fixture/convex/failures.ts:39:23)\n" + } + }, + { + "name": "ordinaryErrorAtClient", + "value": { + "property": null, + "message": "Error: [Request ID: 228d595bb30bdc58] Server Error\nUncaught Error: Uncaught Error: plain failure: thrower-nested-97480696-2012-45e1-91cd-5cdd61150a5d\n at handler (../fixture/convex/failures.ts:39:23)\n\n" + } + }, + { + "name": "throwBoundarySummary", + "value": { + "boundary": "nested", + "caughtDataUnchanged": true, + "passedDataUnchanged": true, + "childRows": 0, + "parentRows": 1, + "plainErrorFromThrower": true, + "writeGuardReached": true + } + } + ] + }, + { + "name": "Probe 3: the cost of a component call from a query (spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query) > executes the bound example against a fresh world", + "file": "tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.test.ts", + "result": "passed", + "durationMs": 3201.291834, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "costSamples", + "value": [ + { + "round": 0, + "path": "helper", + "cacheBuster": 1000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.003960209, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.003960209, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "nested", + "cacheBuster": 2000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.072264458, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.072264458, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "component", + "cacheBuster": 3000, + "result": { + "documentsRead": 50, + "id": "jn7c285erqmz3bxhmhk8b1zq0s8ffjx3" + }, + "callError": null, + "executionTimeSeconds": 0.124968708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.124968708, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "helper", + "cacheBuster": 4000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.002588084, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002588084, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "nested", + "cacheBuster": 5000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.057443334, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.057443334, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "component", + "cacheBuster": 6000, + "result": { + "documentsRead": 50, + "id": "jn7c285erqmz3bxhmhk8b1zq0s8ffjx3" + }, + "callError": null, + "executionTimeSeconds": 0.085630375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.085630375, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "helper", + "cacheBuster": 7000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.002631959, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002631959, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "nested", + "cacheBuster": 8000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.060351791, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.060351791, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "component", + "cacheBuster": 9000, + "result": { + "documentsRead": 50, + "id": "jn7c285erqmz3bxhmhk8b1zq0s8ffjx3" + }, + "callError": null, + "executionTimeSeconds": 0.126857666, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.126857666, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "helper", + "cacheBuster": 10000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.004402375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.004402375, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "nested", + "cacheBuster": 11000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.063041167, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.063041167, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "component", + "cacheBuster": 12000, + "result": { + "documentsRead": 50, + "id": "jn7c285erqmz3bxhmhk8b1zq0s8ffjx3" + }, + "callError": null, + "executionTimeSeconds": 0.080074541, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.080074541, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "helper", + "cacheBuster": 13000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.004291375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.004291375, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "nested", + "cacheBuster": 14000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.049639584, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.049639584, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "component", + "cacheBuster": 15000, + "result": { + "documentsRead": 50, + "id": "jn7c285erqmz3bxhmhk8b1zq0s8ffjx3" + }, + "callError": null, + "executionTimeSeconds": 0.089845958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.089845958, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "helper", + "cacheBuster": 16000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.002376791, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002376791, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "nested", + "cacheBuster": 17000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.059472417, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.059472417, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "component", + "cacheBuster": 18000, + "result": { + "documentsRead": 50, + "id": "jn7c285erqmz3bxhmhk8b1zq0s8ffjx3" + }, + "callError": null, + "executionTimeSeconds": 0.093615541, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.093615541, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "helper", + "cacheBuster": 19000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.002732709, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002732709, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "nested", + "cacheBuster": 20000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.082036833, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.082036833, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "component", + "cacheBuster": 21000, + "result": { + "documentsRead": 50, + "id": "jn7c285erqmz3bxhmhk8b1zq0s8ffjx3" + }, + "callError": null, + "executionTimeSeconds": 0.097814167, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.097814167, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "helper", + "cacheBuster": 22000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.002858208, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002858208, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "nested", + "cacheBuster": 23000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.058263958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.058263958, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "component", + "cacheBuster": 24000, + "result": { + "documentsRead": 50, + "id": "jn7c285erqmz3bxhmhk8b1zq0s8ffjx3" + }, + "callError": null, + "executionTimeSeconds": 0.087744916, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.087744916, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "helper", + "cacheBuster": 25000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.003143333, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.003143333, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "nested", + "cacheBuster": 26000, + "result": { + "documentsRead": 50, + "id": "jx71f5s02qs2wrsn65zm1gcngh8ff4dg" + }, + "callError": null, + "executionTimeSeconds": 0.052509583, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.052509583, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "component", + "cacheBuster": 27000, + "result": { + "documentsRead": 50, + "id": "jn7c285erqmz3bxhmhk8b1zq0s8ffjx3" + }, + "callError": null, + "executionTimeSeconds": 0.075860333, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.075860333, + "cachedResult": false + } + ] + } + ] + }, + { + "name": "readCostSummary", + "value": { + "kind": "query", + "readsPerSample": 50, + "acceptedSamples": { + "helper": 9, + "nested": 9, + "component": 9 + }, + "componentCompletionRecords": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "helper": 0.002858208, + "nested": 0.059472417, + "component": 0.089845958, + "componentMinusHelperSeconds": 0.08698775, + "componentMinusHelperPerReadSeconds": 0.001739755 + } + } + ] + }, + { + "name": "Probe 2: ConvexError data through a component boundary (spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary) > executes the bound example against a fresh world", + "file": "tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.test.ts", + "result": "passed", + "durationMs": 912.0964160000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "catchingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + } + } + }, + { + "name": "rowsAfterCatching", + "value": { + "child": 0, + "parent": 1 + } + }, + { + "name": "passingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + }, + "text": "ConvexError: [Request ID: dd39595886e9b33a] Server Error\nUncaught ConvexError: Uncaught ConvexError: {\"array\":[\"one\",2,false,null],\"boolean\":true,\"code\":\"staleVersion\",\"int64\":\"9007199254740993n\",\"nested\":{\"count\":3,\"label\":\"nested\"},\"nil\":null,\"number\":17.25}\n at handler (../fixture/convex/annex/failures.ts:8:11)\n\n" + } + }, + { + "name": "ordinaryErrorAtParent", + "value": { + "property": null, + "message": "Error: Uncaught Error: plain failure: thrower-annex-762e02c4-f350-4580-852b-af955f0ac5d2\n at handler (../fixture/convex/annex/failures.ts:26:23)\n" + } + }, + { + "name": "ordinaryErrorAtClient", + "value": { + "property": null, + "message": "Error: [Request ID: 3896df6fdef3e2ff] Server Error\nUncaught Error: Uncaught Error: plain failure: thrower-annex-762e02c4-f350-4580-852b-af955f0ac5d2\n at handler (../fixture/convex/annex/failures.ts:26:23)\n\n" + } + }, + { + "name": "throwBoundarySummary", + "value": { + "boundary": "annex", + "caughtDataUnchanged": true, + "passedDataUnchanged": true, + "childRows": 0, + "parentRows": 1, + "plainErrorFromThrower": true, + "writeGuardReached": true + } + } + ] + }, + { + "name": "native: a component query that calls ctx.auth.getUserIdentity() completes, and its answer is recorded", + "file": "tests/native/component-auth.test.ts", + "result": "passed", + "durationMs": 887.981791, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "identitySeenInsideComponent", + "value": { + "answered": "an identity", + "issuer": "https://fixture-issuer.test", + "subject": "user-1" + } + } + ] + }, + { + "name": "Probe 5: a page that outgrows its read cap comes back through the parent as a split (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.test.ts", + "result": "passed", + "durationMs": 942.8025419999999, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "initialPinnedPage", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j9780czmdafw6t0g708kkxf2rs8fecb5", + "j97c86ve39w6bf9gpkq7bnw2a18fe628", + "j970gmtmjy31j2szj2m3dh3xcx8fev02", + "j97c1b26dgs2571ssb3mq6p9sh8feyb1", + "j97d59mgxf1464qeyea3xndwvx8fen4b", + "j976sg4s9se3ysw0ytrqftndds8ffqy9", + "j97dgjbhq1syh9ckpem9pszwzx8feq0h", + "j97556cw3mmkzv8dd9wcb7zn4n8ff38d", + "j978h0ayjdepayty2fxgwe45cn8ffyn6", + "j97bk038nv6tncbj6t9mqqf4s58ffbh8" + ] + } + }, + { + "name": "splitPage", + "value": { + "rows": 20, + "status": "SplitRequired", + "ids": [ + "j9780czmdafw6t0g708kkxf2rs8fecb5", + "j9719q0h6n059wy2xwb06e1c9d8feqc5", + "j97e1mqsvvjxcwzzcagqmbq4z98fesjn", + "j97fwzxtm97x55nc3q4wdpzqjd8fex29", + "j97ae6brj7gy9jy4wqq7asrw2s8fe994", + "j97cj6w2119x1rytfxhhk0dmjh8feeb2", + "j9788gnz8hf68h25hpmrrds8vs8feekj", + "j97cjyx6eex7mhh1jmfwz1ghy18fead1", + "j970d93jhd3wese9dym6scf0th8ffwhx", + "j971hxm94h5k4r3hpgsn5cwc4x8ffkxx", + "j97cjqr9jhd6jdct7fysvr03kx8ffn8x", + "j97dcy3xv49hbnc5k437v3rpth8ffrmn", + "j975sa32jewp9gnpa1nkttxefn8fegw7", + "j97dr0qwg4p52wmd169xqemswn8ffbam", + "j9787jf6j5nzvy40c0gzdqjmb98ffkb3", + "j97ce9n12kpyztve49h2tbyk3d8ffk26", + "j97c86ve39w6bf9gpkq7bnw2a18fe628", + "j970gmtmjy31j2szj2m3dh3xcx8fev02", + "j97c1b26dgs2571ssb3mq6p9sh8feyb1", + "j97d59mgxf1464qeyea3xndwvx8fen4b" + ] + } + }, + { + "name": "relayedSplit", + "value": { + "status": "SplitRequired", + "splitCursor": "[0.5625,1790883007423.274,\"j971hxm94h5k4r3hpgsn5cwc4x8ffkxx\"]", + "continueCursor": "[4,1790883007370.3289,\"j97d59mgxf1464qeyea3xndwvx8fen4b\"]", + "endCursor": "[9,1790883007370.33,\"j97bk038nv6tncbj6t9mqqf4s58ffbh8\"]", + "rows": 20 + } + }, + { + "name": "splitSummary", + "value": { + "pageStatus": "SplitRequired", + "left": [ + "j9780czmdafw6t0g708kkxf2rs8fecb5", + "j9719q0h6n059wy2xwb06e1c9d8feqc5", + "j97e1mqsvvjxcwzzcagqmbq4z98fesjn", + "j97fwzxtm97x55nc3q4wdpzqjd8fex29", + "j97ae6brj7gy9jy4wqq7asrw2s8fe994", + "j97cj6w2119x1rytfxhhk0dmjh8feeb2", + "j9788gnz8hf68h25hpmrrds8vs8feekj", + "j97cjyx6eex7mhh1jmfwz1ghy18fead1", + "j970d93jhd3wese9dym6scf0th8ffwhx", + "j971hxm94h5k4r3hpgsn5cwc4x8ffkxx" + ], + "right": [ + "j97cjqr9jhd6jdct7fysvr03kx8ffn8x", + "j97dcy3xv49hbnc5k437v3rpth8ffrmn", + "j975sa32jewp9gnpa1nkttxefn8fegw7", + "j97dr0qwg4p52wmd169xqemswn8ffbam", + "j9787jf6j5nzvy40c0gzdqjmb98ffkb3", + "j97ce9n12kpyztve49h2tbyk3d8ffk26", + "j97c86ve39w6bf9gpkq7bnw2a18fe628", + "j970gmtmjy31j2szj2m3dh3xcx8fev02", + "j97c1b26dgs2571ssb3mq6p9sh8feyb1", + "j97d59mgxf1464qeyea3xndwvx8fen4b", + "j976sg4s9se3ysw0ytrqftndds8ffqy9", + "j97dgjbhq1syh9ckpem9pszwzx8feq0h", + "j97556cw3mmkzv8dd9wcb7zn4n8ff38d", + "j978h0ayjdepayty2fxgwe45cn8ffyn6", + "j97bk038nv6tncbj6t9mqqf4s58ffbh8" + ], + "wholeRange": [ + "j9780czmdafw6t0g708kkxf2rs8fecb5", + "j9719q0h6n059wy2xwb06e1c9d8feqc5", + "j97e1mqsvvjxcwzzcagqmbq4z98fesjn", + "j97fwzxtm97x55nc3q4wdpzqjd8fex29", + "j97ae6brj7gy9jy4wqq7asrw2s8fe994", + "j97cj6w2119x1rytfxhhk0dmjh8feeb2", + "j9788gnz8hf68h25hpmrrds8vs8feekj", + "j97cjyx6eex7mhh1jmfwz1ghy18fead1", + "j970d93jhd3wese9dym6scf0th8ffwhx", + "j971hxm94h5k4r3hpgsn5cwc4x8ffkxx", + "j97cjqr9jhd6jdct7fysvr03kx8ffn8x", + "j97dcy3xv49hbnc5k437v3rpth8ffrmn", + "j975sa32jewp9gnpa1nkttxefn8fegw7", + "j97dr0qwg4p52wmd169xqemswn8ffbam", + "j9787jf6j5nzvy40c0gzdqjmb98ffkb3", + "j97ce9n12kpyztve49h2tbyk3d8ffk26", + "j97c86ve39w6bf9gpkq7bnw2a18fe628", + "j970gmtmjy31j2szj2m3dh3xcx8fev02", + "j97c1b26dgs2571ssb3mq6p9sh8feyb1", + "j97d59mgxf1464qeyea3xndwvx8fen4b", + "j976sg4s9se3ysw0ytrqftndds8ffqy9", + "j97dgjbhq1syh9ckpem9pszwzx8feq0h", + "j97556cw3mmkzv8dd9wcb7zn4n8ff38d", + "j978h0ayjdepayty2fxgwe45cn8ffyn6", + "j97bk038nv6tncbj6t9mqqf4s58ffbh8" + ] + } + } + ] + }, + { + "name": "An ordinary client cannot call a component's function (spec:platform.native-harness.ordinary-client-refused-component-function) > executes the bound example against a fresh world", + "file": "tests/native/ordinary-client-refused-component-function.test.ts", + "result": "passed", + "durationMs": 4664.432291, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "componentSocketSummary", + "value": { + "sends": 3, + "backendCloses": 3, + "answer": "no answer", + "socketWaitMs": 3000 + } + } + ] + }, + { + "name": "Probe 5: the client hook splits a page across the boundary and loses rows (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.test.ts", + "result": "passed", + "durationMs": 1329.616334, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "hookBeforeInsert", + "value": { + "rows": 30, + "status": "Exhausted" + } + }, + { + "name": "hookCappedPage", + "value": { + "status": "SplitRequired", + "rows": 20, + "continueCursor": "[4,1790883007332.863,\"j97fdptq69ny81rz0rkvb65v8x8fewqf\"]", + "endCursor": "[9,1790883007332.8643,\"j979ev6gh34mx1zagqtfpx594h8fffv5\"]" + } + }, + { + "name": "hookSplitSummary", + "value": { + "rowsInList": 45, + "rowsShown": 40, + "status": "Exhausted", + "missingPositions": [ + 5, + 6, + 7, + 8, + 9 + ] + } + }, + { + "name": "hookMissingCursorRange", + "value": { + "missing": [ + "j97fas3kv01w5nd62d2wt997tn8fem9p", + "j977rgk1cvy47br3n9zs44v3hs8feq4f", + "j976w4qm9r20d2pge0ggx9h0358ffjsf", + "j97fj6hd5vxg2epg81mc22kpn18feb5e", + "j979ev6gh34mx1zagqtfpx594h8fffv5" + ], + "betweenCursors": [ + "j97fas3kv01w5nd62d2wt997tn8fem9p", + "j977rgk1cvy47br3n9zs44v3hs8feq4f", + "j976w4qm9r20d2pge0ggx9h0358ffjsf", + "j97fj6hd5vxg2epg81mc22kpn18feb5e", + "j979ev6gh34mx1zagqtfpx594h8fffv5" + ] + } + } + ] + }, + { + "name": "Probe 1: the HTTP client and a caller's retry (spec:facts.f05-react-client-retries-until-confirmed.probe-1-http-client-retry) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.test.ts", + "result": "passed", + "durationMs": 892.4398330000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "requestsAfterEachCall", + "value": [ + 1, + 2 + ] + }, + { + "name": "httpRetrySummary", + "value": { + "rows": 2, + "requestsAfterCall": [ + 1, + 2 + ] + } + } + ] + }, + { + "name": "native: a taken --port causes fresh port pairs and the third start succeeds", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 3126.5235410000005, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "1c96562587d294ccf66bb657c35998dc1c0850e02590e5e886bbbac3b7f0a5f0", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "A token signed by the fixture issuer yields the caller's identity (spec:platform.native-harness.fixture-issuer-token-yields-identity) > executes the bound example against a fresh world", + "file": "tests/native/fixture-issuer-token-yields-identity.test.ts", + "result": "passed", + "durationMs": 908.8342500000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 5: pages of a component list stay contiguous under live writes (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.test.ts", + "result": "passed", + "durationMs": 1149.313084, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "initialPinnedPage0", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j975875w0qv0va0fnvwd4xhag18ffahy", + "j976c1wy1mwcdgjrqyfec441c58ff30j", + "j978geedd4emeq0kbjx3mv4pvh8fe7vh", + "j979339ny245d03g7js9c7zqjx8fetfj", + "j975jzt9v4jz6y6s9st6w5z3ah8fe25p", + "j972fd7ree946ae73csz7ssxj58fea6q", + "j971paw8r3vchwsgxfvy51kjvs8fec11", + "j975zvevffdr1bvn6149h1cpfh8ffvss", + "j97229gbs1vrktv5nq68smxp9h8fetv6", + "j97dzeq94x68mjrp7wy0nvkwkx8ff58n" + ] + } + }, + { + "name": "initialPinnedPage1", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j97034yyhetwxg95x6b5j1tmkx8feqyy", + "j9765q8hx9h002pvd5aaxdntkh8fe7wd", + "j978g3v58na9kw9z12y0ha9czn8fe8rx", + "j97dskscymgembcvdnfw8t6hb98fft3s", + "j97f54zb329w4vxyrmc0xxbnyd8feydg", + "j973pkhvv8hgdvhq2svvzabaeh8ffskm", + "j97a7vjnqxjcdnswjyatbdsc018fff94", + "j979b78ghzjjfxzd34yraysg0d8fewem", + "j97em3n4xs12p2fq71tcrpj2md8ffnfa", + "j976ype2hmr1vz2jez94ngae7x8ffegt" + ] + } + }, + { + "name": "initialPinnedPage2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j973wpy0q1zh597nmca6567v518fedh1", + "j978r95kph8v15e6mn2j7ttgwx8ffjb0", + "j97dxhp1qtjthzta2ewh2bd0ds8ffy7k", + "j978cqmrebh8rxwhfbxfaxt3758ffsye", + "j974ex7v817c72tv46jrn1p1v18ff0yt", + "j97bvk7npjm7ek6091vtb0axa18fe5jm", + "j975mvre4vz7554str0vdtp36s8fec8e", + "j97fc5p4t5p57n0k4kpr6d8x9n8ff8dd", + "j970d741p1mq30qy6jm4361rj58ffmmx", + "j979w3bz0880q2v87dg1401wjh8ffqmq" + ] + } + }, + { + "name": "initialPinnedPage3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "initialPinnedPages", + "value": [ + { + "rows": 10, + "isDone": false + }, + { + "rows": 10, + "isDone": false + }, + { + "rows": 10, + "isDone": false + }, + { + "rows": 0, + "isDone": true + } + ] + }, + { + "name": "pinnedPageAfterInsert1", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j97034yyhetwxg95x6b5j1tmkx8feqyy", + "j9765q8hx9h002pvd5aaxdntkh8fe7wd", + "j978g3v58na9kw9z12y0ha9czn8fe8rx", + "j97dskscymgembcvdnfw8t6hb98fft3s", + "j97f54zb329w4vxyrmc0xxbnyd8feydg", + "j973pkhvv8hgdvhq2svvzabaeh8ffskm", + "j97a7vjnqxjcdnswjyatbdsc018fff94", + "j979b78ghzjjfxzd34yraysg0d8fewem", + "j97em3n4xs12p2fq71tcrpj2md8ffnfa", + "j976ype2hmr1vz2jez94ngae7x8ffegt" + ] + } + }, + { + "name": "pinnedPageAfterInsert2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j973wpy0q1zh597nmca6567v518fedh1", + "j978r95kph8v15e6mn2j7ttgwx8ffjb0", + "j97dxhp1qtjthzta2ewh2bd0ds8ffy7k", + "j978cqmrebh8rxwhfbxfaxt3758ffsye", + "j974ex7v817c72tv46jrn1p1v18ff0yt", + "j97bvk7npjm7ek6091vtb0axa18fe5jm", + "j975mvre4vz7554str0vdtp36s8fec8e", + "j97fc5p4t5p57n0k4kpr6d8x9n8ff8dd", + "j970d741p1mq30qy6jm4361rj58ffmmx", + "j979w3bz0880q2v87dg1401wjh8ffqmq" + ] + } + }, + { + "name": "pinnedPageAfterInsert3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "pinnedPageAfterInsert0", + "value": { + "rows": 15, + "status": "SplitRecommended", + "ids": [ + "j975875w0qv0va0fnvwd4xhag18ffahy", + "j97dv6kjf6nk24wda59682yg858fea3a", + "j97c5bdghrh98ghpxm56kjdjt18ffbes", + "j9729jtcyf8js01r12pmhy1ban8fezw3", + "j97akrv84cvghhga00h47r94z18ffbxw", + "j972a7z4xxve6vdcjg08dnjbt18ffeza", + "j976c1wy1mwcdgjrqyfec441c58ff30j", + "j978geedd4emeq0kbjx3mv4pvh8fe7vh", + "j979339ny245d03g7js9c7zqjx8fetfj", + "j975jzt9v4jz6y6s9st6w5z3ah8fe25p", + "j972fd7ree946ae73csz7ssxj58fea6q", + "j971paw8r3vchwsgxfvy51kjvs8fec11", + "j975zvevffdr1bvn6149h1cpfh8ffvss", + "j97229gbs1vrktv5nq68smxp9h8fetv6", + "j97dzeq94x68mjrp7wy0nvkwkx8ff58n" + ] + } + }, + { + "name": "pagesAfterInsert", + "value": [ + [ + "j975875w0qv0va0fnvwd4xhag18ffahy", + "j97dv6kjf6nk24wda59682yg858fea3a", + "j97c5bdghrh98ghpxm56kjdjt18ffbes", + "j9729jtcyf8js01r12pmhy1ban8fezw3", + "j97akrv84cvghhga00h47r94z18ffbxw", + "j972a7z4xxve6vdcjg08dnjbt18ffeza", + "j976c1wy1mwcdgjrqyfec441c58ff30j", + "j978geedd4emeq0kbjx3mv4pvh8fe7vh", + "j979339ny245d03g7js9c7zqjx8fetfj", + "j975jzt9v4jz6y6s9st6w5z3ah8fe25p", + "j972fd7ree946ae73csz7ssxj58fea6q", + "j971paw8r3vchwsgxfvy51kjvs8fec11", + "j975zvevffdr1bvn6149h1cpfh8ffvss", + "j97229gbs1vrktv5nq68smxp9h8fetv6", + "j97dzeq94x68mjrp7wy0nvkwkx8ff58n" + ], + [ + "j97034yyhetwxg95x6b5j1tmkx8feqyy", + "j9765q8hx9h002pvd5aaxdntkh8fe7wd", + "j978g3v58na9kw9z12y0ha9czn8fe8rx", + "j97dskscymgembcvdnfw8t6hb98fft3s", + "j97f54zb329w4vxyrmc0xxbnyd8feydg", + "j973pkhvv8hgdvhq2svvzabaeh8ffskm", + "j97a7vjnqxjcdnswjyatbdsc018fff94", + "j979b78ghzjjfxzd34yraysg0d8fewem", + "j97em3n4xs12p2fq71tcrpj2md8ffnfa", + "j976ype2hmr1vz2jez94ngae7x8ffegt" + ], + [ + "j973wpy0q1zh597nmca6567v518fedh1", + "j978r95kph8v15e6mn2j7ttgwx8ffjb0", + "j97dxhp1qtjthzta2ewh2bd0ds8ffy7k", + "j978cqmrebh8rxwhfbxfaxt3758ffsye", + "j974ex7v817c72tv46jrn1p1v18ff0yt", + "j97bvk7npjm7ek6091vtb0axa18fe5jm", + "j975mvre4vz7554str0vdtp36s8fec8e", + "j97fc5p4t5p57n0k4kpr6d8x9n8ff8dd", + "j970d741p1mq30qy6jm4361rj58ffmmx", + "j979w3bz0880q2v87dg1401wjh8ffqmq" + ], + [] + ] + }, + { + "name": "contiguityAfterInsert", + "value": { + "actual": [ + "j975875w0qv0va0fnvwd4xhag18ffahy", + "j97dv6kjf6nk24wda59682yg858fea3a", + "j97c5bdghrh98ghpxm56kjdjt18ffbes", + "j9729jtcyf8js01r12pmhy1ban8fezw3", + "j97akrv84cvghhga00h47r94z18ffbxw", + "j972a7z4xxve6vdcjg08dnjbt18ffeza", + "j976c1wy1mwcdgjrqyfec441c58ff30j", + "j978geedd4emeq0kbjx3mv4pvh8fe7vh", + "j979339ny245d03g7js9c7zqjx8fetfj", + "j975jzt9v4jz6y6s9st6w5z3ah8fe25p", + "j972fd7ree946ae73csz7ssxj58fea6q", + "j971paw8r3vchwsgxfvy51kjvs8fec11", + "j975zvevffdr1bvn6149h1cpfh8ffvss", + "j97229gbs1vrktv5nq68smxp9h8fetv6", + "j97dzeq94x68mjrp7wy0nvkwkx8ff58n", + "j97034yyhetwxg95x6b5j1tmkx8feqyy", + "j9765q8hx9h002pvd5aaxdntkh8fe7wd", + "j978g3v58na9kw9z12y0ha9czn8fe8rx", + "j97dskscymgembcvdnfw8t6hb98fft3s", + "j97f54zb329w4vxyrmc0xxbnyd8feydg", + "j973pkhvv8hgdvhq2svvzabaeh8ffskm", + "j97a7vjnqxjcdnswjyatbdsc018fff94", + "j979b78ghzjjfxzd34yraysg0d8fewem", + "j97em3n4xs12p2fq71tcrpj2md8ffnfa", + "j976ype2hmr1vz2jez94ngae7x8ffegt", + "j973wpy0q1zh597nmca6567v518fedh1", + "j978r95kph8v15e6mn2j7ttgwx8ffjb0", + "j97dxhp1qtjthzta2ewh2bd0ds8ffy7k", + "j978cqmrebh8rxwhfbxfaxt3758ffsye", + "j974ex7v817c72tv46jrn1p1v18ff0yt", + "j97bvk7npjm7ek6091vtb0axa18fe5jm", + "j975mvre4vz7554str0vdtp36s8fec8e", + "j97fc5p4t5p57n0k4kpr6d8x9n8ff8dd", + "j970d741p1mq30qy6jm4361rj58ffmmx", + "j979w3bz0880q2v87dg1401wjh8ffqmq" + ], + "expected": [ + "j975875w0qv0va0fnvwd4xhag18ffahy", + "j97dv6kjf6nk24wda59682yg858fea3a", + "j97c5bdghrh98ghpxm56kjdjt18ffbes", + "j9729jtcyf8js01r12pmhy1ban8fezw3", + "j97akrv84cvghhga00h47r94z18ffbxw", + "j972a7z4xxve6vdcjg08dnjbt18ffeza", + "j976c1wy1mwcdgjrqyfec441c58ff30j", + "j978geedd4emeq0kbjx3mv4pvh8fe7vh", + "j979339ny245d03g7js9c7zqjx8fetfj", + "j975jzt9v4jz6y6s9st6w5z3ah8fe25p", + "j972fd7ree946ae73csz7ssxj58fea6q", + "j971paw8r3vchwsgxfvy51kjvs8fec11", + "j975zvevffdr1bvn6149h1cpfh8ffvss", + "j97229gbs1vrktv5nq68smxp9h8fetv6", + "j97dzeq94x68mjrp7wy0nvkwkx8ff58n", + "j97034yyhetwxg95x6b5j1tmkx8feqyy", + "j9765q8hx9h002pvd5aaxdntkh8fe7wd", + "j978g3v58na9kw9z12y0ha9czn8fe8rx", + "j97dskscymgembcvdnfw8t6hb98fft3s", + "j97f54zb329w4vxyrmc0xxbnyd8feydg", + "j973pkhvv8hgdvhq2svvzabaeh8ffskm", + "j97a7vjnqxjcdnswjyatbdsc018fff94", + "j979b78ghzjjfxzd34yraysg0d8fewem", + "j97em3n4xs12p2fq71tcrpj2md8ffnfa", + "j976ype2hmr1vz2jez94ngae7x8ffegt", + "j973wpy0q1zh597nmca6567v518fedh1", + "j978r95kph8v15e6mn2j7ttgwx8ffjb0", + "j97dxhp1qtjthzta2ewh2bd0ds8ffy7k", + "j978cqmrebh8rxwhfbxfaxt3758ffsye", + "j974ex7v817c72tv46jrn1p1v18ff0yt", + "j97bvk7npjm7ek6091vtb0axa18fe5jm", + "j975mvre4vz7554str0vdtp36s8fec8e", + "j97fc5p4t5p57n0k4kpr6d8x9n8ff8dd", + "j970d741p1mq30qy6jm4361rj58ffmmx", + "j979w3bz0880q2v87dg1401wjh8ffqmq" + ] + } + }, + { + "name": "pinnedPageAfterDelete0", + "value": { + "rows": 15, + "status": "SplitRecommended", + "ids": [ + "j975875w0qv0va0fnvwd4xhag18ffahy", + "j97dv6kjf6nk24wda59682yg858fea3a", + "j97c5bdghrh98ghpxm56kjdjt18ffbes", + "j9729jtcyf8js01r12pmhy1ban8fezw3", + "j97akrv84cvghhga00h47r94z18ffbxw", + "j972a7z4xxve6vdcjg08dnjbt18ffeza", + "j976c1wy1mwcdgjrqyfec441c58ff30j", + "j978geedd4emeq0kbjx3mv4pvh8fe7vh", + "j979339ny245d03g7js9c7zqjx8fetfj", + "j975jzt9v4jz6y6s9st6w5z3ah8fe25p", + "j972fd7ree946ae73csz7ssxj58fea6q", + "j971paw8r3vchwsgxfvy51kjvs8fec11", + "j975zvevffdr1bvn6149h1cpfh8ffvss", + "j97229gbs1vrktv5nq68smxp9h8fetv6", + "j97dzeq94x68mjrp7wy0nvkwkx8ff58n" + ] + } + }, + { + "name": "pinnedPageAfterDelete2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j973wpy0q1zh597nmca6567v518fedh1", + "j978r95kph8v15e6mn2j7ttgwx8ffjb0", + "j97dxhp1qtjthzta2ewh2bd0ds8ffy7k", + "j978cqmrebh8rxwhfbxfaxt3758ffsye", + "j974ex7v817c72tv46jrn1p1v18ff0yt", + "j97bvk7npjm7ek6091vtb0axa18fe5jm", + "j975mvre4vz7554str0vdtp36s8fec8e", + "j97fc5p4t5p57n0k4kpr6d8x9n8ff8dd", + "j970d741p1mq30qy6jm4361rj58ffmmx", + "j979w3bz0880q2v87dg1401wjh8ffqmq" + ] + } + }, + { + "name": "pinnedPageAfterDelete3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "pinnedPageAfterDelete1", + "value": { + "rows": 9, + "status": null, + "ids": [ + "j97034yyhetwxg95x6b5j1tmkx8feqyy", + "j978g3v58na9kw9z12y0ha9czn8fe8rx", + "j97dskscymgembcvdnfw8t6hb98fft3s", + "j97f54zb329w4vxyrmc0xxbnyd8feydg", + "j973pkhvv8hgdvhq2svvzabaeh8ffskm", + "j97a7vjnqxjcdnswjyatbdsc018fff94", + "j979b78ghzjjfxzd34yraysg0d8fewem", + "j97em3n4xs12p2fq71tcrpj2md8ffnfa", + "j976ype2hmr1vz2jez94ngae7x8ffegt" + ] + } + }, + { + "name": "contiguitySummary", + "value": { + "pages": 4, + "rowsAfterInsert": 35, + "rowsAfterDelete": 34, + "removed": "j9765q8hx9h002pvd5aaxdntkh8fe7wd", + "actual": [ + "j975875w0qv0va0fnvwd4xhag18ffahy", + "j97dv6kjf6nk24wda59682yg858fea3a", + "j97c5bdghrh98ghpxm56kjdjt18ffbes", + "j9729jtcyf8js01r12pmhy1ban8fezw3", + "j97akrv84cvghhga00h47r94z18ffbxw", + "j972a7z4xxve6vdcjg08dnjbt18ffeza", + "j976c1wy1mwcdgjrqyfec441c58ff30j", + "j978geedd4emeq0kbjx3mv4pvh8fe7vh", + "j979339ny245d03g7js9c7zqjx8fetfj", + "j975jzt9v4jz6y6s9st6w5z3ah8fe25p", + "j972fd7ree946ae73csz7ssxj58fea6q", + "j971paw8r3vchwsgxfvy51kjvs8fec11", + "j975zvevffdr1bvn6149h1cpfh8ffvss", + "j97229gbs1vrktv5nq68smxp9h8fetv6", + "j97dzeq94x68mjrp7wy0nvkwkx8ff58n", + "j97034yyhetwxg95x6b5j1tmkx8feqyy", + "j978g3v58na9kw9z12y0ha9czn8fe8rx", + "j97dskscymgembcvdnfw8t6hb98fft3s", + "j97f54zb329w4vxyrmc0xxbnyd8feydg", + "j973pkhvv8hgdvhq2svvzabaeh8ffskm", + "j97a7vjnqxjcdnswjyatbdsc018fff94", + "j979b78ghzjjfxzd34yraysg0d8fewem", + "j97em3n4xs12p2fq71tcrpj2md8ffnfa", + "j976ype2hmr1vz2jez94ngae7x8ffegt", + "j973wpy0q1zh597nmca6567v518fedh1", + "j978r95kph8v15e6mn2j7ttgwx8ffjb0", + "j97dxhp1qtjthzta2ewh2bd0ds8ffy7k", + "j978cqmrebh8rxwhfbxfaxt3758ffsye", + "j974ex7v817c72tv46jrn1p1v18ff0yt", + "j97bvk7npjm7ek6091vtb0axa18fe5jm", + "j975mvre4vz7554str0vdtp36s8fec8e", + "j97fc5p4t5p57n0k4kpr6d8x9n8ff8dd", + "j970d741p1mq30qy6jm4361rj58ffmmx", + "j979w3bz0880q2v87dg1401wjh8ffqmq" + ], + "expected": [ + "j975875w0qv0va0fnvwd4xhag18ffahy", + "j97dv6kjf6nk24wda59682yg858fea3a", + "j97c5bdghrh98ghpxm56kjdjt18ffbes", + "j9729jtcyf8js01r12pmhy1ban8fezw3", + "j97akrv84cvghhga00h47r94z18ffbxw", + "j972a7z4xxve6vdcjg08dnjbt18ffeza", + "j976c1wy1mwcdgjrqyfec441c58ff30j", + "j978geedd4emeq0kbjx3mv4pvh8fe7vh", + "j979339ny245d03g7js9c7zqjx8fetfj", + "j975jzt9v4jz6y6s9st6w5z3ah8fe25p", + "j972fd7ree946ae73csz7ssxj58fea6q", + "j971paw8r3vchwsgxfvy51kjvs8fec11", + "j975zvevffdr1bvn6149h1cpfh8ffvss", + "j97229gbs1vrktv5nq68smxp9h8fetv6", + "j97dzeq94x68mjrp7wy0nvkwkx8ff58n", + "j97034yyhetwxg95x6b5j1tmkx8feqyy", + "j978g3v58na9kw9z12y0ha9czn8fe8rx", + "j97dskscymgembcvdnfw8t6hb98fft3s", + "j97f54zb329w4vxyrmc0xxbnyd8feydg", + "j973pkhvv8hgdvhq2svvzabaeh8ffskm", + "j97a7vjnqxjcdnswjyatbdsc018fff94", + "j979b78ghzjjfxzd34yraysg0d8fewem", + "j97em3n4xs12p2fq71tcrpj2md8ffnfa", + "j976ype2hmr1vz2jez94ngae7x8ffegt", + "j973wpy0q1zh597nmca6567v518fedh1", + "j978r95kph8v15e6mn2j7ttgwx8ffjb0", + "j97dxhp1qtjthzta2ewh2bd0ds8ffy7k", + "j978cqmrebh8rxwhfbxfaxt3758ffsye", + "j974ex7v817c72tv46jrn1p1v18ff0yt", + "j97bvk7npjm7ek6091vtb0axa18fe5jm", + "j975mvre4vz7554str0vdtp36s8fec8e", + "j97fc5p4t5p57n0k4kpr6d8x9n8ff8dd", + "j970d741p1mq30qy6jm4361rj58ffmmx", + "j979w3bz0880q2v87dg1401wjh8ffqmq" + ] + } + } + ] + }, + { + "name": "The admin key acting as an identity reaches an internal function (spec:platform.native-harness.admin-key-acting-as-identity-reaches-internal-function) > executes the bound example against a fresh world", + "file": "tests/native/admin-key-acting-as-identity-reaches-internal-function.test.ts", + "result": "passed", + "durationMs": 1003.9536250000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 5: a subscription to a parent query follows the component's data (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.test.ts", + "result": "passed", + "durationMs": 1038.6502500000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "subscriptionSummary", + "value": { + "waitMs": 5000, + "observationAfterDeliveryMs": 100, + "values": [ + { + "id": "j97ap5hq5hrnzyx17qbmdb1abx8fehn7", + "label": "changed" + } + ], + "error": null + } + } + ] + }, + { + "name": "native: a taken --site-proxy-port causes fresh port pairs and the third start succeeds", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 1105.8786249999994, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "6097bc25f9745c9ee4bf66c0de64bc3d10b702dc7d05948bb25edae0e9f42914", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "An ordinary client cannot call an internal function (spec:platform.native-harness.ordinary-client-refused-internal-function) > executes the bound example against a fresh world", + "file": "tests/native/ordinary-client-refused-internal-function.test.ts", + "result": "passed", + "durationMs": 925.5353750000002, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: the parent sweep verifies and kills two real owned backend processes", + "file": "tests/native/harness-sweep.test.ts", + "result": "passed", + "durationMs": 316.206708, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + }, + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native review: failed inspection must not silently discard ownership", + "file": "tests/native/harness-sweep.test.ts", + "result": "passed", + "durationMs": 154.25549999999998, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: three taken ports fail clearly and remove the failed start's storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 3774.373417, + "errors": [], + "backends": [], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "Probe 1: a backend restart with a mutation in flight (spec:facts.f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.test.ts", + "result": "passed", + "durationMs": 11303.144125, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "trials", + "value": [ + { + "trial": "restart 0", + "delayMs": 0, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 1", + "delayMs": 0, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 2", + "delayMs": 0, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 3", + "delayMs": 1, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 4", + "delayMs": 2, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 5", + "delayMs": 4, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 6", + "delayMs": 8, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 7", + "delayMs": 16, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 8", + "delayMs": 32, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 9", + "delayMs": 64, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + } + ] + }, + { + "name": "restartSummary", + "value": { + "trials": 10, + "resolvedAfterRestart": 10, + "resolvedBeforeRelease": 0, + "killedBeforeCommit": 6, + "killedAfterCommit": 4, + "rowCounts": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + } + ] + }, + { + "name": "native: stop escalates to SIGKILL when the owned process ignores SIGINT and keeps its storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 5358.785583000001, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "d43b86c5c4f386e22d2bceacadec05607b59f8aeeeab29051fd51ebfd934a6fa", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: a log mark from before a kill cannot be read after restart", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 857.7276250000014, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 4: a nested call draws on the caller's transaction limits (spec:facts.f13-transactions-have-limits.probe-4-nested-call-shares-limits) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-nested-call-shares-limits.test.ts", + "result": "passed", + "durationMs": 21874.027917000003, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "seededPayloads", + "value": { + "size": 921600, + "parentCount": 10, + "childCount": 10 + } + }, + { + "name": "combinedReadOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 31ded4e2945b3ebd] Server Error\nUncaught Error: Uncaught Error: Too many bytes read in a single function execution (limit: 16777216 bytes). Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\n at async handler (../fixture/convex/limits.ts:47:11)\n\n at async handler (../fixture/convex/limits.ts:78:34)\n" + } + }, + { + "name": "readTogether", + "value": "fails on the bytes-read limit" + }, + { + "name": "parentReadOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childReadOutcome", + "value": { + "outcome": "returns", + "error": null + } + }, + { + "name": "readsAlone", + "value": { + "parent": "commits", + "child": "returns", + "parentDocuments": 10, + "childDocuments": 10, + "childPayloadBytes": 9216000 + } + }, + { + "name": "sharedReadMetrics", + "value": { + "byteGrowth": 921684, + "documentGrowth": 1, + "childBytes": 921684, + "childDocuments": 1, + "payloadBytes": 921600 + } + }, + { + "name": "combinedWriteOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 822a83bc7f482201] Server Error\nUncaught Error: Uncaught Error: Too many bytes written in a single function execution (limit: 16 MiB)\n at async handler (../fixture/convex/limits.ts:23:35)\n\n at async handler (../fixture/convex/limits.ts:102:8)\n" + } + }, + { + "name": "writesTogether", + "value": "fails on the bytes-written limit" + }, + { + "name": "failedWriteRows", + "value": 0 + }, + { + "name": "parentWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "parentWriteRows", + "value": 10 + }, + { + "name": "childWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childWriteRows", + "value": 20 + }, + { + "name": "limitSummary", + "value": { + "boundary": "nested", + "combinedRead": "fails on the bytes-read limit", + "combinedWrite": "fails on the bytes-written limit", + "failedWriteRows": 0, + "committedRows": 20 + } + } + ] + }, + { + "name": "Probe 4: a component call draws on the caller's transaction limits (spec:facts.f13-transactions-have-limits.probe-4-component-call-shares-limits) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-component-call-shares-limits.test.ts", + "result": "passed", + "durationMs": 22059.392083000002, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "seededPayloads", + "value": { + "size": 921600, + "parentCount": 10, + "childCount": 10 + } + }, + { + "name": "combinedReadOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: c2ea05a4f0b048c9] Server Error\nUncaught Error: Uncaught Error: Too many bytes read in a single function execution (limit: 16777216 bytes). Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\n at async handler (../fixture/convex/annex/limits.ts:34:11)\n\n at async handler (../fixture/convex/limits.ts:78:34)\n" + } + }, + { + "name": "readTogether", + "value": "fails on the bytes-read limit" + }, + { + "name": "parentReadOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childReadOutcome", + "value": { + "outcome": "returns", + "error": null + } + }, + { + "name": "readsAlone", + "value": { + "parent": "commits", + "child": "returns", + "parentDocuments": 10, + "childDocuments": 10, + "childPayloadBytes": 9216000 + } + }, + { + "name": "sharedReadMetrics", + "value": { + "byteGrowth": 921684, + "documentGrowth": 1, + "childBytes": 921684, + "childDocuments": 1, + "payloadBytes": 921600 + } + }, + { + "name": "combinedWriteOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: fe38bb69217b7aa9] Server Error\nUncaught Error: Uncaught Error: Too many bytes written in a single function execution (limit: 16 MiB)\n at async handler (../fixture/convex/annex/limits.ts:8:35)\n\n at async handler (../fixture/convex/limits.ts:102:8)\n" + } + }, + { + "name": "writesTogether", + "value": "fails on the bytes-written limit" + }, + { + "name": "failedWriteRows", + "value": 0 + }, + { + "name": "parentWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "parentWriteRows", + "value": 10 + }, + { + "name": "childWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childWriteRows", + "value": 20 + }, + { + "name": "limitSummary", + "value": { + "boundary": "annex", + "combinedRead": "fails on the bytes-read limit", + "combinedWrite": "fails on the bytes-written limit", + "failedWriteRows": 0, + "committedRows": 20 + } + } + ] + }, + { + "name": "native: a restart that stalls after spawning reaps its process", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 30333.047000000002, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "ba5e5886252087598e99ff0e96692c233a4854e3714f5266656bedf0e4bc3344", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: codegen interrupted by SIGINT removes its starting backend and storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 359.6029170000038, + "errors": [], + "backends": [], + "measurements": [] + }, + { + "name": "native: codegen interrupted by SIGTERM removes its starting backend and storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 298.94216700000106, + "errors": [], + "backends": [], + "measurements": [] + }, + { + "name": "native: backend output split across chunks is redacted before the rolling buffer clips it", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 365.21774999999616, + "errors": [], + "backends": [], + "measurements": [] + } + ], + "unhandledErrors": [] +} diff --git a/evidence/native-20261001T195108Z-61c3879-e2da54b9-e845-4fa6-bc1d-2449482970dc.json b/evidence/native-20261001T195108Z-61c3879-e2da54b9-e845-4fa6-bc1d-2449482970dc.json new file mode 100644 index 0000000..027bf07 --- /dev/null +++ b/evidence/native-20261001T195108Z-61c3879-e2da54b9-e845-4fa6-bc1d-2449482970dc.json @@ -0,0 +1,3458 @@ +{ + "tier": "native", + "commit": "61c3879b8f5f65a2e613d059162c4292f06fa3c4", + "clean": true, + "command": "vitest run --project native", + "startedAt": "2026-10-01T19:51:08.563Z", + "finishedAt": "2026-10-01T19:51:57.449Z", + "result": "passed", + "versions": { + "node": "v24.21.0", + "convex": "1.46.0", + "convex-helpers": "0.1.124", + "convex-test": "0.0.60", + "vitest": "5.0.3", + "typescript": "7.0.2", + "react": "19.3.0", + "happy-dom": "20.14.5" + }, + "tests": [ + { + "name": "Probe 4: how deep nested calls may go (spec:facts.f13-transactions-have-limits.probe-4-nesting-depth) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-nesting-depth.test.ts", + "result": "passed", + "durationMs": 1927.8789590000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "stack1Trial", + "value": { + "stack": 1, + "answer": 1, + "error": null, + "rows": 1 + } + }, + { + "name": "stack2Trial", + "value": { + "stack": 2, + "answer": 2, + "error": null, + "rows": 2 + } + }, + { + "name": "stack3Trial", + "value": { + "stack": 3, + "answer": 3, + "error": null, + "rows": 3 + } + }, + { + "name": "stack4Trial", + "value": { + "stack": 4, + "answer": 4, + "error": null, + "rows": 4 + } + }, + { + "name": "stack5Trial", + "value": { + "stack": 5, + "answer": 5, + "error": null, + "rows": 5 + } + }, + { + "name": "stack6Trial", + "value": { + "stack": 6, + "answer": 6, + "error": null, + "rows": 6 + } + }, + { + "name": "stack7Trial", + "value": { + "stack": 7, + "answer": 7, + "error": null, + "rows": 7 + } + }, + { + "name": "stack8Trial", + "value": { + "stack": 8, + "answer": 8, + "error": null, + "rows": 8 + } + }, + { + "name": "stack9Trial", + "value": { + "stack": 9, + "answer": 9, + "error": null, + "rows": 9 + } + }, + { + "name": "stack10Trial", + "value": { + "stack": 10, + "answer": null, + "error": "Error: [Request ID: 7dce56292276cd33] Server Error\nUncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Cross component call depth limit exceeded. Do you have an infinite loop in your app?\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n", + "rows": 0 + } + }, + { + "name": "depthBoundary", + "value": { + "deepest": 9, + "firstFailure": 10, + "refusal": "Error: [Request ID: 7dce56292276cd33] Server Error\nUncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Uncaught Error: Cross component call depth limit exceeded. Do you have an infinite loop in your app?\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n\n at async handler (../fixture/convex/limits.ts:119:12)\n" + } + } + ] + }, + { + "name": "native: stop and kill keep stored rows across restart, and dispose removes storage twice", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 1993.4373329999999, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://changed-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 1: a client closed with a pending mutation (spec:facts.f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.test.ts", + "result": "passed", + "durationMs": 2520.424125, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "trials", + "value": [ + { + "trial": "closed 0", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 1", + "gapMs": 1, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 2", + "gapMs": 2, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 3", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 4", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 5", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 6", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 7", + "gapMs": 1, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 8", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 9", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 10", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 11", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 12", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 13", + "gapMs": 1, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 14", + "gapMs": 2, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 15", + "gapMs": 3, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 16", + "gapMs": 4, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 17", + "gapMs": 5, + "confirmed": false, + "rows": 1 + }, + { + "trial": "closed 18", + "gapMs": 0, + "confirmed": false, + "rows": 0 + }, + { + "trial": "closed 19", + "gapMs": 1, + "confirmed": false, + "rows": 0 + } + ] + }, + { + "name": "closedClientSummary", + "value": { + "trials": 20, + "confirmed": 0, + "noRow": 8, + "oneRow": 12 + } + } + ] + }, + { + "name": "Probe 3: the cost of a component call from a mutation (spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.test.ts", + "result": "passed", + "durationMs": 3466.482875, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "costSamples", + "value": [ + { + "round": 0, + "path": "helper", + "cacheBuster": 1000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.003905084, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.003905084, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "nested", + "cacheBuster": 2000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.084061708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.084061708, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "component", + "cacheBuster": 3000, + "result": { + "documentsRead": 50, + "id": "jn76802t3b0ybt2n87vm1wm0gh8femsw" + }, + "callError": null, + "executionTimeSeconds": 0.186643333, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.186643333, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "helper", + "cacheBuster": 4000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.003361542, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.003361542, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "nested", + "cacheBuster": 5000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.067981708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.067981708, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "component", + "cacheBuster": 6000, + "result": { + "documentsRead": 50, + "id": "jn76802t3b0ybt2n87vm1wm0gh8femsw" + }, + "callError": null, + "executionTimeSeconds": 0.094367667, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.094367667, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "helper", + "cacheBuster": 7000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.004705708, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.004705708, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "nested", + "cacheBuster": 8000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.071504833, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.071504833, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "component", + "cacheBuster": 9000, + "result": { + "documentsRead": 50, + "id": "jn76802t3b0ybt2n87vm1wm0gh8femsw" + }, + "callError": null, + "executionTimeSeconds": 0.090539584, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.090539584, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "helper", + "cacheBuster": 10000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.003237458, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.003237458, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "nested", + "cacheBuster": 11000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.063458542, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.063458542, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "component", + "cacheBuster": 12000, + "result": { + "documentsRead": 50, + "id": "jn76802t3b0ybt2n87vm1wm0gh8femsw" + }, + "callError": null, + "executionTimeSeconds": 0.111068334, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.111068334, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "helper", + "cacheBuster": 13000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.002724291, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002724291, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "nested", + "cacheBuster": 14000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.065182542, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.065182542, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "component", + "cacheBuster": 15000, + "result": { + "documentsRead": 50, + "id": "jn76802t3b0ybt2n87vm1wm0gh8femsw" + }, + "callError": null, + "executionTimeSeconds": 0.113779334, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.113779334, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "helper", + "cacheBuster": 16000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.002906041, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.002906041, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "nested", + "cacheBuster": 17000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.073884333, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.073884333, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "component", + "cacheBuster": 18000, + "result": { + "documentsRead": 50, + "id": "jn76802t3b0ybt2n87vm1wm0gh8femsw" + }, + "callError": null, + "executionTimeSeconds": 0.121628125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.121628125, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "helper", + "cacheBuster": 19000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.006367833, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.006367833, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "nested", + "cacheBuster": 20000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.065670833, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.065670833, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "component", + "cacheBuster": 21000, + "result": { + "documentsRead": 50, + "id": "jn76802t3b0ybt2n87vm1wm0gh8femsw" + }, + "callError": null, + "executionTimeSeconds": 0.089313792, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.089313792, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "helper", + "cacheBuster": 22000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.003307, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.003307, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "nested", + "cacheBuster": 23000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.058419375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.058419375, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "component", + "cacheBuster": 24000, + "result": { + "documentsRead": 50, + "id": "jn76802t3b0ybt2n87vm1wm0gh8femsw" + }, + "callError": null, + "executionTimeSeconds": 0.094710458, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.094710458, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "helper", + "cacheBuster": 25000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.003087458, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.003087458, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "nested", + "cacheBuster": 26000, + "result": { + "documentsRead": 50, + "id": "jx7c56kbxfz3qgef5n2jnp83818feqz4" + }, + "callError": null, + "executionTimeSeconds": 0.055333, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.055333, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "component", + "cacheBuster": 27000, + "result": { + "documentsRead": 50, + "id": "jn76802t3b0ybt2n87vm1wm0gh8femsw" + }, + "callError": null, + "executionTimeSeconds": 0.088121958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaMutation", + "componentPath": null, + "executionTime": 0.088121958, + "cachedResult": false + } + ] + } + ] + }, + { + "name": "readCostSummary", + "value": { + "kind": "mutation", + "readsPerSample": 50, + "acceptedSamples": { + "helper": 9, + "nested": 9, + "component": 9 + }, + "componentCompletionRecords": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "helper": 0.003307, + "nested": 0.065670833, + "component": 0.094710458, + "componentMinusHelperSeconds": 0.09140345799999999, + "componentMinusHelperPerReadSeconds": 0.0018280691599999999 + } + } + ] + }, + { + "name": "Probe 3: the cost of a component call from a query (spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query) > executes the bound example against a fresh world", + "file": "tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.test.ts", + "result": "passed", + "durationMs": 3481.9027079999996, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "costSamples", + "value": [ + { + "round": 0, + "path": "helper", + "cacheBuster": 1000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.008241875, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.008241875, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "nested", + "cacheBuster": 2000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.095961, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.095961, + "cachedResult": false + } + ] + }, + { + "round": 0, + "path": "component", + "cacheBuster": 3000, + "result": { + "documentsRead": 50, + "id": "jn7dt11e8bhhr1nc7gary2pytx8ffjt5" + }, + "callError": null, + "executionTimeSeconds": 0.20417325, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.20417325, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "helper", + "cacheBuster": 4000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.003435916, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.003435916, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "nested", + "cacheBuster": 5000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.069885667, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.069885667, + "cachedResult": false + } + ] + }, + { + "round": 1, + "path": "component", + "cacheBuster": 6000, + "result": { + "documentsRead": 50, + "id": "jn7dt11e8bhhr1nc7gary2pytx8ffjt5" + }, + "callError": null, + "executionTimeSeconds": 0.09193925, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.09193925, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "helper", + "cacheBuster": 7000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.003850125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.003850125, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "nested", + "cacheBuster": 8000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.065996, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.065996, + "cachedResult": false + } + ] + }, + { + "round": 2, + "path": "component", + "cacheBuster": 9000, + "result": { + "documentsRead": 50, + "id": "jn7dt11e8bhhr1nc7gary2pytx8ffjt5" + }, + "callError": null, + "executionTimeSeconds": 0.094791667, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.094791667, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "helper", + "cacheBuster": 10000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.003640375, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.003640375, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "nested", + "cacheBuster": 11000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.064744041, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.064744041, + "cachedResult": false + } + ] + }, + { + "round": 3, + "path": "component", + "cacheBuster": 12000, + "result": { + "documentsRead": 50, + "id": "jn7dt11e8bhhr1nc7gary2pytx8ffjt5" + }, + "callError": null, + "executionTimeSeconds": 0.118854125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.118854125, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "helper", + "cacheBuster": 13000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.002810084, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002810084, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "nested", + "cacheBuster": 14000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.078794833, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.078794833, + "cachedResult": false + } + ] + }, + { + "round": 4, + "path": "component", + "cacheBuster": 15000, + "result": { + "documentsRead": 50, + "id": "jn7dt11e8bhhr1nc7gary2pytx8ffjt5" + }, + "callError": null, + "executionTimeSeconds": 0.110918333, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.110918333, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "helper", + "cacheBuster": 16000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.00341925, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.00341925, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "nested", + "cacheBuster": 17000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.077857917, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.077857917, + "cachedResult": false + } + ] + }, + { + "round": 5, + "path": "component", + "cacheBuster": 18000, + "result": { + "documentsRead": 50, + "id": "jn7dt11e8bhhr1nc7gary2pytx8ffjt5" + }, + "callError": null, + "executionTimeSeconds": 0.123331042, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.123331042, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "helper", + "cacheBuster": 19000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.003123417, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.003123417, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "nested", + "cacheBuster": 20000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.062879709, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.062879709, + "cachedResult": false + } + ] + }, + { + "round": 6, + "path": "component", + "cacheBuster": 21000, + "result": { + "documentsRead": 50, + "id": "jn7dt11e8bhhr1nc7gary2pytx8ffjt5" + }, + "callError": null, + "executionTimeSeconds": 0.090091833, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.090091833, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "helper", + "cacheBuster": 22000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.003413166, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.003413166, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "nested", + "cacheBuster": 23000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.060824667, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.060824667, + "cachedResult": false + } + ] + }, + { + "round": 7, + "path": "component", + "cacheBuster": 24000, + "result": { + "documentsRead": 50, + "id": "jn7dt11e8bhhr1nc7gary2pytx8ffjt5" + }, + "callError": null, + "executionTimeSeconds": 0.097573208, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.097573208, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "helper", + "cacheBuster": 25000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.002253, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.002253, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "nested", + "cacheBuster": 26000, + "result": { + "documentsRead": 50, + "id": "jx7cexvxkke7q59mw09xjvf99s8fe4rq" + }, + "callError": null, + "executionTimeSeconds": 0.057896958, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.057896958, + "cachedResult": false + } + ] + }, + { + "round": 8, + "path": "component", + "cacheBuster": 27000, + "result": { + "documentsRead": 50, + "id": "jn7dt11e8bhhr1nc7gary2pytx8ffjt5" + }, + "callError": null, + "executionTimeSeconds": 0.086258125, + "cachedResult": false, + "error": null, + "records": [ + { + "identifier": "readCost:viaQuery", + "componentPath": null, + "executionTime": 0.086258125, + "cachedResult": false + } + ] + } + ] + }, + { + "name": "readCostSummary", + "value": { + "kind": "query", + "readsPerSample": 50, + "acceptedSamples": { + "helper": 9, + "nested": 9, + "component": 9 + }, + "componentCompletionRecords": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "helper": 0.00341925, + "nested": 0.065996, + "component": 0.097573208, + "componentMinusHelperSeconds": 0.094153958, + "componentMinusHelperPerReadSeconds": 0.00188307916 + } + } + ] + }, + { + "name": "Probe 5: the client hook splits a page across the boundary and loses rows (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.test.ts", + "result": "passed", + "durationMs": 1274.441708, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "hookBeforeInsert", + "value": { + "rows": 30, + "status": "Exhausted" + } + }, + { + "name": "hookCappedPage", + "value": { + "status": "SplitRequired", + "rows": 20, + "continueCursor": "[4,1790884272539.892,\"j978x61pnqfhsd95bvst9rt3ex8ff5z2\"]", + "endCursor": "[9,1790884272539.8933,\"j97b3p3n80dgcyx727hw65ebas8ff5c0\"]" + } + }, + { + "name": "hookSplitSummary", + "value": { + "rowsInList": 45, + "rowsShown": 40, + "status": "Exhausted", + "missingPositions": [ + 5, + 6, + 7, + 8, + 9 + ] + } + }, + { + "name": "hookMissingCursorRange", + "value": { + "missing": [ + "j9708m9p4gbde9pve85wwjazj98fejcm", + "j9787c7ve4q8882wm41mn9822s8ff7xc", + "j97d5t7jgvj757hmt1b4qed1bd8ff9sf", + "j97db32a0hknrzfar2f95brgsx8fezm7", + "j97b3p3n80dgcyx727hw65ebas8ff5c0" + ], + "betweenCursors": [ + "j9708m9p4gbde9pve85wwjazj98fejcm", + "j9787c7ve4q8882wm41mn9822s8ff7xc", + "j97d5t7jgvj757hmt1b4qed1bd8ff9sf", + "j97db32a0hknrzfar2f95brgsx8fezm7", + "j97b3p3n80dgcyx727hw65ebas8ff5c0" + ] + } + } + ] + }, + { + "name": "Probe 2: ConvexError data through a component boundary (spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary) > executes the bound example against a fresh world", + "file": "tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.test.ts", + "result": "passed", + "durationMs": 971.120792, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "catchingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + } + } + }, + { + "name": "rowsAfterCatching", + "value": { + "child": 0, + "parent": 1 + } + }, + { + "name": "passingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + }, + "text": "ConvexError: [Request ID: ce3f19a28007852d] Server Error\nUncaught ConvexError: Uncaught ConvexError: {\"array\":[\"one\",2,false,null],\"boolean\":true,\"code\":\"staleVersion\",\"int64\":\"9007199254740993n\",\"nested\":{\"count\":3,\"label\":\"nested\"},\"nil\":null,\"number\":17.25}\n at handler (../fixture/convex/annex/failures.ts:8:11)\n\n" + } + }, + { + "name": "ordinaryErrorAtParent", + "value": { + "property": null, + "message": "Error: Uncaught Error: plain failure: thrower-annex-babb5639-4b87-4ac2-9340-de67ec1a6249\n at handler (../fixture/convex/annex/failures.ts:26:23)\n" + } + }, + { + "name": "ordinaryErrorAtClient", + "value": { + "property": null, + "message": "Error: [Request ID: bd490650396029eb] Server Error\nUncaught Error: Uncaught Error: plain failure: thrower-annex-babb5639-4b87-4ac2-9340-de67ec1a6249\n at handler (../fixture/convex/annex/failures.ts:26:23)\n\n" + } + }, + { + "name": "throwBoundarySummary", + "value": { + "boundary": "annex", + "caughtDataUnchanged": true, + "passedDataUnchanged": true, + "childRows": 0, + "parentRows": 1, + "plainErrorFromThrower": true, + "writeGuardReached": true + } + } + ] + }, + { + "name": "An ordinary client cannot call a component's function (spec:platform.native-harness.ordinary-client-refused-component-function) > executes the bound example against a fresh world", + "file": "tests/native/ordinary-client-refused-component-function.test.ts", + "result": "passed", + "durationMs": 4696.539000000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "componentSocketSummary", + "value": { + "sends": 3, + "backendCloses": 3, + "answer": "no answer", + "socketWaitMs": 3000 + } + } + ] + }, + { + "name": "Probe 2: ConvexError data through a nested mutation (spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.test.ts", + "result": "passed", + "durationMs": 988.9565420000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "catchingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + } + } + }, + { + "name": "rowsAfterCatching", + "value": { + "child": 0, + "parent": 1 + } + }, + { + "name": "passingParent", + "value": { + "isConvexError": true, + "data": { + "array": [ + "one", + 2, + false, + null + ], + "boolean": true, + "code": "staleVersion", + "int64": { + "$integer": "AQAAAAAAIAA=" + }, + "nested": { + "count": 3, + "label": "nested" + }, + "nil": null, + "number": 17.25 + }, + "text": "ConvexError: [Request ID: 20f4b43c381e25f4] Server Error\nUncaught ConvexError: Uncaught ConvexError: {\"array\":[\"one\",2,false,null],\"boolean\":true,\"code\":\"staleVersion\",\"int64\":\"9007199254740993n\",\"nested\":{\"count\":3,\"label\":\"nested\"},\"nil\":null,\"number\":17.25}\n at handler (../fixture/convex/failures.ts:21:11)\n\n" + } + }, + { + "name": "ordinaryErrorAtParent", + "value": { + "property": null, + "message": "Error: Uncaught Error: plain failure: thrower-nested-4dfa993d-3956-4563-8ea8-6bf51aad5938\n at handler (../fixture/convex/failures.ts:39:23)\n" + } + }, + { + "name": "ordinaryErrorAtClient", + "value": { + "property": null, + "message": "Error: [Request ID: 7e4538ad9d56064d] Server Error\nUncaught Error: Uncaught Error: plain failure: thrower-nested-4dfa993d-3956-4563-8ea8-6bf51aad5938\n at handler (../fixture/convex/failures.ts:39:23)\n\n" + } + }, + { + "name": "throwBoundarySummary", + "value": { + "boundary": "nested", + "caughtDataUnchanged": true, + "passedDataUnchanged": true, + "childRows": 0, + "parentRows": 1, + "plainErrorFromThrower": true, + "writeGuardReached": true + } + } + ] + }, + { + "name": "An ordinary client cannot call an internal function (spec:platform.native-harness.ordinary-client-refused-internal-function) > executes the bound example against a fresh world", + "file": "tests/native/ordinary-client-refused-internal-function.test.ts", + "result": "passed", + "durationMs": 983.1294170000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 5: a subscription to a parent query follows the component's data (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.test.ts", + "result": "passed", + "durationMs": 1023.952667, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "subscriptionSummary", + "value": { + "waitMs": 5000, + "observationAfterDeliveryMs": 100, + "values": [ + { + "id": "j97ay0hdkffw1gks7j7w62n34h8ffpbt", + "label": "changed" + } + ], + "error": null + } + } + ] + }, + { + "name": "native: a taken --port causes fresh port pairs and the third start succeeds", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 3121.740209, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "7c85c21b3848b0fe4f6f1d89e27d828b4946b1cc0823689f5cd9b25ffcd107e7", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "A token signed by the fixture issuer yields the caller's identity (spec:platform.native-harness.fixture-issuer-token-yields-identity) > executes the bound example against a fresh world", + "file": "tests/native/fixture-issuer-token-yields-identity.test.ts", + "result": "passed", + "durationMs": 985.85275, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: a taken --site-proxy-port causes fresh port pairs and the third start succeeds", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 1116.93725, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "8816c267d781c84975d211a9e2e9a7d83dac639d259771c211fb8051af38ae5e", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "Probe 1: the HTTP client and a caller's retry (spec:facts.f05-react-client-retries-until-confirmed.probe-1-http-client-retry) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.test.ts", + "result": "passed", + "durationMs": 1149.135958, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "requestsAfterEachCall", + "value": [ + 1, + 2 + ] + }, + { + "name": "httpRetrySummary", + "value": { + "rows": 2, + "requestsAfterCall": [ + 1, + 2 + ] + } + } + ] + }, + { + "name": "Probe 5: pages of a component list stay contiguous under live writes (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.test.ts", + "result": "passed", + "durationMs": 1267.1406670000001, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "initialPinnedPage0", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j97cc7kh9rapdkfj4k0znx42298fetvp", + "j97dvr58g8t5jkppggjhmqbsqn8fe5a3", + "j97c3s2707gt7qkw9kjqhcdrwn8fftbg", + "j979fnnwymqjk7058jz1t3csts8ffp8z", + "j97cs4fymwcppwy36wqhhsckp98ff2sh", + "j973rpy3rrrg2jrz55mbm8wen18ff1wd", + "j9700cv7t0sxneagcpe8xq8js98ffvxv", + "j970qb0x6g59cddqz6mrr802bh8fesyx", + "j97fs6jvrzw4wvm2j4v51kcyms8fef89", + "j97eg38n9ph7z75jqb6ktz59sh8ff8gf" + ] + } + }, + { + "name": "initialPinnedPage1", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j97d1kagqtkw8nwfg1gt8nvsf98ffwj3", + "j979yd8mnmzvpn0m8831pn4akd8ffyk7", + "j9723ex2z5yaxhd4b82wf8w5158ff22a", + "j97ej8r0ncyaj6qb472msm0z3x8fe26b", + "j974m95npx8ejkxn2vrb3c2knn8ff9an", + "j9783yetw296at3kyfs013wwb58ff5p2", + "j972nt8rst7xxwee6pk8jnfp298fe38v", + "j97f1t68y2vxy3r2s6rt8ww8nh8fe8zc", + "j97cf23wj2rdnb3x24hhpsdv158ffbb3", + "j970t5cnpnpjtvv799ajyn0qfx8ffex5" + ] + } + }, + { + "name": "initialPinnedPage2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j971v61frg7h25qhv6hq9xfzmx8ff0dt", + "j976rjh42neyensj2fhr9dd07n8ffvsy", + "j97476sx2dyk0c5krk6pp64npx8fehf0", + "j97eh480pve476sz5g4365d0gs8ffw81", + "j979b149a4db0f1tfxkpqt3m1d8fe4my", + "j976njr1cm5g4fkbbad7vrxe358ff4zc", + "j97c1pwt19hxhkj1p7bh6gt2b18fea27", + "j974sp67gq4ek595g59vfmvnc18feda0", + "j971csq66yepcbydnyvskq6q6s8ff9z4", + "j971n9ghmgjmewsgjgq5vbzgm18fe2dq" + ] + } + }, + { + "name": "initialPinnedPage3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "initialPinnedPages", + "value": [ + { + "rows": 10, + "isDone": false + }, + { + "rows": 10, + "isDone": false + }, + { + "rows": 10, + "isDone": false + }, + { + "rows": 0, + "isDone": true + } + ] + }, + { + "name": "pinnedPageAfterInsert1", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j97d1kagqtkw8nwfg1gt8nvsf98ffwj3", + "j979yd8mnmzvpn0m8831pn4akd8ffyk7", + "j9723ex2z5yaxhd4b82wf8w5158ff22a", + "j97ej8r0ncyaj6qb472msm0z3x8fe26b", + "j974m95npx8ejkxn2vrb3c2knn8ff9an", + "j9783yetw296at3kyfs013wwb58ff5p2", + "j972nt8rst7xxwee6pk8jnfp298fe38v", + "j97f1t68y2vxy3r2s6rt8ww8nh8fe8zc", + "j97cf23wj2rdnb3x24hhpsdv158ffbb3", + "j970t5cnpnpjtvv799ajyn0qfx8ffex5" + ] + } + }, + { + "name": "pinnedPageAfterInsert2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j971v61frg7h25qhv6hq9xfzmx8ff0dt", + "j976rjh42neyensj2fhr9dd07n8ffvsy", + "j97476sx2dyk0c5krk6pp64npx8fehf0", + "j97eh480pve476sz5g4365d0gs8ffw81", + "j979b149a4db0f1tfxkpqt3m1d8fe4my", + "j976njr1cm5g4fkbbad7vrxe358ff4zc", + "j97c1pwt19hxhkj1p7bh6gt2b18fea27", + "j974sp67gq4ek595g59vfmvnc18feda0", + "j971csq66yepcbydnyvskq6q6s8ff9z4", + "j971n9ghmgjmewsgjgq5vbzgm18fe2dq" + ] + } + }, + { + "name": "pinnedPageAfterInsert3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "pinnedPageAfterInsert0", + "value": { + "rows": 15, + "status": "SplitRecommended", + "ids": [ + "j97cc7kh9rapdkfj4k0znx42298fetvp", + "j9700467qpnzkr587cf59mfpyh8feksy", + "j971xzgs6rz2b1n34t1x0sdqtx8ff2jw", + "j97a3haac23z5zy4rre3hne4f98ff80f", + "j97cybw878t77ve9fnvqn2y8ds8fewzp", + "j97ejtbk50nxz7z6vk969jmvax8fewag", + "j97dvr58g8t5jkppggjhmqbsqn8fe5a3", + "j97c3s2707gt7qkw9kjqhcdrwn8fftbg", + "j979fnnwymqjk7058jz1t3csts8ffp8z", + "j97cs4fymwcppwy36wqhhsckp98ff2sh", + "j973rpy3rrrg2jrz55mbm8wen18ff1wd", + "j9700cv7t0sxneagcpe8xq8js98ffvxv", + "j970qb0x6g59cddqz6mrr802bh8fesyx", + "j97fs6jvrzw4wvm2j4v51kcyms8fef89", + "j97eg38n9ph7z75jqb6ktz59sh8ff8gf" + ] + } + }, + { + "name": "pagesAfterInsert", + "value": [ + [ + "j97cc7kh9rapdkfj4k0znx42298fetvp", + "j9700467qpnzkr587cf59mfpyh8feksy", + "j971xzgs6rz2b1n34t1x0sdqtx8ff2jw", + "j97a3haac23z5zy4rre3hne4f98ff80f", + "j97cybw878t77ve9fnvqn2y8ds8fewzp", + "j97ejtbk50nxz7z6vk969jmvax8fewag", + "j97dvr58g8t5jkppggjhmqbsqn8fe5a3", + "j97c3s2707gt7qkw9kjqhcdrwn8fftbg", + "j979fnnwymqjk7058jz1t3csts8ffp8z", + "j97cs4fymwcppwy36wqhhsckp98ff2sh", + "j973rpy3rrrg2jrz55mbm8wen18ff1wd", + "j9700cv7t0sxneagcpe8xq8js98ffvxv", + "j970qb0x6g59cddqz6mrr802bh8fesyx", + "j97fs6jvrzw4wvm2j4v51kcyms8fef89", + "j97eg38n9ph7z75jqb6ktz59sh8ff8gf" + ], + [ + "j97d1kagqtkw8nwfg1gt8nvsf98ffwj3", + "j979yd8mnmzvpn0m8831pn4akd8ffyk7", + "j9723ex2z5yaxhd4b82wf8w5158ff22a", + "j97ej8r0ncyaj6qb472msm0z3x8fe26b", + "j974m95npx8ejkxn2vrb3c2knn8ff9an", + "j9783yetw296at3kyfs013wwb58ff5p2", + "j972nt8rst7xxwee6pk8jnfp298fe38v", + "j97f1t68y2vxy3r2s6rt8ww8nh8fe8zc", + "j97cf23wj2rdnb3x24hhpsdv158ffbb3", + "j970t5cnpnpjtvv799ajyn0qfx8ffex5" + ], + [ + "j971v61frg7h25qhv6hq9xfzmx8ff0dt", + "j976rjh42neyensj2fhr9dd07n8ffvsy", + "j97476sx2dyk0c5krk6pp64npx8fehf0", + "j97eh480pve476sz5g4365d0gs8ffw81", + "j979b149a4db0f1tfxkpqt3m1d8fe4my", + "j976njr1cm5g4fkbbad7vrxe358ff4zc", + "j97c1pwt19hxhkj1p7bh6gt2b18fea27", + "j974sp67gq4ek595g59vfmvnc18feda0", + "j971csq66yepcbydnyvskq6q6s8ff9z4", + "j971n9ghmgjmewsgjgq5vbzgm18fe2dq" + ], + [] + ] + }, + { + "name": "contiguityAfterInsert", + "value": { + "actual": [ + "j97cc7kh9rapdkfj4k0znx42298fetvp", + "j9700467qpnzkr587cf59mfpyh8feksy", + "j971xzgs6rz2b1n34t1x0sdqtx8ff2jw", + "j97a3haac23z5zy4rre3hne4f98ff80f", + "j97cybw878t77ve9fnvqn2y8ds8fewzp", + "j97ejtbk50nxz7z6vk969jmvax8fewag", + "j97dvr58g8t5jkppggjhmqbsqn8fe5a3", + "j97c3s2707gt7qkw9kjqhcdrwn8fftbg", + "j979fnnwymqjk7058jz1t3csts8ffp8z", + "j97cs4fymwcppwy36wqhhsckp98ff2sh", + "j973rpy3rrrg2jrz55mbm8wen18ff1wd", + "j9700cv7t0sxneagcpe8xq8js98ffvxv", + "j970qb0x6g59cddqz6mrr802bh8fesyx", + "j97fs6jvrzw4wvm2j4v51kcyms8fef89", + "j97eg38n9ph7z75jqb6ktz59sh8ff8gf", + "j97d1kagqtkw8nwfg1gt8nvsf98ffwj3", + "j979yd8mnmzvpn0m8831pn4akd8ffyk7", + "j9723ex2z5yaxhd4b82wf8w5158ff22a", + "j97ej8r0ncyaj6qb472msm0z3x8fe26b", + "j974m95npx8ejkxn2vrb3c2knn8ff9an", + "j9783yetw296at3kyfs013wwb58ff5p2", + "j972nt8rst7xxwee6pk8jnfp298fe38v", + "j97f1t68y2vxy3r2s6rt8ww8nh8fe8zc", + "j97cf23wj2rdnb3x24hhpsdv158ffbb3", + "j970t5cnpnpjtvv799ajyn0qfx8ffex5", + "j971v61frg7h25qhv6hq9xfzmx8ff0dt", + "j976rjh42neyensj2fhr9dd07n8ffvsy", + "j97476sx2dyk0c5krk6pp64npx8fehf0", + "j97eh480pve476sz5g4365d0gs8ffw81", + "j979b149a4db0f1tfxkpqt3m1d8fe4my", + "j976njr1cm5g4fkbbad7vrxe358ff4zc", + "j97c1pwt19hxhkj1p7bh6gt2b18fea27", + "j974sp67gq4ek595g59vfmvnc18feda0", + "j971csq66yepcbydnyvskq6q6s8ff9z4", + "j971n9ghmgjmewsgjgq5vbzgm18fe2dq" + ], + "expected": [ + "j97cc7kh9rapdkfj4k0znx42298fetvp", + "j9700467qpnzkr587cf59mfpyh8feksy", + "j971xzgs6rz2b1n34t1x0sdqtx8ff2jw", + "j97a3haac23z5zy4rre3hne4f98ff80f", + "j97cybw878t77ve9fnvqn2y8ds8fewzp", + "j97ejtbk50nxz7z6vk969jmvax8fewag", + "j97dvr58g8t5jkppggjhmqbsqn8fe5a3", + "j97c3s2707gt7qkw9kjqhcdrwn8fftbg", + "j979fnnwymqjk7058jz1t3csts8ffp8z", + "j97cs4fymwcppwy36wqhhsckp98ff2sh", + "j973rpy3rrrg2jrz55mbm8wen18ff1wd", + "j9700cv7t0sxneagcpe8xq8js98ffvxv", + "j970qb0x6g59cddqz6mrr802bh8fesyx", + "j97fs6jvrzw4wvm2j4v51kcyms8fef89", + "j97eg38n9ph7z75jqb6ktz59sh8ff8gf", + "j97d1kagqtkw8nwfg1gt8nvsf98ffwj3", + "j979yd8mnmzvpn0m8831pn4akd8ffyk7", + "j9723ex2z5yaxhd4b82wf8w5158ff22a", + "j97ej8r0ncyaj6qb472msm0z3x8fe26b", + "j974m95npx8ejkxn2vrb3c2knn8ff9an", + "j9783yetw296at3kyfs013wwb58ff5p2", + "j972nt8rst7xxwee6pk8jnfp298fe38v", + "j97f1t68y2vxy3r2s6rt8ww8nh8fe8zc", + "j97cf23wj2rdnb3x24hhpsdv158ffbb3", + "j970t5cnpnpjtvv799ajyn0qfx8ffex5", + "j971v61frg7h25qhv6hq9xfzmx8ff0dt", + "j976rjh42neyensj2fhr9dd07n8ffvsy", + "j97476sx2dyk0c5krk6pp64npx8fehf0", + "j97eh480pve476sz5g4365d0gs8ffw81", + "j979b149a4db0f1tfxkpqt3m1d8fe4my", + "j976njr1cm5g4fkbbad7vrxe358ff4zc", + "j97c1pwt19hxhkj1p7bh6gt2b18fea27", + "j974sp67gq4ek595g59vfmvnc18feda0", + "j971csq66yepcbydnyvskq6q6s8ff9z4", + "j971n9ghmgjmewsgjgq5vbzgm18fe2dq" + ] + } + }, + { + "name": "pinnedPageAfterDelete0", + "value": { + "rows": 15, + "status": "SplitRecommended", + "ids": [ + "j97cc7kh9rapdkfj4k0znx42298fetvp", + "j9700467qpnzkr587cf59mfpyh8feksy", + "j971xzgs6rz2b1n34t1x0sdqtx8ff2jw", + "j97a3haac23z5zy4rre3hne4f98ff80f", + "j97cybw878t77ve9fnvqn2y8ds8fewzp", + "j97ejtbk50nxz7z6vk969jmvax8fewag", + "j97dvr58g8t5jkppggjhmqbsqn8fe5a3", + "j97c3s2707gt7qkw9kjqhcdrwn8fftbg", + "j979fnnwymqjk7058jz1t3csts8ffp8z", + "j97cs4fymwcppwy36wqhhsckp98ff2sh", + "j973rpy3rrrg2jrz55mbm8wen18ff1wd", + "j9700cv7t0sxneagcpe8xq8js98ffvxv", + "j970qb0x6g59cddqz6mrr802bh8fesyx", + "j97fs6jvrzw4wvm2j4v51kcyms8fef89", + "j97eg38n9ph7z75jqb6ktz59sh8ff8gf" + ] + } + }, + { + "name": "pinnedPageAfterDelete2", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j971v61frg7h25qhv6hq9xfzmx8ff0dt", + "j976rjh42neyensj2fhr9dd07n8ffvsy", + "j97476sx2dyk0c5krk6pp64npx8fehf0", + "j97eh480pve476sz5g4365d0gs8ffw81", + "j979b149a4db0f1tfxkpqt3m1d8fe4my", + "j976njr1cm5g4fkbbad7vrxe358ff4zc", + "j97c1pwt19hxhkj1p7bh6gt2b18fea27", + "j974sp67gq4ek595g59vfmvnc18feda0", + "j971csq66yepcbydnyvskq6q6s8ff9z4", + "j971n9ghmgjmewsgjgq5vbzgm18fe2dq" + ] + } + }, + { + "name": "pinnedPageAfterDelete3", + "value": { + "rows": 0, + "status": null, + "ids": [] + } + }, + { + "name": "pinnedPageAfterDelete1", + "value": { + "rows": 9, + "status": null, + "ids": [ + "j97d1kagqtkw8nwfg1gt8nvsf98ffwj3", + "j9723ex2z5yaxhd4b82wf8w5158ff22a", + "j97ej8r0ncyaj6qb472msm0z3x8fe26b", + "j974m95npx8ejkxn2vrb3c2knn8ff9an", + "j9783yetw296at3kyfs013wwb58ff5p2", + "j972nt8rst7xxwee6pk8jnfp298fe38v", + "j97f1t68y2vxy3r2s6rt8ww8nh8fe8zc", + "j97cf23wj2rdnb3x24hhpsdv158ffbb3", + "j970t5cnpnpjtvv799ajyn0qfx8ffex5" + ] + } + }, + { + "name": "contiguitySummary", + "value": { + "pages": 4, + "rowsAfterInsert": 35, + "rowsAfterDelete": 34, + "removed": "j979yd8mnmzvpn0m8831pn4akd8ffyk7", + "actual": [ + "j97cc7kh9rapdkfj4k0znx42298fetvp", + "j9700467qpnzkr587cf59mfpyh8feksy", + "j971xzgs6rz2b1n34t1x0sdqtx8ff2jw", + "j97a3haac23z5zy4rre3hne4f98ff80f", + "j97cybw878t77ve9fnvqn2y8ds8fewzp", + "j97ejtbk50nxz7z6vk969jmvax8fewag", + "j97dvr58g8t5jkppggjhmqbsqn8fe5a3", + "j97c3s2707gt7qkw9kjqhcdrwn8fftbg", + "j979fnnwymqjk7058jz1t3csts8ffp8z", + "j97cs4fymwcppwy36wqhhsckp98ff2sh", + "j973rpy3rrrg2jrz55mbm8wen18ff1wd", + "j9700cv7t0sxneagcpe8xq8js98ffvxv", + "j970qb0x6g59cddqz6mrr802bh8fesyx", + "j97fs6jvrzw4wvm2j4v51kcyms8fef89", + "j97eg38n9ph7z75jqb6ktz59sh8ff8gf", + "j97d1kagqtkw8nwfg1gt8nvsf98ffwj3", + "j9723ex2z5yaxhd4b82wf8w5158ff22a", + "j97ej8r0ncyaj6qb472msm0z3x8fe26b", + "j974m95npx8ejkxn2vrb3c2knn8ff9an", + "j9783yetw296at3kyfs013wwb58ff5p2", + "j972nt8rst7xxwee6pk8jnfp298fe38v", + "j97f1t68y2vxy3r2s6rt8ww8nh8fe8zc", + "j97cf23wj2rdnb3x24hhpsdv158ffbb3", + "j970t5cnpnpjtvv799ajyn0qfx8ffex5", + "j971v61frg7h25qhv6hq9xfzmx8ff0dt", + "j976rjh42neyensj2fhr9dd07n8ffvsy", + "j97476sx2dyk0c5krk6pp64npx8fehf0", + "j97eh480pve476sz5g4365d0gs8ffw81", + "j979b149a4db0f1tfxkpqt3m1d8fe4my", + "j976njr1cm5g4fkbbad7vrxe358ff4zc", + "j97c1pwt19hxhkj1p7bh6gt2b18fea27", + "j974sp67gq4ek595g59vfmvnc18feda0", + "j971csq66yepcbydnyvskq6q6s8ff9z4", + "j971n9ghmgjmewsgjgq5vbzgm18fe2dq" + ], + "expected": [ + "j97cc7kh9rapdkfj4k0znx42298fetvp", + "j9700467qpnzkr587cf59mfpyh8feksy", + "j971xzgs6rz2b1n34t1x0sdqtx8ff2jw", + "j97a3haac23z5zy4rre3hne4f98ff80f", + "j97cybw878t77ve9fnvqn2y8ds8fewzp", + "j97ejtbk50nxz7z6vk969jmvax8fewag", + "j97dvr58g8t5jkppggjhmqbsqn8fe5a3", + "j97c3s2707gt7qkw9kjqhcdrwn8fftbg", + "j979fnnwymqjk7058jz1t3csts8ffp8z", + "j97cs4fymwcppwy36wqhhsckp98ff2sh", + "j973rpy3rrrg2jrz55mbm8wen18ff1wd", + "j9700cv7t0sxneagcpe8xq8js98ffvxv", + "j970qb0x6g59cddqz6mrr802bh8fesyx", + "j97fs6jvrzw4wvm2j4v51kcyms8fef89", + "j97eg38n9ph7z75jqb6ktz59sh8ff8gf", + "j97d1kagqtkw8nwfg1gt8nvsf98ffwj3", + "j9723ex2z5yaxhd4b82wf8w5158ff22a", + "j97ej8r0ncyaj6qb472msm0z3x8fe26b", + "j974m95npx8ejkxn2vrb3c2knn8ff9an", + "j9783yetw296at3kyfs013wwb58ff5p2", + "j972nt8rst7xxwee6pk8jnfp298fe38v", + "j97f1t68y2vxy3r2s6rt8ww8nh8fe8zc", + "j97cf23wj2rdnb3x24hhpsdv158ffbb3", + "j970t5cnpnpjtvv799ajyn0qfx8ffex5", + "j971v61frg7h25qhv6hq9xfzmx8ff0dt", + "j976rjh42neyensj2fhr9dd07n8ffvsy", + "j97476sx2dyk0c5krk6pp64npx8fehf0", + "j97eh480pve476sz5g4365d0gs8ffw81", + "j979b149a4db0f1tfxkpqt3m1d8fe4my", + "j976njr1cm5g4fkbbad7vrxe358ff4zc", + "j97c1pwt19hxhkj1p7bh6gt2b18fea27", + "j974sp67gq4ek595g59vfmvnc18feda0", + "j971csq66yepcbydnyvskq6q6s8ff9z4", + "j971n9ghmgjmewsgjgq5vbzgm18fe2dq" + ] + } + } + ] + }, + { + "name": "Probe 5: a page that outgrows its read cap comes back through the parent as a split (spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed) > executes the bound example against a fresh world", + "file": "tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.test.ts", + "result": "passed", + "durationMs": 1227.290875, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "initialPinnedPage", + "value": { + "rows": 10, + "status": null, + "ids": [ + "j979acc29r432qddqjfp11bb0d8ffbj3", + "j97c533sgp3mz869dnxvqxcvk58fezqd", + "j9756hbqnq245w1w265c9jq52s8feedz", + "j97c6w8qa2y0kqw1ns9wmkjdgd8ffm2g", + "j975d3v9v850ngfrb4zxm932fs8ff4gn", + "j97a2xcq1yfmjq3yp2cn6597858fffqz", + "j970v7x09ny356hj8tgnbfbcds8fe3ef", + "j97cj17c68x7e07vt5he0wy0058fe1d7", + "j970fvztqa2nseny5p8x9ms9xn8ff9cv", + "j97fj87jg4fby5pkexrpesf9tx8ffvc1" + ] + } + }, + { + "name": "splitPage", + "value": { + "rows": 20, + "status": "SplitRequired", + "ids": [ + "j979acc29r432qddqjfp11bb0d8ffbj3", + "j97e5q5tdbnzsann397p1tg0pd8ffdar", + "j975a04esvyxsyaejmpjat83cx8ffcka", + "j978bgqd35gexx12jqcj479rjd8fem7k", + "j97dzhera4htgnpwjyzffjf3958feckd", + "j973v0dwqptdsjz3e5d55mehj98ffv8f", + "j973cpsh106cfs8jyr34smpe5d8ffkqt", + "j97f7bpj622cg704qsmzcymzqh8fea00", + "j975k4e1c7bkvmvgj8j2q0pg498ffqsj", + "j972e3fqt10tgwbfm3kkezcayh8ff97k", + "j97bt313g07wjy6z2ptwxtbzmh8fesns", + "j972aa06cgpwtgyckgtdr86pzh8ff19e", + "j974r4e94wzt758bzks6v35ndn8ffzye", + "j97d37vtbjjv32bwpp67xkzdp98ffx8m", + "j975r5ygs8ja2ag457bbn9tywn8ffjgf", + "j97d4pqg6dpszfnxcdn1my08yd8fenrk", + "j97c533sgp3mz869dnxvqxcvk58fezqd", + "j9756hbqnq245w1w265c9jq52s8feedz", + "j97c6w8qa2y0kqw1ns9wmkjdgd8ffm2g", + "j975d3v9v850ngfrb4zxm932fs8ff4gn" + ] + } + }, + { + "name": "relayedSplit", + "value": { + "status": "SplitRequired", + "splitCursor": "[0.5625,1790884275646.638,\"j972e3fqt10tgwbfm3kkezcayh8ff97k\"]", + "continueCursor": "[4,1790884275586.389,\"j975d3v9v850ngfrb4zxm932fs8ff4gn\"]", + "endCursor": "[9,1790884275586.3901,\"j97fj87jg4fby5pkexrpesf9tx8ffvc1\"]", + "rows": 20 + } + }, + { + "name": "splitSummary", + "value": { + "pageStatus": "SplitRequired", + "left": [ + "j979acc29r432qddqjfp11bb0d8ffbj3", + "j97e5q5tdbnzsann397p1tg0pd8ffdar", + "j975a04esvyxsyaejmpjat83cx8ffcka", + "j978bgqd35gexx12jqcj479rjd8fem7k", + "j97dzhera4htgnpwjyzffjf3958feckd", + "j973v0dwqptdsjz3e5d55mehj98ffv8f", + "j973cpsh106cfs8jyr34smpe5d8ffkqt", + "j97f7bpj622cg704qsmzcymzqh8fea00", + "j975k4e1c7bkvmvgj8j2q0pg498ffqsj", + "j972e3fqt10tgwbfm3kkezcayh8ff97k" + ], + "right": [ + "j97bt313g07wjy6z2ptwxtbzmh8fesns", + "j972aa06cgpwtgyckgtdr86pzh8ff19e", + "j974r4e94wzt758bzks6v35ndn8ffzye", + "j97d37vtbjjv32bwpp67xkzdp98ffx8m", + "j975r5ygs8ja2ag457bbn9tywn8ffjgf", + "j97d4pqg6dpszfnxcdn1my08yd8fenrk", + "j97c533sgp3mz869dnxvqxcvk58fezqd", + "j9756hbqnq245w1w265c9jq52s8feedz", + "j97c6w8qa2y0kqw1ns9wmkjdgd8ffm2g", + "j975d3v9v850ngfrb4zxm932fs8ff4gn", + "j97a2xcq1yfmjq3yp2cn6597858fffqz", + "j970v7x09ny356hj8tgnbfbcds8fe3ef", + "j97cj17c68x7e07vt5he0wy0058fe1d7", + "j970fvztqa2nseny5p8x9ms9xn8ff9cv", + "j97fj87jg4fby5pkexrpesf9tx8ffvc1" + ], + "wholeRange": [ + "j979acc29r432qddqjfp11bb0d8ffbj3", + "j97e5q5tdbnzsann397p1tg0pd8ffdar", + "j975a04esvyxsyaejmpjat83cx8ffcka", + "j978bgqd35gexx12jqcj479rjd8fem7k", + "j97dzhera4htgnpwjyzffjf3958feckd", + "j973v0dwqptdsjz3e5d55mehj98ffv8f", + "j973cpsh106cfs8jyr34smpe5d8ffkqt", + "j97f7bpj622cg704qsmzcymzqh8fea00", + "j975k4e1c7bkvmvgj8j2q0pg498ffqsj", + "j972e3fqt10tgwbfm3kkezcayh8ff97k", + "j97bt313g07wjy6z2ptwxtbzmh8fesns", + "j972aa06cgpwtgyckgtdr86pzh8ff19e", + "j974r4e94wzt758bzks6v35ndn8ffzye", + "j97d37vtbjjv32bwpp67xkzdp98ffx8m", + "j975r5ygs8ja2ag457bbn9tywn8ffjgf", + "j97d4pqg6dpszfnxcdn1my08yd8fenrk", + "j97c533sgp3mz869dnxvqxcvk58fezqd", + "j9756hbqnq245w1w265c9jq52s8feedz", + "j97c6w8qa2y0kqw1ns9wmkjdgd8ffm2g", + "j975d3v9v850ngfrb4zxm932fs8ff4gn", + "j97a2xcq1yfmjq3yp2cn6597858fffqz", + "j970v7x09ny356hj8tgnbfbcds8fe3ef", + "j97cj17c68x7e07vt5he0wy0058fe1d7", + "j970fvztqa2nseny5p8x9ms9xn8ff9cv", + "j97fj87jg4fby5pkexrpesf9tx8ffvc1" + ] + } + } + ] + }, + { + "name": "The admin key acting as an identity reaches an internal function (spec:platform.native-harness.admin-key-acting-as-identity-reaches-internal-function) > executes the bound example against a fresh world", + "file": "tests/native/admin-key-acting-as-identity-reaches-internal-function.test.ts", + "result": "passed", + "durationMs": 1126.111583, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: a component query that calls ctx.auth.getUserIdentity() completes, and its answer is recorded", + "file": "tests/native/component-auth.test.ts", + "result": "passed", + "durationMs": 1036.5114999999998, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "identitySeenInsideComponent", + "value": { + "answered": "an identity", + "issuer": "https://fixture-issuer.test", + "subject": "user-1" + } + } + ] + }, + { + "name": "native: the parent sweep verifies and kills two real owned backend processes", + "file": "tests/native/harness-sweep.test.ts", + "result": "passed", + "durationMs": 310.28874999999994, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + }, + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native review: failed inspection must not silently discard ownership", + "file": "tests/native/harness-sweep.test.ts", + "result": "passed", + "durationMs": 155.96012499999995, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: three taken ports fail clearly and remove the failed start's storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 3999.128791, + "errors": [], + "backends": [], + "measurements": [ + { + "name": "realBackendExecutableSha256", + "value": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + } + ] + }, + { + "name": "Probe 1: a backend restart with a mutation in flight (spec:facts.f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation) > executes the bound example against a fresh world", + "file": "tests/native/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.test.ts", + "result": "passed", + "durationMs": 13983.771791, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "trials", + "value": [ + { + "trial": "restart 0", + "delayMs": 0, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 1", + "delayMs": 0, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 2", + "delayMs": 0, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 3", + "delayMs": 1, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 4", + "delayMs": 2, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 5", + "delayMs": 4, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 6", + "delayMs": 8, + "committedBeforeKill": false, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 7", + "delayMs": 16, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 8", + "delayMs": 32, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + }, + { + "trial": "restart 9", + "delayMs": 64, + "committedBeforeKill": true, + "pendingAtKill": true, + "pendingAtRelease": true, + "resolved": true, + "rows": 1 + } + ] + }, + { + "name": "restartSummary", + "value": { + "trials": 10, + "resolvedAfterRestart": 10, + "resolvedBeforeRelease": 0, + "killedBeforeCommit": 6, + "killedAfterCommit": 4, + "rowCounts": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + } + ] + }, + { + "name": "native: stop escalates to SIGKILL when the owned process ignores SIGINT and keeps its storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 5349.099708, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "df728e7ca0eb52d89890c59f2be280ca82f3caa7213c9f1a2329516806e799db", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: a log mark from before a kill cannot be read after restart", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 1070.9417500000018, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "Probe 4: a nested call draws on the caller's transaction limits (spec:facts.f13-transactions-have-limits.probe-4-nested-call-shares-limits) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-nested-call-shares-limits.test.ts", + "result": "passed", + "durationMs": 22370.256333, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "seededPayloads", + "value": { + "size": 921600, + "parentCount": 10, + "childCount": 10 + } + }, + { + "name": "combinedReadOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 4ad104e02d09f13e] Server Error\nUncaught Error: Uncaught Error: Too many bytes read in a single function execution (limit: 16777216 bytes). Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\n at async handler (../fixture/convex/limits.ts:47:11)\n\n at async handler (../fixture/convex/limits.ts:78:34)\n" + } + }, + { + "name": "readTogether", + "value": "fails on the bytes-read limit" + }, + { + "name": "parentReadOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childReadOutcome", + "value": { + "outcome": "returns", + "error": null + } + }, + { + "name": "readsAlone", + "value": { + "parent": "commits", + "child": "returns", + "parentDocuments": 10, + "childDocuments": 10, + "childPayloadBytes": 9216000 + } + }, + { + "name": "sharedReadMetrics", + "value": { + "byteGrowth": 921684, + "documentGrowth": 1, + "childBytes": 921684, + "childDocuments": 1, + "payloadBytes": 921600 + } + }, + { + "name": "combinedWriteOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 16a8f8dc61757b52] Server Error\nUncaught Error: Uncaught Error: Too many bytes written in a single function execution (limit: 16 MiB)\n at async handler (../fixture/convex/limits.ts:23:35)\n\n at async handler (../fixture/convex/limits.ts:102:8)\n" + } + }, + { + "name": "writesTogether", + "value": "fails on the bytes-written limit" + }, + { + "name": "failedWriteRows", + "value": 0 + }, + { + "name": "parentWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "parentWriteRows", + "value": 10 + }, + { + "name": "childWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childWriteRows", + "value": 20 + }, + { + "name": "limitSummary", + "value": { + "boundary": "nested", + "combinedRead": "fails on the bytes-read limit", + "combinedWrite": "fails on the bytes-written limit", + "failedWriteRows": 0, + "committedRows": 20 + } + } + ] + }, + { + "name": "Probe 4: a component call draws on the caller's transaction limits (spec:facts.f13-transactions-have-limits.probe-4-component-call-shares-limits) > executes the bound example against a fresh world", + "file": "tests/native/f13-transactions-have-limits.probe-4-component-call-shares-limits.test.ts", + "result": "passed", + "durationMs": 22538.757959, + "errors": [], + "backends": [ + { + "composition": "fixture", + "installedLayers": [], + "executable": { + "release": "precompiled-2026-09-28-5c7cb5b", + "sha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676", + "source": "cache" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [ + { + "name": "seededPayloads", + "value": { + "size": 921600, + "parentCount": 10, + "childCount": 10 + } + }, + { + "name": "combinedReadOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: 22955f9136156a25] Server Error\nUncaught Error: Uncaught Error: Too many bytes read in a single function execution (limit: 16777216 bytes). Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\n at async handler (../fixture/convex/annex/limits.ts:34:11)\n\n at async handler (../fixture/convex/limits.ts:78:34)\n" + } + }, + { + "name": "readTogether", + "value": "fails on the bytes-read limit" + }, + { + "name": "parentReadOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childReadOutcome", + "value": { + "outcome": "returns", + "error": null + } + }, + { + "name": "readsAlone", + "value": { + "parent": "commits", + "child": "returns", + "parentDocuments": 10, + "childDocuments": 10, + "childPayloadBytes": 9216000 + } + }, + { + "name": "sharedReadMetrics", + "value": { + "byteGrowth": 921684, + "documentGrowth": 1, + "childBytes": 921684, + "childDocuments": 1, + "payloadBytes": 921600 + } + }, + { + "name": "combinedWriteOutcome", + "value": { + "outcome": "failed", + "error": "Error: [Request ID: c6c8748563fcdcf3] Server Error\nUncaught Error: Uncaught Error: Too many bytes written in a single function execution (limit: 16 MiB)\n at async handler (../fixture/convex/annex/limits.ts:8:35)\n\n at async handler (../fixture/convex/limits.ts:102:8)\n" + } + }, + { + "name": "writesTogether", + "value": "fails on the bytes-written limit" + }, + { + "name": "failedWriteRows", + "value": 0 + }, + { + "name": "parentWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "parentWriteRows", + "value": 10 + }, + { + "name": "childWriteOutcome", + "value": { + "outcome": "commits", + "error": null + } + }, + { + "name": "childWriteRows", + "value": 20 + }, + { + "name": "limitSummary", + "value": { + "boundary": "annex", + "combinedRead": "fails on the bytes-read limit", + "combinedWrite": "fails on the bytes-written limit", + "failedWriteRows": 0, + "committedRows": 20 + } + } + ] + }, + { + "name": "native: a restart that stalls after spawning reaps its process", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 30375.616208999996, + "errors": [], + "backends": [ + { + "composition": null, + "installedLayers": [], + "executable": { + "release": null, + "sha256": "a350ab72108a75ae0c9bfe365360740e7be674fe788d9844cb3e2f2d7c5908ff", + "source": "CONVEX_BACKEND_BINARY" + }, + "identitySource": { + "kind": "fixture issuer", + "issuer": "https://fixture-issuer.test" + }, + "environment": [ + "AUTH_APPLICATION_ID", + "AUTH_ISSUER", + "AUTH_JWKS" + ], + "dataset": "empty at start" + } + ], + "measurements": [] + }, + { + "name": "native: codegen interrupted by SIGINT removes its starting backend and storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 396.45908300000156, + "errors": [], + "backends": [], + "measurements": [] + }, + { + "name": "native: codegen interrupted by SIGTERM removes its starting backend and storage", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 295.71379100000195, + "errors": [], + "backends": [], + "measurements": [] + }, + { + "name": "native: backend output split across chunks is redacted before the rolling buffer clips it", + "file": "tests/native/harness-backend.test.ts", + "result": "passed", + "durationMs": 366.6687499999971, + "errors": [], + "backends": [], + "measurements": [] + } + ], + "unhandledErrors": [] +} diff --git a/fixture/convex/_generated/api.d.ts b/fixture/convex/_generated/api.d.ts new file mode 100644 index 0000000..18c936a --- /dev/null +++ b/fixture/convex/_generated/api.d.ts @@ -0,0 +1,63 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type * as failures from "../failures.js"; +import type * as identity from "../identity.js"; +import type * as limits from "../limits.js"; +import type * as list from "../list.js"; +import type * as markers from "../markers.js"; +import type * as notes from "../notes.js"; +import type * as readCost from "../readCost.js"; + +import type { + ApiFromModules, + FilterApi, + FunctionReference, +} from "convex/server"; + +declare const fullApi: ApiFromModules<{ + failures: typeof failures; + identity: typeof identity; + limits: typeof limits; + list: typeof list; + markers: typeof markers; + notes: typeof notes; + readCost: typeof readCost; +}>; + +/** + * A utility for referencing Convex functions in your app's public API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export declare const api: FilterApi< + typeof fullApi, + FunctionReference +>; + +/** + * A utility for referencing Convex functions in your app's internal API. + * + * Usage: + * ```js + * const myFunctionReference = internal.myModule.myFunction; + * ``` + */ +export declare const internal: FilterApi< + typeof fullApi, + FunctionReference +>; + +export declare const components: { + annex: import("../annex/_generated/component.js").ComponentApi<"annex">; +}; diff --git a/fixture/convex/_generated/api.js b/fixture/convex/_generated/api.js new file mode 100644 index 0000000..44bf985 --- /dev/null +++ b/fixture/convex/_generated/api.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { anyApi, componentsGeneric } from "convex/server"; + +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export const api = anyApi; +export const internal = anyApi; +export const components = componentsGeneric(); diff --git a/fixture/convex/_generated/dataModel.d.ts b/fixture/convex/_generated/dataModel.d.ts new file mode 100644 index 0000000..f97fd19 --- /dev/null +++ b/fixture/convex/_generated/dataModel.d.ts @@ -0,0 +1,60 @@ +/* eslint-disable */ +/** + * Generated data model types. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + DataModelFromSchemaDefinition, + DocumentByName, + TableNamesInDataModel, + SystemTableNames, +} from "convex/server"; +import type { GenericId } from "convex/values"; +import schema from "../schema.js"; + +/** + * The names of all of your Convex tables. + */ +export type TableNames = TableNamesInDataModel; + +/** + * The type of a document stored in Convex. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Doc = DocumentByName< + DataModel, + TableName +>; + +/** + * An identifier for a document in Convex. + * + * Convex documents are uniquely identified by their `Id`, which is accessible + * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). + * + * Documents can be loaded using `db.get(tableName, id)` in query and mutation functions. + * + * IDs are just strings at runtime, but this type can be used to distinguish them from other + * strings when type checking. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Id = + GenericId; + +/** + * A type describing your Convex data model. + * + * This type includes information about what tables you have, the type of + * documents stored in those tables, and the indexes defined on them. + * + * This type is used to parameterize methods like `queryGeneric` and + * `mutationGeneric` to make them type-safe. + */ +export type DataModel = DataModelFromSchemaDefinition; diff --git a/fixture/convex/_generated/server.d.ts b/fixture/convex/_generated/server.d.ts new file mode 100644 index 0000000..f235db4 --- /dev/null +++ b/fixture/convex/_generated/server.d.ts @@ -0,0 +1,162 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + ActionBuilder, + HttpActionBuilder, + MutationBuilder, + QueryBuilder, + GenericActionCtx, + GenericMutationCtx, + GenericQueryCtx, + GenericDatabaseReader, + GenericDatabaseWriter, +} from "convex/server"; +import type { DataModel } from "./dataModel.js"; + +/** + * Typesafe environment variables. + * + * This includes platform-provided env vars and any variables declared in + * `convex.config.ts`. + */ +type Env = { + readonly CONVEX_CLOUD_URL: string; + readonly CONVEX_SITE_URL: string; +}; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const query: QueryBuilder; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const internalQuery: QueryBuilder; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const mutation: MutationBuilder; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const internalMutation: MutationBuilder; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export declare const action: ActionBuilder; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export declare const internalAction: ActionBuilder; + +/** + * Define an HTTP action. + * + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export declare const httpAction: HttpActionBuilder; + +/** + * Typesafe environment variables. + * + * This includes platform-provided env vars and any variables declared in + * `convex.config.ts`. + */ +export declare const env: Env; + +/** + * A set of services for use within Convex query functions. + * + * The query context is passed as the first argument to any Convex query + * function run on the server. + * + * This differs from the {@link MutationCtx} because all of the services are + * read-only. + */ +export type QueryCtx = GenericQueryCtx; + +/** + * A set of services for use within Convex mutation functions. + * + * The mutation context is passed as the first argument to any Convex mutation + * function run on the server. + */ +export type MutationCtx = GenericMutationCtx; + +/** + * A set of services for use within Convex action functions. + * + * The action context is passed as the first argument to any Convex action + * function run on the server. + */ +export type ActionCtx = GenericActionCtx; + +/** + * An interface to read from the database within Convex query functions. + * + * The two entry points are {@link DatabaseReader.get}, which fetches a single + * document by its {@link Id}, or {@link DatabaseReader.query}, which starts + * building a query. + */ +export type DatabaseReader = GenericDatabaseReader; + +/** + * An interface to read from and write to the database within Convex mutation + * functions. + * + * Convex guarantees that all writes within a single mutation are + * executed atomically, so you never have to worry about partial writes leaving + * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) + * for the guarantees Convex provides your functions. + */ +export type DatabaseWriter = GenericDatabaseWriter; diff --git a/fixture/convex/_generated/server.js b/fixture/convex/_generated/server.js new file mode 100644 index 0000000..2dbe0db --- /dev/null +++ b/fixture/convex/_generated/server.js @@ -0,0 +1,101 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + actionGeneric, + httpActionGeneric, + queryGeneric, + mutationGeneric, + internalActionGeneric, + internalMutationGeneric, + internalQueryGeneric, +} from "convex/server"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const query = queryGeneric; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const internalQuery = internalQueryGeneric; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const mutation = mutationGeneric; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const internalMutation = internalMutationGeneric; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export const action = actionGeneric; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export const internalAction = internalActionGeneric; + +/** + * Define an HTTP action. + * + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export const httpAction = httpActionGeneric; + +/** + * Typesafe environment variables. + * + * This includes platform-provided env vars and any variables declared in + * `convex.config.ts`. + */ +export const env = process.env; diff --git a/fixture/convex/annex/_generated/api.ts b/fixture/convex/annex/_generated/api.ts new file mode 100644 index 0000000..e9076bc --- /dev/null +++ b/fixture/convex/annex/_generated/api.ts @@ -0,0 +1,60 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type * as failures from "../failures.js"; +import type * as identity from "../identity.js"; +import type * as limits from "../limits.js"; +import type * as list from "../list.js"; +import type * as notes from "../notes.js"; +import type * as readCost from "../readCost.js"; + +import type { + ApiFromModules, + FilterApi, + FunctionReference, +} from "convex/server"; +import { anyApi, componentsGeneric } from "convex/server"; + +const fullApi: ApiFromModules<{ + failures: typeof failures; + identity: typeof identity; + limits: typeof limits; + list: typeof list; + notes: typeof notes; + readCost: typeof readCost; +}> = anyApi as any; + +/** + * A utility for referencing Convex functions in your app's public API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export const api: FilterApi< + typeof fullApi, + FunctionReference +> = anyApi as any; + +/** + * A utility for referencing Convex functions in your app's internal API. + * + * Usage: + * ```js + * const myFunctionReference = internal.myModule.myFunction; + * ``` + */ +export const internal: FilterApi< + typeof fullApi, + FunctionReference +> = anyApi as any; + +export const components = componentsGeneric() as unknown as {}; diff --git a/fixture/convex/annex/_generated/component.ts b/fixture/convex/annex/_generated/component.ts new file mode 100644 index 0000000..5ebf629 --- /dev/null +++ b/fixture/convex/annex/_generated/component.ts @@ -0,0 +1,107 @@ +/* eslint-disable */ +/** + * Generated `ComponentApi` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { FunctionReference } from "convex/server"; + +/** + * A utility for referencing a Convex component's exposed API. + * + * Useful when expecting a parameter like `components.myComponent`. + * Usage: + * ```ts + * async function myFunction(ctx: QueryCtx, component: ComponentApi) { + * return ctx.runQuery(component.someFile.someQuery, { ...args }); + * } + * ``` + */ +export type ComponentApi = + { + failures: { + throwAfterWrite: FunctionReference< + "mutation", + "internal", + { data: any; kind: "convexError" | "plainError" }, + null, + Name + >; + }; + identity: { + caller: FunctionReference<"query", "internal", {}, any, Name>; + }; + limits: { + insertBlobs: FunctionReference< + "mutation", + "internal", + { count: number; group: string; size: number }, + null, + Name + >; + readBlobs: FunctionReference< + "query", + "internal", + { count: number; group: string }, + any, + Name + >; + }; + list: { + first: FunctionReference<"query", "internal", {}, any, Name>; + insert: FunctionReference< + "mutation", + "internal", + { rows: Array<{ label: string; position: number }> }, + null, + Name + >; + page: FunctionReference< + "query", + "internal", + { + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + }, + any, + Name + >; + relabel: FunctionReference< + "mutation", + "internal", + { id: string; label: string }, + null, + Name + >; + remove: FunctionReference< + "mutation", + "internal", + { id: string }, + null, + Name + >; + }; + notes: { + add: FunctionReference<"mutation", "internal", {}, null, Name>; + }; + readCost: { + readOne: FunctionReference< + "query", + "internal", + { cacheBuster: number; id: string }, + any, + Name + >; + seed: FunctionReference<"mutation", "internal", {}, string, Name>; + }; + }; diff --git a/fixture/convex/annex/_generated/dataModel.ts b/fixture/convex/annex/_generated/dataModel.ts new file mode 100644 index 0000000..f97fd19 --- /dev/null +++ b/fixture/convex/annex/_generated/dataModel.ts @@ -0,0 +1,60 @@ +/* eslint-disable */ +/** + * Generated data model types. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + DataModelFromSchemaDefinition, + DocumentByName, + TableNamesInDataModel, + SystemTableNames, +} from "convex/server"; +import type { GenericId } from "convex/values"; +import schema from "../schema.js"; + +/** + * The names of all of your Convex tables. + */ +export type TableNames = TableNamesInDataModel; + +/** + * The type of a document stored in Convex. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Doc = DocumentByName< + DataModel, + TableName +>; + +/** + * An identifier for a document in Convex. + * + * Convex documents are uniquely identified by their `Id`, which is accessible + * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). + * + * Documents can be loaded using `db.get(tableName, id)` in query and mutation functions. + * + * IDs are just strings at runtime, but this type can be used to distinguish them from other + * strings when type checking. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Id = + GenericId; + +/** + * A type describing your Convex data model. + * + * This type includes information about what tables you have, the type of + * documents stored in those tables, and the indexes defined on them. + * + * This type is used to parameterize methods like `queryGeneric` and + * `mutationGeneric` to make them type-safe. + */ +export type DataModel = DataModelFromSchemaDefinition; diff --git a/fixture/convex/annex/_generated/server.ts b/fixture/convex/annex/_generated/server.ts new file mode 100644 index 0000000..1a7688a --- /dev/null +++ b/fixture/convex/annex/_generated/server.ts @@ -0,0 +1,169 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + ActionBuilder, + HttpActionBuilder, + MutationBuilder, + QueryBuilder, + GenericActionCtx, + GenericMutationCtx, + GenericQueryCtx, + GenericDatabaseReader, + GenericDatabaseWriter, +} from "convex/server"; +import { + actionGeneric, + httpActionGeneric, + queryGeneric, + mutationGeneric, + internalActionGeneric, + internalMutationGeneric, + internalQueryGeneric, +} from "convex/server"; +import type { DataModel } from "./dataModel.js"; + +/** + * Typesafe environment variables. + * + * This includes platform-provided env vars and any variables declared in + * `convex.config.ts`. + */ +type Env = { + readonly CONVEX_CLOUD_URL: string; + readonly CONVEX_SITE_URL: string; +}; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const query: QueryBuilder = queryGeneric; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const internalQuery: QueryBuilder = + internalQueryGeneric; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const mutation: MutationBuilder = mutationGeneric; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const internalMutation: MutationBuilder = + internalMutationGeneric; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export const action: ActionBuilder = actionGeneric; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export const internalAction: ActionBuilder = + internalActionGeneric; + +/** + * Define an HTTP action. + * + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export const httpAction: HttpActionBuilder = httpActionGeneric; +export const env: Env = (globalThis as unknown as { process: { env: Env } }) + .process.env; + +/** + * A set of services for use within Convex query functions. + * + * The query context is passed as the first argument to any Convex query + * function run on the server. + * + * If you're using code generation, use the `QueryCtx` type in `convex/_generated/server.d.ts` instead. + */ +export type QueryCtx = GenericQueryCtx; + +/** + * A set of services for use within Convex mutation functions. + * + * The mutation context is passed as the first argument to any Convex mutation + * function run on the server. + * + * If you're using code generation, use the `MutationCtx` type in `convex/_generated/server.d.ts` instead. + */ +export type MutationCtx = GenericMutationCtx; + +/** + * A set of services for use within Convex action functions. + * + * The action context is passed as the first argument to any Convex action + * function run on the server. + */ +export type ActionCtx = GenericActionCtx; + +/** + * An interface to read from the database within Convex query functions. + * + * The two entry points are {@link DatabaseReader.get}, which fetches a single + * document by its {@link Id}, or {@link DatabaseReader.query}, which starts + * building a query. + */ +export type DatabaseReader = GenericDatabaseReader; + +/** + * An interface to read from and write to the database within Convex mutation + * functions. + * + * Convex guarantees that all writes within a single mutation are + * executed atomically, so you never have to worry about partial writes leaving + * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) + * for the guarantees Convex provides your functions. + */ +export type DatabaseWriter = GenericDatabaseWriter; diff --git a/fixture/convex/annex/convex.config.ts b/fixture/convex/annex/convex.config.ts new file mode 100644 index 0000000..4078f07 --- /dev/null +++ b/fixture/convex/annex/convex.config.ts @@ -0,0 +1,2 @@ +import { defineComponent } from "convex/server"; +export default defineComponent("annex"); diff --git a/fixture/convex/annex/failures.ts b/fixture/convex/annex/failures.ts new file mode 100644 index 0000000..fb0ef35 --- /dev/null +++ b/fixture/convex/annex/failures.ts @@ -0,0 +1,31 @@ +import { ConvexError, v } from "convex/values"; +import { mutation } from "./_generated/server.js"; +export const throwAfterWrite = mutation({ + args: { + kind: v.union(v.literal("convexError"), v.literal("plainError")), + data: v.any(), + }, + returns: v.null(), + handler: async (ctx, { kind, data }) => { + const before = await ctx.meta.getTransactionMetrics(); + await ctx.db.insert("throwerWrites", { by: "thrower" }); + const after = await ctx.meta.getTransactionMetrics(); + const rows = await ctx.db + .query("throwerWrites") + .withIndex("by_by", (q) => q.eq("by", "thrower")) + .take(2); + // This guard runs inside the transaction, before the throw rolls the row back. + if ( + after.documentsWritten.used - before.documentsWritten.used !== 1 || + rows.length !== 1 + ) + throw new Error( + "The thrower did not write exactly one row before throwing", + ); + if (kind === "plainError") + throw Object.assign(new Error(`plain failure: ${String(data)}`), { + addedProperty: "present", + }); + throw new ConvexError(data); + }, +}); diff --git a/fixture/convex/annex/identity.ts b/fixture/convex/annex/identity.ts new file mode 100644 index 0000000..e573439 --- /dev/null +++ b/fixture/convex/annex/identity.ts @@ -0,0 +1,7 @@ +import { v } from "convex/values"; +import { query } from "./_generated/server.js"; +export const caller = query({ + args: {}, + returns: v.any(), + handler: (ctx) => ctx.auth.getUserIdentity(), +}); diff --git a/fixture/convex/annex/limits.ts b/fixture/convex/annex/limits.ts new file mode 100644 index 0000000..884b87e --- /dev/null +++ b/fixture/convex/annex/limits.ts @@ -0,0 +1,42 @@ +import type { TransactionMetrics } from "convex/server"; +import { v } from "convex/values"; +import { mutation, query } from "./_generated/server.js"; +export const insertBlobs = mutation({ + args: { group: v.string(), count: v.number(), size: v.number() }, + returns: v.null(), + handler: async (ctx, { group, count, size }) => { + for (let i = 0; i < count; i++) { + await ctx.db.insert("blobs", { + group, + bytes: new ArrayBuffer(size), + }); + } + return null; + }, +}); +export const readBlobs = query({ + args: { group: v.string(), count: v.number() }, + returns: v.any(), + handler: async ( + ctx, + { group, count }, + ): Promise<{ + documents: number; + payloadBytes: number; + before: TransactionMetrics; + after: TransactionMetrics; + }> => { + const before = await ctx.meta.getTransactionMetrics(); + const documents = await ctx.db + .query("blobs") + .withIndex("by_group", (q) => q.eq("group", group)) + .take(count); + const after = await ctx.meta.getTransactionMetrics(); + return { + documents: documents.length, + payloadBytes: documents.reduce((sum, d) => sum + d.bytes.byteLength, 0), + before, + after, + }; + }, +}); diff --git a/fixture/convex/annex/list.ts b/fixture/convex/annex/list.ts new file mode 100644 index 0000000..8d45b4c --- /dev/null +++ b/fixture/convex/annex/list.ts @@ -0,0 +1,45 @@ +import { paginator } from "convex-helpers/server/pagination"; +import { paginationOptsValidator } from "convex/server"; +import { v } from "convex/values"; +import { mutation, query } from "./_generated/server.js"; +import schema from "./schema.js"; +export const page = query({ + args: { paginationOpts: paginationOptsValidator }, + returns: v.any(), + handler: (ctx, { paginationOpts }) => + paginator(ctx.db, schema) + .query("rows") + .withIndex("by_position") + .paginate(paginationOpts), +}); +export const first = query({ + args: {}, + returns: v.any(), + handler: (ctx) => ctx.db.query("rows").withIndex("by_position").first(), +}); +export const insert = mutation({ + args: { + rows: v.array(v.object({ position: v.number(), label: v.string() })), + }, + returns: v.null(), + handler: async (ctx, { rows }) => { + for (const row of rows) await ctx.db.insert("rows", row); + return null; + }, +}); +export const remove = mutation({ + args: { id: v.id("rows") }, + returns: v.null(), + handler: async (ctx, { id }) => { + await ctx.db.delete("rows", id); + return null; + }, +}); +export const relabel = mutation({ + args: { id: v.id("rows"), label: v.string() }, + returns: v.null(), + handler: async (ctx, { id, label }) => { + await ctx.db.patch("rows", id, { label }); + return null; + }, +}); diff --git a/fixture/convex/annex/notes.ts b/fixture/convex/annex/notes.ts new file mode 100644 index 0000000..2d88a89 --- /dev/null +++ b/fixture/convex/annex/notes.ts @@ -0,0 +1,10 @@ +import { v } from "convex/values"; +import { mutation } from "./_generated/server.js"; +export const add = mutation({ + args: {}, + returns: v.null(), + handler: async (ctx) => { + await ctx.db.insert("notes", { source: "annex" }); + return null; + }, +}); diff --git a/fixture/convex/annex/readCost.ts b/fixture/convex/annex/readCost.ts new file mode 100644 index 0000000..e8074fc --- /dev/null +++ b/fixture/convex/annex/readCost.ts @@ -0,0 +1,14 @@ +import { v } from "convex/values"; +import { mutation, query } from "./_generated/server.js"; +export const seed = mutation({ + args: {}, + returns: v.id("samples"), + handler: (ctx) => ctx.db.insert("samples", { value: 7 }), +}); +export const readOne = query({ + args: { id: v.id("samples"), cacheBuster: v.number() }, + returns: v.any(), + handler: async (ctx, { id }) => { + return await ctx.db.get("samples", id); + }, +}); diff --git a/fixture/convex/annex/schema.ts b/fixture/convex/annex/schema.ts new file mode 100644 index 0000000..8b9eaa2 --- /dev/null +++ b/fixture/convex/annex/schema.ts @@ -0,0 +1,15 @@ +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; +export default defineSchema({ + notes: defineTable({ source: v.string() }), + throwerWrites: defineTable({ by: v.string() }).index("by_by", ["by"]), + samples: defineTable({ value: v.number() }), + blobs: defineTable({ group: v.string(), bytes: v.bytes() }).index( + "by_group", + ["group"], + ), + rows: defineTable({ position: v.number(), label: v.string() }).index( + "by_position", + ["position"], + ), +}); diff --git a/fixture/convex/auth.config.ts b/fixture/convex/auth.config.ts new file mode 100644 index 0000000..235ed84 --- /dev/null +++ b/fixture/convex/auth.config.ts @@ -0,0 +1,17 @@ +import type { AuthConfig } from "convex/server"; +function required(name: string): string { + const value = process.env[name]; + if (!value) throw new Error(`Missing ${name}`); + return value; +} +export default { + providers: [ + { + type: "customJwt", + algorithm: "ES256", + issuer: required("AUTH_ISSUER"), + applicationID: required("AUTH_APPLICATION_ID"), + jwks: required("AUTH_JWKS"), + }, + ], +} satisfies AuthConfig; diff --git a/fixture/convex/convex.config.ts b/fixture/convex/convex.config.ts new file mode 100644 index 0000000..9c3b435 --- /dev/null +++ b/fixture/convex/convex.config.ts @@ -0,0 +1,5 @@ +import { defineApp } from "convex/server"; +import annex from "./annex/convex.config.js"; +const app = defineApp(); +app.use(annex); +export default app; diff --git a/fixture/convex/failures.ts b/fixture/convex/failures.ts new file mode 100644 index 0000000..b2c7df4 --- /dev/null +++ b/fixture/convex/failures.ts @@ -0,0 +1,85 @@ +import { ConvexError, v } from "convex/values"; +import { components, internal } from "./_generated/api.js"; +import { internalMutation, mutation } from "./_generated/server.js"; +import type { MutationCtx } from "./_generated/server.js"; +export interface Caught { + isConvexError: boolean; + data: unknown; + message: string; + addedProperty: string | null; +} +const thrown = { + kind: v.union(v.literal("convexError"), v.literal("plainError")), + data: v.any(), +}; +const parentArgs = { + ...thrown, + boundary: v.union(v.literal("nested"), v.literal("annex")), +}; +export const throwAfterWrite = internalMutation({ + args: thrown, + returns: v.null(), + handler: async (ctx, { kind, data }) => { + const before = await ctx.meta.getTransactionMetrics(); + await ctx.db.insert("throwerWrites", { by: "thrower" }); + const after = await ctx.meta.getTransactionMetrics(); + const rows = await ctx.db + .query("throwerWrites") + .withIndex("by_by", (q) => q.eq("by", "thrower")) + .take(2); + // This guard runs inside the transaction, before the throw rolls the row back. + if ( + after.documentsWritten.used - before.documentsWritten.used !== 1 || + rows.length !== 1 + ) + throw new Error( + "The thrower did not write exactly one row before throwing", + ); + if (kind === "plainError") + throw Object.assign(new Error(`plain failure: ${String(data)}`), { + addedProperty: "present", + }); + throw new ConvexError(data); + }, +}); +function callThrower( + ctx: MutationCtx, + a: { + boundary: "nested" | "annex"; + kind: "convexError" | "plainError"; + data: unknown; + }, +): Promise { + return ctx.runMutation( + a.boundary === "nested" + ? internal.failures.throwAfterWrite + : components.annex.failures.throwAfterWrite, + { kind: a.kind, data: a.data }, + ); +} +export const catching = mutation({ + args: parentArgs, + returns: v.any(), + handler: async (ctx, a): Promise => { + await ctx.db.insert("parentWrites", { by: "catching parent" }); + let caught: unknown; + try { + await callThrower(ctx, a); + } catch (error) { + caught = error; + } + return { + isConvexError: caught instanceof ConvexError, + data: caught instanceof ConvexError ? (caught.data as unknown) : null, + message: caught === undefined ? "nothing was thrown" : String(caught), + addedProperty: + (caught as { addedProperty?: string } | undefined)?.addedProperty ?? + null, + }; + }, +}); +export const passing = mutation({ + args: parentArgs, + returns: v.null(), + handler: (ctx, a): Promise => callThrower(ctx, a), +}); diff --git a/fixture/convex/identity.ts b/fixture/convex/identity.ts new file mode 100644 index 0000000..2c650d7 --- /dev/null +++ b/fixture/convex/identity.ts @@ -0,0 +1,14 @@ +import { v } from "convex/values"; +import { components } from "./_generated/api.js"; +import { query } from "./_generated/server.js"; +export const caller = query({ + args: {}, + returns: v.any(), + handler: (ctx) => ctx.auth.getUserIdentity(), +}); +export const callerSeenByAnnex = query({ + args: {}, + returns: v.any(), + handler: (ctx): Promise => + ctx.runQuery(components.annex.identity.caller, {}), +}); diff --git a/fixture/convex/limits.ts b/fixture/convex/limits.ts new file mode 100644 index 0000000..6d70c52 --- /dev/null +++ b/fixture/convex/limits.ts @@ -0,0 +1,124 @@ +import type { TransactionMetrics } from "convex/server"; +import { v } from "convex/values"; +import { api, components, internal } from "./_generated/api.js"; +import { internalQuery, mutation, query } from "./_generated/server.js"; +export interface BlobRead { + documents: number; + payloadBytes: number; + before: TransactionMetrics; + after: TransactionMetrics; +} +export interface ReadThenCall { + parentDocuments: number; + before: TransactionMetrics; + after: TransactionMetrics; + child: BlobRead; +} +const boundary = v.union(v.literal("nested"), v.literal("annex")); +const blobs = { group: v.string(), count: v.number(), size: v.number() }; +export const insertBlobs = mutation({ + args: blobs, + returns: v.null(), + handler: async (ctx, { group, count, size }) => { + for (let i = 0; i < count; i++) { + await ctx.db.insert("blobs", { + group, + bytes: new ArrayBuffer(size), + }); + } + return null; + }, +}); +export const insertAnnexBlobs = mutation({ + args: blobs, + returns: v.null(), + handler: (ctx, a): Promise => + ctx.runMutation(components.annex.limits.insertBlobs, a), +}); +export const readBlobs = internalQuery({ + args: { group: v.string(), count: v.number() }, + returns: v.any(), + handler: async (ctx, { group, count }): Promise => { + const before = await ctx.meta.getTransactionMetrics(); + const documents = await ctx.db + .query("blobs") + .withIndex("by_group", (q) => q.eq("group", group)) + .take(count); + const after = await ctx.meta.getTransactionMetrics(); + return { + documents: documents.length, + payloadBytes: documents.reduce((sum, d) => sum + d.bytes.byteLength, 0), + before, + after, + }; + }, +}); +export const childAlone = query({ + args: { through: boundary, count: v.number() }, + returns: v.any(), + handler: (ctx, { through, count }): Promise => + ctx.runQuery( + through === "nested" + ? internal.limits.readBlobs + : components.annex.limits.readBlobs, + { group: "child", count }, + ), +}); +export const readThenCall = mutation({ + args: { through: boundary, parentCount: v.number(), childCount: v.number() }, + returns: v.any(), + handler: async (ctx, a): Promise => { + const own = await ctx.db + .query("blobs") + .withIndex("by_group", (q) => q.eq("group", "parent")) + .take(a.parentCount); + const before = await ctx.meta.getTransactionMetrics(); + const child: BlobRead = await ctx.runQuery( + a.through === "nested" + ? internal.limits.readBlobs + : components.annex.limits.readBlobs, + { group: "child", count: a.childCount }, + ); + const after = await ctx.meta.getTransactionMetrics(); + return { parentDocuments: own.length, before, after, child }; + }, +}); +export const writeThenCall = mutation({ + args: { + through: boundary, + group: v.string(), + parentCount: v.number(), + childCount: v.number(), + size: v.number(), + }, + returns: v.null(), + handler: async (ctx, a): Promise => { + for (let i = 0; i < a.parentCount; i++) { + await ctx.db.insert("blobs", { + group: a.group, + bytes: new ArrayBuffer(a.size), + }); + } + await ctx.runMutation( + a.through === "nested" + ? api.limits.insertBlobs + : components.annex.limits.insertBlobs, + { group: a.group, count: a.childCount, size: a.size }, + ); + return null; + }, +}); +export const recurse = mutation({ + args: { label: v.string(), stack: v.number() }, + returns: v.number(), + handler: async (ctx, { label, stack }): Promise => { + await ctx.db.insert("depthRows", { trial: label }); + return stack <= 1 + ? 1 + : 1 + + (await ctx.runMutation(api.limits.recurse, { + label, + stack: stack - 1, + })); + }, +}); diff --git a/fixture/convex/list.ts b/fixture/convex/list.ts new file mode 100644 index 0000000..a675989 --- /dev/null +++ b/fixture/convex/list.ts @@ -0,0 +1,51 @@ +import { paginationOptsValidator } from "convex/server"; +import type { PaginationResult } from "convex/server"; +import { v } from "convex/values"; +import { components } from "./_generated/api.js"; +import { mutation, query } from "./_generated/server.js"; +export interface ListRow { + _id: string; + _creationTime: number; + position: number; + label: string; +} +export type ListPage = PaginationResult; +const row = v.object({ position: v.number(), label: v.string() }); +export const page = query({ + args: { + paginationOpts: paginationOptsValidator, + maximumRowsRead: v.optional(v.number()), + }, + returns: v.any(), + handler: (ctx, { paginationOpts, maximumRowsRead }): Promise => + ctx.runQuery(components.annex.list.page, { + paginationOpts: { + ...paginationOpts, + ...(maximumRowsRead === undefined ? {} : { maximumRowsRead }), + }, + }), +}); +export const first = query({ + args: {}, + returns: v.any(), + handler: (ctx): Promise => + ctx.runQuery(components.annex.list.first, {}), +}); +export const insert = mutation({ + args: { rows: v.array(row) }, + returns: v.null(), + handler: (ctx, a): Promise => + ctx.runMutation(components.annex.list.insert, a), +}); +export const remove = mutation({ + args: { id: v.string() }, + returns: v.null(), + handler: (ctx, a): Promise => + ctx.runMutation(components.annex.list.remove, a), +}); +export const relabel = mutation({ + args: { id: v.string(), label: v.string() }, + returns: v.null(), + handler: (ctx, a): Promise => + ctx.runMutation(components.annex.list.relabel, a), +}); diff --git a/fixture/convex/markers.ts b/fixture/convex/markers.ts new file mode 100644 index 0000000..d9831ab --- /dev/null +++ b/fixture/convex/markers.ts @@ -0,0 +1,10 @@ +import { v } from "convex/values"; +import { mutation } from "./_generated/server.js"; +export const insert = mutation({ + args: { trial: v.string() }, + returns: v.null(), + handler: async (ctx, { trial }) => { + await ctx.db.insert("markers", { trial }); + return null; + }, +}); diff --git a/fixture/convex/notes.ts b/fixture/convex/notes.ts new file mode 100644 index 0000000..5b4c368 --- /dev/null +++ b/fixture/convex/notes.ts @@ -0,0 +1,10 @@ +import { v } from "convex/values"; +import { internalMutation } from "./_generated/server.js"; +export const addInternal = internalMutation({ + args: {}, + returns: v.null(), + handler: async (ctx) => { + await ctx.db.insert("notes", { source: "internal" }); + return null; + }, +}); diff --git a/fixture/convex/readCost.ts b/fixture/convex/readCost.ts new file mode 100644 index 0000000..9241a65 --- /dev/null +++ b/fixture/convex/readCost.ts @@ -0,0 +1,79 @@ +import { v } from "convex/values"; +import { components, internal } from "./_generated/api.js"; +import type { Id } from "./_generated/dataModel.js"; +import { internalQuery, mutation, query } from "./_generated/server.js"; +import type { QueryCtx } from "./_generated/server.js"; +function readSample(ctx: QueryCtx, id: Id<"samples">) { + return ctx.db.get("samples", id); +} +export const seed = mutation({ + args: {}, + returns: v.object({ own: v.id("samples"), annex: v.string() }), + handler: async (ctx): Promise<{ own: Id<"samples">; annex: string }> => ({ + own: await ctx.db.insert("samples", { value: 7 }), + annex: await ctx.runMutation(components.annex.readCost.seed, {}), + }), +}); +export const readOne = internalQuery({ + args: { id: v.id("samples"), cacheBuster: v.number() }, + returns: v.any(), + handler: (ctx, { id }) => readSample(ctx, id), +}); +const parentArgs = { + path: v.union( + v.literal("helper"), + v.literal("nested"), + v.literal("component"), + ), + reads: v.number(), + own: v.id("samples"), + annex: v.string(), + cacheBuster: v.number(), +}; +// The count of reads comes from the transaction's own metrics, not from the loop. The backend +// counts every document read by this function and by the queries it calls, in the app or in the +// component, so a path that skipped a read would report fewer. +async function documentsRead(ctx: QueryCtx): Promise { + return (await ctx.meta.getTransactionMetrics()).documentsRead.used; +} +async function readRepeatedly( + ctx: QueryCtx, + a: { + path: "helper" | "nested" | "component"; + reads: number; + own: Id<"samples">; + annex: string; + cacheBuster: number; + }, +): Promise<{ documentsRead: number; id: string }> { + const id = a.path === "component" ? a.annex : a.own; + const before = await documentsRead(ctx); + for (let call = 0; call < a.reads; call++) { + let document: { _id: string; value: number } | null; + if (a.path === "helper") document = await readSample(ctx, a.own); + else if (a.path === "nested") + document = await ctx.runQuery(internal.readCost.readOne, { + id: a.own, + cacheBuster: a.cacheBuster + call, + }); + else + document = await ctx.runQuery(components.annex.readCost.readOne, { + id: a.annex, + cacheBuster: a.cacheBuster + call, + }); + if (document === null || document._id !== id || document.value !== 7) + throw new Error("The read path did not return the seeded document"); + } + return { documentsRead: (await documentsRead(ctx)) - before, id }; +} +const readResult = v.object({ documentsRead: v.number(), id: v.string() }); +export const viaMutation = mutation({ + args: parentArgs, + returns: readResult, + handler: readRepeatedly, +}); +export const viaQuery = query({ + args: parentArgs, + returns: readResult, + handler: readRepeatedly, +}); diff --git a/fixture/convex/schema.ts b/fixture/convex/schema.ts new file mode 100644 index 0000000..dbb0ce1 --- /dev/null +++ b/fixture/convex/schema.ts @@ -0,0 +1,14 @@ +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; +export default defineSchema({ + notes: defineTable({ source: v.string() }), + depthRows: defineTable({ trial: v.string() }).index("by_trial", ["trial"]), + markers: defineTable({ trial: v.string() }).index("by_trial", ["trial"]), + throwerWrites: defineTable({ by: v.string() }).index("by_by", ["by"]), + parentWrites: defineTable({ by: v.string() }), + samples: defineTable({ value: v.number() }), + blobs: defineTable({ group: v.string(), bytes: v.bytes() }).index( + "by_group", + ["group"], + ), +}); diff --git a/harness/admin.ts b/harness/admin.ts new file mode 100644 index 0000000..1016769 --- /dev/null +++ b/harness/admin.ts @@ -0,0 +1,314 @@ +import { BaseConvexClient, ConvexHttpClient } from "convex/browser"; +import type { Value } from "convex/values"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { setTimeout as sleep } from "node:timers/promises"; +import { redact, runChild } from "./child.js"; +export interface AdminTarget { + url: string; + adminKey: string; + home: string; + secrets: readonly string[]; + signal?: AbortSignal; +} +export interface AdminState { + deployed: boolean; + environment: Set; + logProcess?: object | undefined; +} +export interface LogMark { + readonly cursorMs: number; +} +export interface CompletionRecord { + kind: "Completion"; + udfType: "Query" | "Mutation" | "Action" | "HttpAction"; + identifier: string; + componentPath: string | null; + requestId: string; + executionId: string; + parentExecutionId: string | null; + timestamp: number; + executionTime: number; + cachedResult: boolean; + error: string | null; + caller: string; + usageStats: Record; +} +export interface AdminAccess { + deploy(): Promise; + codegen(): Promise; + setEnvironment(variables: Readonly>): Promise; + environment(): Promise>; + run( + name: string, + args?: Record, + options?: { component?: string }, + ): Promise; + readTable( + table: string, + options?: { component?: string; pageSize?: number }, + ): Promise[]>; + logMark(): Promise; + completionsSince( + mark: LogMark, + until: (records: readonly CompletionRecord[]) => boolean, + timeoutMs?: number, + ): Promise; +} +export interface CliCall { + file: string; + args: string[]; + cwd: string; + env: Record; + // Infinity for a call that runs until it is stopped. runChild refuses it. + timeoutMs: number; +} +const repositoryRoot = join(import.meta.dirname, ".."); +const commandArgs = { + deploy: ["deploy", "--yes", "--codegen", "disable", "--typecheck", "disable"], + codegen: ["codegen", "--typecheck", "disable"], + dev: ["dev", "--typecheck", "disable", "--tail-logs", "pause-on-deploy"], +} as const; +const commandTimeoutMs = { + deploy: 60000, + codegen: 120000, + dev: Number.POSITIVE_INFINITY, +} as const; +export function convexCli( + target: Pick, + command: "deploy" | "codegen" | "dev", +): CliCall { + const selection = ["--url", target.url, "--admin-key", target.adminKey]; + return { + file: process.execPath, + args: [ + join(repositoryRoot, "node_modules/convex/bin/main.js"), + ...commandArgs[command], + ...selection, + ], + cwd: repositoryRoot, + env: { PATH: process.env.PATH ?? "", HOME: target.home, TMPDIR: tmpdir() }, + timeoutMs: commandTimeoutMs[command], + }; +} +// Convex 1.46.0 implements these members and strips them from its declarations. +type AdminHttpClient = ConvexHttpClient & { + setAdminAuth(key: string): void; + function( + name: string, + componentPath: string | undefined, + args: Record, + ): Promise; +}; +type AdminSocketClient = { + setAdminAuth(key: string): void; + subscribe( + name: string, + args: Record, + options: { componentPath?: string }, + ): unknown; + localQueryResultByToken(token: string): Value | undefined; + close(): Promise; +}; +export function createAdminAccess( + target: AdminTarget, + state: AdminState, +): AdminAccess { + const marks = new WeakMap(); + const http = new ConvexHttpClient(target.url) as AdminHttpClient; + http.setAdminAuth(target.adminKey); + const authorization = { Authorization: `Convex ${target.adminKey}` }; + async function cli(command: "deploy" | "codegen") { + const call = convexCli(target, command); + // CLI children are short and bounded; they lose their backend when a run is swept. + await runChild(`convex ${command}`, call.file, call.args, { + cwd: call.cwd, + env: call.env, + timeoutMs: call.timeoutMs, + secrets: target.secrets, + ...(target.signal === undefined ? {} : { signal: target.signal }), + }); + } + function systemQuery( + name: string, + args: Record, + component: string | undefined, + timeoutMs = 10000, + ): Promise { + return new Promise((resolve, reject) => { + let settled = false; + const settle = (finish: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timer); + void client.close(); + finish(); + }; + const client = new BaseConvexClient( + target.url, + (tokens) => { + for (const token of tokens) { + let value: Value | undefined; + try { + value = client.localQueryResultByToken(token); + } catch (error) { + return settle(() => reject(error)); + } + if (value !== undefined) return settle(() => resolve(value)); + } + }, + { unsavedChangesWarning: false }, + ) as unknown as AdminSocketClient; + const timer = setTimeout( + () => + settle(() => + reject( + new Error(`Admin read ${name} got no answer in ${timeoutMs} ms`), + ), + ), + timeoutMs, + ); + client.setAdminAuth(target.adminKey); + client.subscribe( + name, + args, + component === undefined ? {} : { componentPath: component }, + ); + }); + } + return { + async deploy() { + await cli("deploy"); + state.deployed = true; + }, + codegen: () => cli("codegen"), + async setEnvironment(variables) { + const response = await fetch( + `${target.url}/api/update_environment_variables`, + { + method: "POST", + headers: { ...authorization, "Content-Type": "application/json" }, + body: JSON.stringify({ + changes: Object.entries(variables).map(([name, value]) => ({ + name, + value, + })), + }), + signal: AbortSignal.timeout(10000), + }, + ); + if (!response.ok) + throw new Error( + `Setting environment variables failed with status ${response.status}: ${redact(await response.text(), target.secrets)}`, + ); + for (const name of Object.keys(variables)) state.environment.add(name); + }, + async environment() { + const rows = (await systemQuery( + "_system/cli/queryEnvironmentVariables", + {}, + undefined, + )) as { name: string; value: string }[]; + return Object.fromEntries(rows.map((row) => [row.name, row.value])); + }, + run: (name, args = {}, options = {}) => + http.function(name, options.component, args), + async readTable(table, options = {}) { + const documents: Record[] = []; + let cursor: string | null = null; + for (;;) { + const page = (await systemQuery( + "_system/cli/tableData", + { + table, + order: "asc", + paginationOpts: { cursor, numItems: options.pageSize ?? 1000 }, + }, + options.component, + )) as { + page: Record[]; + isDone: boolean; + continueCursor: string; + }; + documents.push(...page.page); + if (page.isDone) break; + cursor = page.continueCursor; + } + if (documents.length === 0) { + const tables = ( + (await systemQuery( + "_system/cli/tables", + { paginationOpts: { cursor: null, numItems: 10000 } }, + options.component, + )) as { page: { name: string }[] } + ).page + .map((entry) => entry.name) + .sort(); + if (!tables.includes(table)) + throw new Error( + `No table "${table}" in ${options.component === undefined ? "the app" : `component "${options.component}"`}. Tables: ${tables.join(", ")}`, + ); + } + return documents; + }, + async logMark() { + // The log's cursor is a time in milliseconds on this machine's clock. Round up, then wait + // past it, so that every earlier record is at or before the mark and every later one after. + const process = state.logProcess; + const cursorMs = Date.now() + 1; + await sleep(2); + const mark = { cursorMs }; + marks.set(mark, process); + return mark; + }, + async completionsSince(mark, until, timeoutMs = 10000) { + const assertProcess = () => { + if (!marks.has(mark) || marks.get(mark) !== state.logProcess) + throw new Error( + "The function log mark belongs to another backend process", + ); + }; + assertProcess(); + const records: CompletionRecord[] = []; + const deadline = Date.now() + timeoutMs; + let cursor = mark.cursorMs; + while (!until(records)) { + assertProcess(); + const left = deadline - Date.now(); + if (left <= 0) + throw new Error( + `The function log held ${records.length} completion records ${timeoutMs} ms after the mark, and the test waited for more`, + ); + let response: Response; + try { + response = await fetch( + `${target.url}/api/stream_function_logs?cursor=${cursor}`, + { headers: authorization, signal: AbortSignal.timeout(left) }, + ); + } catch (error) { + if ((error as Error).name === "TimeoutError") continue; + throw error; + } + assertProcess(); + if (!response.ok) + throw new Error( + `Reading the function log failed with status ${response.status}`, + ); + const body = (await response.json()) as { + entries: { kind: string }[]; + newCursor: number; + }; + if (body.entries.length >= 1000) + throw new Error( + `One function log read returned ${body.entries.length} entries. The backend keeps its last 1000, so records may be lost. Read the log more often.`, + ); + for (const entry of body.entries) + if (entry.kind === "Completion") + records.push(entry as CompletionRecord); + cursor = body.newCursor; + } + assertProcess(); + return records; + }, + }; +} diff --git a/harness/backend-release.json b/harness/backend-release.json new file mode 100644 index 0000000..8d64a63 --- /dev/null +++ b/harness/backend-release.json @@ -0,0 +1,15 @@ +{ + "release": "precompiled-2026-09-28-5c7cb5b", + "assets": { + "darwin-arm64": { + "file": "convex-local-backend-aarch64-apple-darwin.zip", + "zipSha256": "c1613bb4a3ac884871184020662c865a045cde95ad781fff002a6bd25f2f2890", + "executableSha256": "c6290f5a86bc2f6b31a931833c25106ccc06c0c8c9f0894c35f2d7cecf1fd676" + }, + "linux-x64": { + "file": "convex-local-backend-x86_64-unknown-linux-gnu.zip", + "zipSha256": "5b4adbe9fc2f9985c66eb12b19dadedf2700d8796c839ddba09802196099f241", + "executableSha256": "7ebc6a4696499640c2f2255395990b4f8b901b045388a0ced2ac765b81ebadae" + } + } +} diff --git a/harness/backend.ts b/harness/backend.ts new file mode 100644 index 0000000..a2e7054 --- /dev/null +++ b/harness/backend.ts @@ -0,0 +1,301 @@ +import { spawn } from "node:child_process"; +import type { ChildProcess } from "node:child_process"; +import { randomBytes, randomUUID } from "node:crypto"; +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import { publishOwnership } from "./ownership.js"; +import { createServer } from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { setTimeout as sleep } from "node:timers/promises"; +import { createAdminAccess } from "./admin.js"; +import type { AdminAccess, AdminState } from "./admin.js"; +import { redact, redactedBuffer, runChild } from "./child.js"; +import type { Executable } from "./executable.js"; +import type { FixtureIssuer } from "./identity.js"; +export const fixtureComposition = { + name: "fixture", + functions: "fixture/convex", + installedLayers: [] as readonly string[], +} as const; +export interface BackendFacts { + composition: "fixture" | null; + installedLayers: string[]; + executable: Pick; + identitySource: { kind: "fixture issuer"; issuer: string }; + environment: string[]; + dataset: "empty at start"; +} +export interface StartOptions { + executable: Executable; + issuer: FixtureIssuer; + parentDirectory?: string; + signal?: AbortSignal; +} +export interface Backend { + readonly url: string; + readonly adminKey: string; + readonly issuer: FixtureIssuer; + readonly admin: AdminAccess; + stop(): Promise; + kill(): Promise; + restart(): Promise; + dispose(): Promise; + facts(): BackendFacts; +} +async function freePort(): Promise { + const server = createServer(); + await new Promise((ok, fail) => { + server.once("error", fail); + server.listen(0, "127.0.0.1", ok); + }); + const address = server.address(); + await new Promise((ok) => server.close(() => ok())); + if (address === null || typeof address === "string") + throw new Error("The operating system gave no port"); + return address.port; +} +function exitOf(child: ChildProcess, timeoutMs: number): Promise { + if (child.exitCode !== null || child.signalCode !== null) + return Promise.resolve(true); + return new Promise((done) => { + const timer = setTimeout(() => done(false), timeoutMs); + child.once("exit", () => { + clearTimeout(timer); + done(true); + }); + }); +} +export async function startBackend(options: StartOptions): Promise { + const directory = await mkdtemp( + join(options.parentDirectory ?? tmpdir(), "backend-"), + ); + const home = join(directory, "home"); + const instanceName = `libar-${randomUUID()}`; + const instanceSecret = randomBytes(32).toString("hex"); + const secrets: string[] = [instanceSecret]; + let child: ChildProcess | undefined; + let output = ""; + let port = 0; + let sitePort = 0; + let disposed = false; + let identityIssuer = options.issuer.issuer; + const state: AdminState = { deployed: false, environment: new Set() }; + const assertLive = () => { + if (disposed) throw new Error("The backend is disposed"); + }; + const running = () => + child !== undefined && child.exitCode === null && child.signalCode === null; + async function spawnAndWait(): Promise<"ready" | "port taken"> { + output = ""; + state.logProcess = {}; + const buffer = redactedBuffer(secrets, 16000); + const spawned = spawn( + options.executable.path, + [ + "--port", + String(port), + "--site-proxy-port", + String(sitePort), + "--interface", + "127.0.0.1", + "--instance-name", + instanceName, + "--instance-secret", + instanceSecret, + "--local-storage", + join(directory, "storage"), + "--disable-beacon", + join(directory, "backend.sqlite3"), + ], + { stdio: ["ignore", "pipe", "pipe"] }, + ); + child = spawned; + const keep = (chunk: Buffer, stream: "stdout" | "stderr" | "error") => { + buffer.append(chunk.toString(), stream); + output = buffer.text(); + }; + spawned.stdout?.on("data", (chunk: Buffer) => keep(chunk, "stdout")); + spawned.stderr?.on("data", (chunk: Buffer) => keep(chunk, "stderr")); + spawned.on("error", (error) => keep(Buffer.from(String(error)), "error")); + if (spawned.pid !== undefined) + publishOwnership(directory, spawned.pid, instanceName); + const deadline = Date.now() + 30000; + while (Date.now() < deadline) { + options.signal?.throwIfAborted(); + if (spawned.exitCode !== null || spawned.signalCode !== null) { + if (output.includes("Address already in use")) return "port taken"; + throw new Error( + `The backend exited with code ${spawned.exitCode} before it was ready: ${redact(output, secrets)}`, + ); + } + try { + const response = await fetch(`http://127.0.0.1:${port}/instance_name`, { + signal: AbortSignal.timeout(1000), + }); + if ( + response.ok && + (await response.text()) === instanceName && + spawned.exitCode === null && + spawned.signalCode === null + ) + return "ready"; + } catch { + // Not listening yet. + } + await sleep(50); + } + throw new Error( + `The backend was not ready in 30000 ms: ${redact(output, secrets)}`, + ); + } + async function end(signal: "SIGINT" | "SIGKILL"): Promise { + if (child === undefined || !running()) return; + child.kill(signal); + if (await exitOf(child, 5000)) return; + if (signal === "SIGINT") { + child.kill("SIGKILL"); + if (await exitOf(child, 5000)) return; + } + throw new Error( + `Backend process ${child.pid} did not exit within 5000 ms after SIGKILL`, + ); + } + async function dispose(): Promise { + if (disposed) return; + disposed = true; + state.logProcess = undefined; + const left: string[] = []; + await end("SIGKILL").catch((error: Error) => left.push(error.message)); + await rm(directory, { + recursive: true, + force: true, + maxRetries: 5, + retryDelay: 100, + }).catch((error: Error) => + left.push(`could not remove ${directory}: ${error.message}`), + ); + if (left.length > 0) + throw new Error( + `Backend disposal left something behind: ${left.join("; ")}`, + ); + } + try { + await mkdir(home); + const adminKey = ( + await runChild( + "convex-local-backend keygen", + options.executable.path, + [ + "keygen", + "admin-key", + "--instance-name", + instanceName, + "--instance-secret", + instanceSecret, + ], + { + timeoutMs: 10000, + secrets, + ...(options.signal === undefined ? {} : { signal: options.signal }), + }, + ) + ).trim(); + secrets.push(adminKey); + const usedPorts = new Set(); + async function freshPort(): Promise { + for (let pick = 0; pick < 100; pick++) { + const candidate = await freePort(); + if (usedPorts.has(candidate)) continue; + usedPorts.add(candidate); + return candidate; + } + throw new Error("The operating system gave no fresh port in 100 picks"); + } + for (let attempt = 1; ; attempt++) { + port = await freshPort(); + sitePort = await freshPort(); + if ((await spawnAndWait()) === "ready") break; + if (attempt === 3) + throw new Error( + `The backend could not bind its ports in 3 attempts: ${redact(output, secrets)}`, + ); + } + const url = `http://127.0.0.1:${port}`; + const access = createAdminAccess( + { + url, + adminKey, + home, + secrets, + ...(options.signal === undefined ? {} : { signal: options.signal }), + }, + state, + ); + const admin: AdminAccess = { + ...access, + async setEnvironment(variables) { + await access.setEnvironment(variables); + identityIssuer = variables.AUTH_ISSUER ?? identityIssuer; + }, + }; + await admin.setEnvironment({ + AUTH_ISSUER: options.issuer.issuer, + AUTH_APPLICATION_ID: options.issuer.applicationID, + AUTH_JWKS: options.issuer.jwks, + }); + return { + url, + adminKey, + issuer: options.issuer, + admin, + async stop() { + assertLive(); + state.logProcess = undefined; + await end("SIGINT"); + }, + async kill() { + assertLive(); + state.logProcess = undefined; + await end("SIGKILL"); + }, + async restart() { + assertLive(); + if (running()) + throw new Error( + "The backend is still running. Stop or kill it before restart.", + ); + try { + if ((await spawnAndWait()) === "port taken") + throw new Error( + `The backend could not restart on port ${port}: ${redact(output, secrets)}`, + ); + } catch (error) { + state.logProcess = undefined; + await end("SIGKILL"); + throw error; + } + }, + dispose, + facts: () => ({ + composition: state.deployed ? fixtureComposition.name : null, + installedLayers: state.deployed + ? [...fixtureComposition.installedLayers] + : [], + executable: { + release: options.executable.release, + sha256: options.executable.sha256, + source: options.executable.source, + }, + identitySource: { + kind: "fixture issuer", + issuer: identityIssuer, + }, + environment: [...state.environment].sort(), + dataset: "empty at start", + }), + }; + } catch (error) { + await dispose().catch(() => undefined); + throw error; + } +} diff --git a/harness/child.ts b/harness/child.ts new file mode 100644 index 0000000..0ba723a --- /dev/null +++ b/harness/child.ts @@ -0,0 +1,118 @@ +import { execFile } from "node:child_process"; +export interface ChildOptions { + timeoutMs: number; + cwd?: string; + env?: Readonly>; + secrets?: readonly string[]; + signal?: AbortSignal; +} +export function redact(text: string, secrets: readonly string[]): string { + const keys = secrets + .filter((secret) => secret !== "") + .sort((a, b) => b.length - a.length); + let result = ""; + let consumed = 0; + while (consumed < text.length) { + const match = keys.find((key) => text.startsWith(key, consumed)); + if (match !== undefined) { + result += "[redacted]"; + consumed += match.length; + } else { + result += text[consumed]; + consumed++; + } + } + return result; +} +export function runChild( + name: string, + file: string, + args: readonly string[], + options: ChildOptions, +): Promise { + return new Promise((resolve, reject) => { + const child = execFile( + file, + [...args], + { + cwd: options.cwd, + env: { ...(options.env ?? { PATH: process.env.PATH ?? "" }) }, + timeout: options.timeoutMs, + killSignal: "SIGKILL", + maxBuffer: 16 * 1024 * 1024, + signal: options.signal, + }, + (error, stdout, stderr) => { + if (error === null) return resolve(stdout); + if (options.signal?.aborted) return reject(options.signal.reason); + if (error.killed) + return reject( + new Error(`${name} did not finish in ${options.timeoutMs} ms`), + ); + if (typeof error.code !== "number") + return reject( + new Error(`${name} could not start: ${String(error.code)}`), + ); + const output = redact( + stderr.trim() === "" ? stdout : stderr, + options.secrets ?? [], + ).slice(-4000); + reject( + new Error(`${name} failed with exit code ${error.code}: ${output}`), + ); + }, + ); + // On abort Node sends SIGTERM, not killSignal, which a child can handle and outlive. + const signal = options.signal; + if (signal === undefined) return; + const kill = () => child.kill("SIGKILL"); + if (signal.aborted) kill(); + else { + signal.addEventListener("abort", kill, { once: true }); + child.once("exit", () => signal.removeEventListener("abort", kill)); + } + }); +} + +// Keep each stream's possible secret prefix separate from the other streams. +export function redactedBuffer( + secrets: readonly string[], + limit: number, +): { + append(chunk: string, stream?: "stdout" | "stderr" | "error"): void; + text(): string; +} { + let safe = ""; + const tails = new Map(); + const keys = secrets + .filter((secret) => secret !== "") + .sort((a, b) => b.length - a.length); + return { + append(chunk, stream = "stdout") { + let pending = (tails.get(stream) ?? "") + chunk; + let consumed = 0; + let complete = ""; + while (consumed < pending.length) { + const rest = pending.slice(consumed); + if ( + keys.some((key) => key.length > rest.length && key.startsWith(rest)) + ) + break; + const match = keys.find((key) => rest.startsWith(key)); + if (match !== undefined) { + complete += "[redacted]"; + consumed += match.length; + } else if (keys.some((key) => key.startsWith(rest))) { + break; + } else { + complete += pending[consumed]; + consumed++; + } + } + pending = pending.slice(consumed); + tails.set(stream, pending); + safe = (safe + complete).slice(-limit); + }, + text: () => safe, + }; +} diff --git a/harness/clients.ts b/harness/clients.ts new file mode 100644 index 0000000..615a5d4 --- /dev/null +++ b/harness/clients.ts @@ -0,0 +1,188 @@ +import { ConvexClient, ConvexHttpClient } from "convex/browser"; +import type { + FunctionArgs, + FunctionReference, + FunctionReturnType, +} from "convex/server"; +import { waitUntil } from "./wait.js"; +export function ordinaryClient( + url: string, + options: { token?: string; fetch?: typeof globalThis.fetch } = {}, +): ConvexHttpClient { + const client = new ConvexHttpClient( + url, + options.fetch === undefined ? {} : { fetch: options.fetch }, + ); + if (options.token !== undefined) client.setAuth(options.token); + return client; +} +export function ordinarySocketClient( + url: string, + options: { token?: string; webSocket?: typeof WebSocket } = {}, +): ConvexClient { + const client = new ConvexClient( + url, + options.webSocket === undefined + ? {} + : { webSocketConstructor: options.webSocket }, + ); + const token = options.token; + if (token !== undefined) client.setAuth(async () => token); + return client; +} +export interface CountingFetch { + readonly fetch: typeof globalThis.fetch; + requests(): number; +} +export function countingFetch(options: { + loseResponses: boolean; +}): CountingFetch { + let requests = 0; + return { + requests: () => requests, + fetch: async (input, init) => { + requests++; + const response = await fetch(input, init); + if (!options.loseResponses) return response; + // The backend has answered. Read the answer to the end, then lose it. + await response.arrayBuffer(); + throw new TypeError("The response was lost"); + }, + }; +} +export interface HeldWebSocket { + readonly WebSocket: typeof WebSocket; + holdResponses(): void; + pauseConnections(): void; + release(): void; +} +export function heldWebSocket(): HeldWebSocket { + const NativeWebSocket = globalThis.WebSocket; + let holding = false; + let paused = false; + const sockets = new Set(); + class Held { + onopen: WebSocket["onopen"] = null; + onclose: WebSocket["onclose"] = null; + onerror: WebSocket["onerror"] = null; + onmessage: WebSocket["onmessage"] = null; + private native: WebSocket | undefined; + private held: MessageEvent[] = []; + private closed = false; + private url: string | URL; + constructor(url: string | URL) { + this.url = url; + sockets.add(this); + if (!paused) this.connect(); + } + connect() { + if (this.native !== undefined || this.closed) return; + const native = new NativeWebSocket(this.url); + this.native = native; + native.onopen = (event) => this.onopen?.call(native, event); + native.onerror = (event) => this.onerror?.call(native, event); + native.onclose = (event) => { + this.held = []; + sockets.delete(this); + this.onclose?.call(native, event); + }; + native.onmessage = (event) => { + if (holding) this.held.push(event); + else this.onmessage?.call(native, event); + }; + } + deliver() { + const native = this.native; + if (native === undefined) return; + for (const event of this.held) this.onmessage?.call(native, event); + this.held = []; + } + send(data: string) { + if (this.native === undefined) + throw new Error("The socket is not connected"); + this.native.send(data); + } + close() { + this.closed = true; + this.held = []; + if (this.native !== undefined) return this.native.close(); + sockets.delete(this); + queueMicrotask(() => + this.onclose?.call( + this as unknown as WebSocket, + new CloseEvent("close"), + ), + ); + } + } + return { + WebSocket: Held as unknown as typeof WebSocket, + holdResponses() { + holding = true; + }, + pauseConnections() { + paused = true; + }, + release() { + holding = false; + paused = false; + for (const socket of sockets) { + socket.connect(); + socket.deliver(); + } + }, + }; +} +export interface QueryWatch { + readonly values: readonly T[]; + readonly error: Error | undefined; + until( + matches: (value: T) => boolean, + description: string, + timeoutMs?: number, + ): Promise; + stop(): void; +} +export function watchQuery>( + client: ConvexClient, + query: Query, + args: FunctionArgs, +): QueryWatch> { + type Result = FunctionReturnType; + const values: Result[] = []; + let error: Error | undefined; + let stopped = false; + const unsubscribe = client.onUpdate( + query, + args, + (value) => { + values.push(value); + }, + (failure) => { + error = failure; + }, + ); + return { + values, + get error() { + return error; + }, + async until(matches, description, timeoutMs = 5000) { + await waitUntil( + description, + () => { + if (error !== undefined) throw error; + return values.length > 0 && matches(values[values.length - 1]); + }, + timeoutMs, + ); + return values[values.length - 1]; + }, + stop() { + // ConvexClient throws when a subscription is removed twice. + if (stopped) return; + stopped = true; + unsubscribe(); + }, + }; +} diff --git a/harness/evidence.ts b/harness/evidence.ts new file mode 100644 index 0000000..0bcf118 --- /dev/null +++ b/harness/evidence.ts @@ -0,0 +1,188 @@ +import { randomUUID } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { readFileSync, renameSync, writeFileSync } from "node:fs"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { Reporter, TestCase } from "vitest/node"; +import type { BackendFacts } from "./backend.js"; +export type JsonValue = + null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }; +export interface Measurement { + name: string; + value: JsonValue; +} +export interface NativeTestFacts { + backends: BackendFacts[]; + measurements: Measurement[]; +} +export interface NativeTestEntry extends NativeTestFacts { + name: string; + file: string; + result: "passed" | "failed" | "skipped"; + durationMs: number | null; + errors: string[]; +} +export interface NativeRunRecord { + tier: "native"; + commit: string; + clean: boolean; + command: string; + startedAt: string; + finishedAt: string; + result: "passed" | "failed" | "interrupted"; + versions: Record; + tests: NativeTestEntry[]; + unhandledErrors: string[]; +} +declare module "vitest" { + interface TaskMeta { + native?: NativeTestFacts; + } +} +const repositoryRoot = join(import.meta.dirname, ".."); +const versioned: Record = { + convex: "convex", + "convex-helpers": "convex-helpers", + "convex-test": "convex-test", + vitest: "vitest", + typescript: "@typescript/native", + react: "react", + "happy-dom": "happy-dom", +}; +function git(...args: string[]): string | undefined { + try { + return execFileSync("git", args, { + cwd: repositoryRoot, + encoding: "utf8", + }).trim(); + } catch { + return undefined; + } +} +export default class EvidenceReporter implements Reporter { + private readonly directory: string; + // Vitest passes the reporter's options, an empty object unless the configuration names some. + constructor(options: { directory?: string } = {}) { + this.directory = options.directory ?? join(repositoryRoot, "evidence/runs"); + } + private native = false; + private startedAt = ""; + private commit = "unknown"; + private clean = false; + private tests: NativeTestEntry[] = []; + onTestRunStart( + specifications: Parameters>[0], + ) { + this.native = specifications.some( + (specification) => specification.project.name === "native", + ); + this.startedAt = new Date().toISOString(); + this.commit = git("rev-parse", "HEAD") ?? "unknown"; + this.clean = git("status", "--porcelain") === ""; + this.tests = []; + } + onTestCaseResult(test: TestCase) { + if (test.project.name !== "native") return; + const result = test.result(); + if (result.state === "pending") return; + this.tests.push({ + name: test.fullName, + file: test.module.relativeModuleId, + result: result.state, + durationMs: test.diagnostic()?.duration ?? null, + errors: (result.errors ?? []).map((error) => error.message), + backends: test.meta().native?.backends ?? [], + measurements: test.meta().native?.measurements ?? [], + }); + } + async onTestRunEnd( + ...[, unhandledErrors, reason]: Parameters< + NonNullable + > + ) { + if (!this.native) return; + const versions: Record = { node: process.version }; + for (const [name, packageName] of Object.entries(versioned)) + versions[name] = ( + JSON.parse( + await readFile( + join(repositoryRoot, "node_modules", packageName, "package.json"), + "utf8", + ), + ) as { version: string } + ).version; + const record: NativeRunRecord = { + tier: "native", + commit: this.commit, + clean: this.clean, + command: invocation(process.argv.slice(2)), + startedAt: this.startedAt, + finishedAt: new Date().toISOString(), + result: reason, + versions, + tests: this.tests, + unhandledErrors: unhandledErrors.map((error) => error.message), + }; + const directory = this.directory; + await mkdir(directory, { recursive: true }); + const name = await writeRunRecord(directory, record); + rememberRunRecord(join(directory, name)); + console.log(`Native run record: evidence/runs/${name}`); + } +} + +export function invocation(args: readonly string[]): string { + const quote = (arg: string) => + /^[a-zA-Z0-9_./=:-]+$/.test(arg) + ? arg + : "'" + arg.replaceAll("'", "'\\''") + "'"; + return ["vitest", ...args].map(quote).join(" "); +} + +export async function writeRunRecord( + directory: string, + record: NativeRunRecord, +): Promise { + const name = `native-${record.startedAt.replace(/[-:]|\.\d+/g, "")}-${record.commit.slice(0, 7)}-${randomUUID()}.json`; + await writeFile( + join(directory, name), + JSON.stringify(record, null, 2) + "\n", + { flag: "wx" }, + ); + return name; +} + +// Vitest ends the run, and the reporter writes its record, before the global teardown sweeps. The +// global setup opens a run, the reporter puts its record's path on the open run, and that run's +// cleanup amends that record only. The open run is kept on the process, not in this module, +// because the reporter and the global setup are loaded as separate module instances. +export interface RunRecordSlot { + path?: string; + swept?: boolean; +} +const runRecordKey = Symbol.for("libar-platform.native-run-record"); +const onProcess = globalThis as Record; +export function openRunRecord(): RunRecordSlot { + return (onProcess[runRecordKey] = {}); +} +export function rememberRunRecord(path: string): void { + const run = onProcess[runRecordKey]; + if (run !== undefined) run.path = path; +} +// A failure after the record was written, a failed final sweep, makes the run's record a failed +// one with the failure among its unhandled errors, replacing the old record in one step. Returns +// the record's path, if the run has one. +export function failRunRecord( + run: RunRecordSlot, + message: string, +): string | undefined { + const path = run.path; + if (path === undefined) return undefined; + const record = JSON.parse(readFileSync(path, "utf8")) as NativeRunRecord; + record.result = "failed"; + if (!record.unhandledErrors.includes(message)) + record.unhandledErrors.push(message); + writeFileSync(`${path}.next`, JSON.stringify(record, null, 2) + "\n"); + renameSync(`${path}.next`, path); + return path; +} diff --git a/harness/executable.ts b/harness/executable.ts new file mode 100644 index 0000000..598dcf9 --- /dev/null +++ b/harness/executable.ts @@ -0,0 +1,147 @@ +import { createHash, randomUUID } from "node:crypto"; +import { createReadStream } from "node:fs"; +import { + chmod, + mkdir, + mkdtemp, + readFile, + rename, + rm, + writeFile, +} from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { runChild } from "./child.js"; +export interface Executable { + path: string; + sha256: string; + release: string | null; + source: "cache" | "CONVEX_BACKEND_BINARY"; +} +export interface ResolveOptions { + cacheRoot?: string; + fetch?: typeof globalThis.fetch; + // Stops a download in progress; the call then rejects with the signal's reason. + signal?: AbortSignal; +} +interface ReleasePin { + release: string; + assets: Record< + string, + { file: string; zipSha256: string; executableSha256: string } + >; +} +const repositoryRoot = join(import.meta.dirname, ".."); +export function sha256OfFile(path: string): Promise { + return new Promise((done, fail) => { + const hash = createHash("sha256"); + createReadStream(path) + .on("data", (chunk) => hash.update(chunk)) + .on("error", fail) + .on("end", () => done(hash.digest("hex"))); + }); +} +// Settles with the work, or rejects with the signal's reason as soon as it aborts. The work's own +// rejection is handled either way. +function untilAborted(work: Promise, signal: AbortSignal): Promise { + if (signal.aborted) { + work.catch(() => undefined); + return Promise.reject(signal.reason as Error); + } + return new Promise((done, fail) => { + const stop = () => fail(signal.reason as Error); + signal.addEventListener("abort", stop, { once: true }); + work.then(done, fail).finally(() => { + signal.removeEventListener("abort", stop); + }); + }); +} +export async function resolveExecutable( + options: ResolveOptions = {}, +): Promise { + const stop = options.signal ?? new AbortController().signal; + stop.throwIfAborted(); + const pin = JSON.parse( + await readFile(join(import.meta.dirname, "backend-release.json"), "utf8"), + ) as ReleasePin; + const override = process.env.CONVEX_BACKEND_BINARY; + if (override !== undefined && override !== "") { + const path = resolve(override); + const sha256 = await sha256OfFile(path).catch(() => { + throw new Error(`CONVEX_BACKEND_BINARY names no readable file: ${path}`); + }); + const pinned = Object.values(pin.assets).some( + (asset) => asset.executableSha256 === sha256, + ); + return { + path, + sha256, + release: pinned ? pin.release : null, + source: "CONVEX_BACKEND_BINARY", + }; + } + const asset = pin.assets[`${process.platform}-${process.arch}`]; + if (asset === undefined) + throw new Error( + `No backend release is pinned for ${process.platform}/${process.arch}. Set CONVEX_BACKEND_BINARY to a convex-local-backend executable.`, + ); + const directory = join( + options.cacheRoot ?? join(repositoryRoot, ".cache"), + pin.release, + asset.file, + ); + const path = join(directory, "convex-local-backend"); + const pinned: Executable = { + path, + sha256: asset.executableSha256, + release: pin.release, + source: "cache", + }; + // A cache entry is used only when its hash is the pinned one. Anything else is replaced. + const cached = await sha256OfFile(path).catch(() => undefined); + if (cached === asset.executableSha256) return pinned; + stop.throwIfAborted(); + await mkdir(directory, { recursive: true }); + const work = await mkdtemp(join(directory, "download-")); + try { + // An interrupt while the directory was made; the finally removes it. + stop.throwIfAborted(); + const response = await untilAborted( + (options.fetch ?? fetch)( + `https://github.com/get-convex/convex-backend/releases/download/${pin.release}/${asset.file}`, + { signal: AbortSignal.any([stop, AbortSignal.timeout(300000)]) }, + ), + stop, + ); + if (!response.ok) + throw new Error( + `Backend download failed with status ${response.status}: ${asset.file}`, + ); + const bytes = Buffer.from(await untilAborted(response.arrayBuffer(), stop)); + const zipSha256 = createHash("sha256").update(bytes).digest("hex"); + if (zipSha256 !== asset.zipSha256) + throw new Error( + `Backend download ${asset.file} has SHA-256 ${zipSha256}; harness/backend-release.json pins ${asset.zipSha256}`, + ); + const zip = join(work, "backend.zip"); + await writeFile(zip, bytes); + await runChild("unzip", "unzip", ["-q", zip, "-d", work], { + timeoutMs: 60000, + signal: stop, + }); + const extracted = join(work, "convex-local-backend"); + const sha256 = await sha256OfFile(extracted); + if (sha256 !== asset.executableSha256) + throw new Error( + `The executable inside ${asset.file} has SHA-256 ${sha256}; harness/backend-release.json pins ${asset.executableSha256}`, + ); + await chmod(extracted, 0o755); + stop.throwIfAborted(); + // Rename within one directory, so no reader sees a partial file. + const staged = join(directory, `staged-${randomUUID()}`); + await rename(extracted, staged); + await rename(staged, path); + return pinned; + } finally { + await rm(work, { recursive: true, force: true }); + } +} diff --git a/harness/identity.ts b/harness/identity.ts new file mode 100644 index 0000000..a6d6266 --- /dev/null +++ b/harness/identity.ts @@ -0,0 +1,39 @@ +import { exportJWK, generateKeyPair, SignJWT } from "jose"; +export const fixtureIssuerUrl = "https://fixture-issuer.test"; +export const fixtureApplicationId = "fixture-app"; +export const fixtureKeyId = "fixture-key-1"; +export interface FixtureIssuer { + readonly issuer: string; + readonly applicationID: string; + readonly jwks: string; + token(subject: string): Promise; +} +export async function createFixtureIssuer( + issuer: string = fixtureIssuerUrl, +): Promise { + const { publicKey, privateKey } = await generateKeyPair("ES256", { + extractable: true, + }); + const jwk = { + ...(await exportJWK(publicKey)), + kid: fixtureKeyId, + alg: "ES256", + use: "sig", + }; + return { + issuer, + applicationID: fixtureApplicationId, + jwks: + "data:text/plain;charset=utf-8;base64," + + Buffer.from(JSON.stringify({ keys: [jwk] })).toString("base64"), + token: (subject) => + new SignJWT({}) + .setProtectedHeader({ kid: fixtureKeyId, alg: "ES256", typ: "JWT" }) + .setIssuer(issuer) + .setAudience(fixtureApplicationId) + .setSubject(subject) + .setIssuedAt() + .setExpirationTime("10m") + .sign(privateKey), + }; +} diff --git a/harness/local-write-rate.ts b/harness/local-write-rate.ts new file mode 100644 index 0000000..0aa8625 --- /dev/null +++ b/harness/local-write-rate.ts @@ -0,0 +1,7 @@ +import { setTimeout as sleep } from "node:timers/promises"; +export const localWriteRateBytesPerSecond = 4 * 1024 * 1024; +export async function paceAfterWrite(bytesWritten: number): Promise { + await sleep( + Math.ceil((bytesWritten / localWriteRateBytesPerSecond) * 1000 * 1.25), + ); +} diff --git a/harness/native-run.ts b/harness/native-run.ts new file mode 100644 index 0000000..6549623 --- /dev/null +++ b/harness/native-run.ts @@ -0,0 +1,92 @@ +import { readdirSync, rmSync } from "node:fs"; +import { mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { TestProject } from "vitest/node"; +import { failRunRecord, openRunRecord } from "./evidence.js"; +import type { RunRecordSlot } from "./evidence.js"; +import { signalOwned } from "./ownership.js"; +import { resolveExecutable } from "./executable.js"; +import type { Executable } from "./executable.js"; +export interface NativeRun { + directory: string; + executable: Executable; +} +declare module "vitest" { + interface ProvidedContext { + nativeRun: NativeRun; + } +} +// Kills every owned backend under a directory and removes the directory. A backend directory +// whose process cannot be inspected is kept with its ownership record, the rest is removed, and +// the sweep then fails naming what it kept. +export function sweep(directory: string): void { + const reasons: string[] = []; + // Returns whether root or anything under it is kept. + function killOwned(root: string): boolean { + try { + signalOwned(root); + } catch (error) { + reasons.push((error as Error).message); + return true; + } + let entries; + try { + entries = readdirSync(root, { withFileTypes: true }); + } catch { + return false; + } + const keeping = new Set(); + for (const entry of entries) + if (entry.isDirectory() && killOwned(join(root, entry.name))) + keeping.add(entry.name); + if (keeping.size === 0) return false; + for (const entry of entries) + if (!keeping.has(entry.name)) + rmSync(join(root, entry.name), { recursive: true, force: true }); + return true; + } + if (!killOwned(directory)) { + rmSync(directory, { recursive: true, force: true }); + return; + } + throw new Error( + `The sweep kept ${reasons.length === 1 ? "a backend directory" : `${reasons.length} backend directories`} with the ownership record, because it could not tell whether the backend is running. ${reasons.join(". ")}.`, + ); +} +export default async function setup( + project: TestProject, +): Promise<() => Promise> { + const executable = await resolveExecutable(); + const directory = await mkdtemp(join(tmpdir(), "libar-native-")); + const run = openRunRecord(); + project.provide("nativeRun", { directory, executable }); + process.once("exit", () => { + try { + finalSweep(directory, run); + } catch (error) { + process.stderr.write(`${(error as Error).message}\n`); + process.exitCode = 1; + } + }); + return async () => finalSweep(directory, run); +} +// The run's last sweep, once: the teardown or, without one, the exit. Its failure also fails the +// run's record, which is written before it. +export function finalSweep(directory: string, run: RunRecordSlot): void { + if (run.swept === true) return; + run.swept = true; + try { + sweep(directory); + } catch (error) { + const message = (error as Error).message; + try { + failRunRecord(run, message); + } catch (amend) { + throw new Error( + `${message} The run record could not be marked failed: ${(amend as Error).message}`, + ); + } + throw error; + } +} diff --git a/harness/native.ts b/harness/native.ts new file mode 100644 index 0000000..15838ef --- /dev/null +++ b/harness/native.ts @@ -0,0 +1,73 @@ +import { + codeAnchor, + codeAnchorId, + ref, +} from "@libar-dev/software-delivery-protocol"; +import { TestRunner, inject, onTestFinished } from "vitest"; +import { startBackend } from "./backend.js"; +import type { Executable } from "./executable.js"; +import type { Backend } from "./backend.js"; +import type { JsonValue, NativeTestFacts } from "./evidence.js"; +import { createFixtureIssuer } from "./identity.js"; +// Binds the code under harness/ to its Spec in the corpus graph. +const anchor = codeAnchor({ + id: codeAnchorId("impl:platform.native-harness"), + label: "the native harness", + satisfies: ref("spec:platform.native-harness"), +}); +void anchor; +function current() { + const test = TestRunner.getCurrentTest(); + if (test === undefined) + throw new Error("fixtureBackend() and measure() must run inside a test"); + const facts: NativeTestFacts = (test.meta.native ??= { + backends: [], + measurements: [], + }); + return { test, facts }; +} +export async function fixtureBackend( + options: { + issuer?: string; + executable?: Executable; + parentDirectory?: string; + deploy?: boolean; + } = {}, +): Promise { + const { test, facts } = current(); + const run = inject("nativeRun"); + const issuer = await createFixtureIssuer(options.issuer); + let owned: Backend | undefined; + const starting = startBackend({ + executable: options.executable ?? run.executable, + issuer, + parentDirectory: options.parentDirectory ?? run.directory, + signal: test.context.signal, + }).then(async (backend) => { + owned = backend; + try { + if (options.deploy !== false) await backend.admin.deploy(); + return backend; + } catch (error) { + await backend.dispose().catch(() => undefined); + throw error; + } + }); + // Registered before the first await on the start, so a test that times out during the start + // still has its backend removed. + onTestFinished(async () => { + await starting.catch(() => undefined); + if (owned === undefined) return; + facts.backends.push(owned.facts()); + await owned.dispose(); + }); + return starting; +} +export function measure(name: string, value: JsonValue): void { + current().facts.measurements.push({ name, value }); +} +export function required(value: T | undefined, what: string): T { + if (value === undefined) + throw new Error(`An earlier step did not set ${what}`); + return value; +} diff --git a/harness/ownership.ts b/harness/ownership.ts new file mode 100644 index 0000000..543231d --- /dev/null +++ b/harness/ownership.ts @@ -0,0 +1,134 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync, renameSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +// Publish without an await after spawn. A parent sweep sees the whole record or no record. +export function publishOwnership( + directory: string, + pid: number, + instanceName: string, +): void { + if (!Number.isSafeInteger(pid) || pid <= 0) + throw new Error("Invalid backend process id"); + const staged = join(directory, "pid.next"); + writeFileSync(staged, JSON.stringify({ pid, instanceName, directory }), { + flag: "wx", + }); + renameSync(staged, join(directory, "pid")); +} +interface OwnershipRecord { + pid: number; + instanceName: string; + directory: string; +} +function readRecord(directory: string): OwnershipRecord | undefined { + try { + const record = JSON.parse( + readFileSync(join(directory, "pid"), "utf8"), + ) as Partial | null; + if ( + typeof record !== "object" || + record === null || + typeof record.pid !== "number" || + !Number.isSafeInteger(record.pid) || + record.pid <= 0 || + record.directory !== directory || + typeof record.instanceName !== "string" || + !/^libar-[0-9a-f-]{36}$/.test(record.instanceName) + ) + return undefined; + return record as OwnershipRecord; + } catch { + return undefined; + } +} +// ps -p exits with status 1 and prints nothing when no process has the id, on macOS and on Linux +// procps. The process table confirms it, so a ps that fails in that same way for another reason +// is not taken as absence. +function gone(pid: number, error: unknown): boolean { + const failure = error as { + status?: unknown; + stdout?: unknown; + stderr?: unknown; + }; + if (failure.status !== 1 || failure.stdout !== "" || failure.stderr !== "") + return false; + try { + process.kill(pid, 0); + return false; + } catch (probe) { + return (probe as NodeJS.ErrnoException).code === "ESRCH"; + } +} +// ps answers for a process whose command line it could not read with the name alone, in brackets +// on Linux procps and in parentheses on macOS. A zombie answers `` on macOS and +// `[name] ` on Linux procps, which is no backend's command line, so it counts as absent +// below. A zombie that answers with its name alone is told apart by its state, Z, and a process +// gone by then by the same proof as the first inspection. +function zombie(pid: number): boolean { + try { + return execFileSync("ps", ["-p", String(pid), "-o", "state="], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }) + .trim() + .startsWith("Z"); + } catch (error) { + return gone(pid, error); + } +} +function unreadable(answer: string): boolean { + return answer === "" || /^\[.*\]$/s.test(answer) || /^\(.*\)$/s.test(answer); +} +// Kills the backend a directory's ownership record names, when that process is still the backend. +// A missing or invalid record, a process that is gone and a process that is another program are +// left alone. Throws when it cannot tell, so the caller keeps the record. The message names the +// process id and the directory, never the command line, which holds the instance secret. +export function signalOwned(directory: string): void { + const record = readRecord(directory); + if (record === undefined) return; + let command: string; + try { + command = execFileSync( + "ps", + ["-ww", "-p", String(record.pid), "-o", "command="], + { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, + ); + } catch (error) { + if (gone(record.pid, error)) return; + const { code, status } = error as { code?: unknown; status?: unknown }; + throw new Error( + `Could not tell whether backend process ${record.pid} recorded in ${directory} is running: ${ + typeof status === "number" + ? `ps exited with status ${status}` + : `ps could not run (${String(code)})` + }`, + ); + } + const answer = command.trim(); + if (unreadable(answer)) { + if (zombie(record.pid)) return; + throw new Error( + `Could not tell whether backend process ${record.pid} recorded in ${directory} is running: ps could not read its command line`, + ); + } + // The random instance name and storage path survive a wrapper's exec and distinguish PID reuse. + // Never print this command: it contains the instance secret. + if ( + !command.includes(`--instance-name ${record.instanceName} `) || + !command.includes(`--local-storage ${join(directory, "storage")} `) || + !command.trimEnd().endsWith(join(directory, "backend.sqlite3")) + ) + return; + try { + process.kill(record.pid, "SIGKILL"); + } catch (error) { + // ESRCH: it exited after the inspection. Any other failure does not prove that it exited, so + // the caller keeps the directory and the record. + const code = (error as NodeJS.ErrnoException).code; + if (code !== "ESRCH") + throw new Error( + `Could not stop backend process ${record.pid} recorded in ${directory}: ${String(code)}`, + ); + } +} diff --git a/harness/wait.ts b/harness/wait.ts new file mode 100644 index 0000000..b1238a7 --- /dev/null +++ b/harness/wait.ts @@ -0,0 +1,42 @@ +import { setTimeout as sleep } from "node:timers/promises"; +export { + localWriteRateBytesPerSecond, + paceAfterWrite, +} from "./local-write-rate.js"; +export async function waitUntil( + description: string, + condition: () => boolean | Promise, + timeoutMs = 5000, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (await condition()) return; + await sleep(10); + } + throw new Error(`Timed out after ${timeoutMs} ms waiting for ${description}`); +} +export async function within( + promise: Promise, + description: string, + timeoutMs = 10000, +): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout( + () => + reject( + new Error( + `Timed out after ${timeoutMs} ms waiting for ${description}`, + ), + ), + timeoutMs, + ); + }), + ]); + } finally { + clearTimeout(timer); + } +} diff --git a/harness/watch.ts b/harness/watch.ts new file mode 100644 index 0000000..1487b6f --- /dev/null +++ b/harness/watch.ts @@ -0,0 +1,255 @@ +// The watch mode behind `npm run dev`: one disposable backend, `convex dev` bound to it, until an +// interrupt or until the CLI exits. scripts/dev.mjs supplies the real steps; the pure tests supply +// their own. +import type { ChildProcess } from "node:child_process"; +import { existsSync } from "node:fs"; +import { readFile, rm } from "node:fs/promises"; +import { createInterface } from "node:readline"; +import type { Readable, Writable } from "node:stream"; +import { convexCli } from "./admin.js"; +import type { CliCall } from "./admin.js"; +import type { Backend, StartOptions } from "./backend.js"; +import { redact } from "./child.js"; +import type { Executable } from "./executable.js"; +import type { FixtureIssuer } from "./identity.js"; + +export type StopSignal = "SIGINT" | "SIGTERM"; +// The reason an interrupted watch aborts with. +export class Interrupt extends Error { + readonly signal: StopSignal; + constructor(signal: StopSignal) { + super(`Interrupted by ${signal}`); + this.signal = signal; + } +} +export const exitCodes: Record = { + SIGINT: 130, + SIGTERM: 143, +}; + +// Copies a stream to a destination line by line with the secrets removed. A secret split across +// chunks is whole by the time its line is complete, and the last line is copied when the stream +// ends, with or without a newline. +export function relay( + stream: Readable, + destination: Writable, + secrets: readonly string[], +): Promise { + return new Promise((done) => { + createInterface({ input: stream, crlfDelay: Infinity }) + .on("line", (line) => destination.write(`${redact(line, secrets)}\n`)) + .once("close", () => done()); + }); +} + +// convex dev, given --url and --admin-key, writes CONVEX_URL and CONVEX_SITE_URL lines to +// .env.local. A file is that file only when every non-empty line is one of those two lines naming +// this run's backend. An empty file names no backend and is not that file. +export function writtenForBackend( + content: string, + backend: { url: string; siteUrl: string }, +): boolean { + const lines = content.split(/\r?\n/).filter((line) => line.trim() !== ""); + return ( + lines.length > 0 && + lines.every( + (line) => + line === `CONVEX_URL=${backend.url}` || + line === `CONVEX_SITE_URL=${backend.siteUrl}`, + ) + ); +} + +// Removes the env file when the CLI was spawned and the file is the one it wrote for this backend. +// Any other file stays, and so does a file that was there before the CLI was spawned. +export async function settleEnvFile( + path: string, + run: { spawned: boolean; url: string; siteUrl: string | undefined }, +): Promise<"absent" | "removed" | "kept"> { + let content: string; + try { + content = await readFile(path, "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return "absent"; + throw error; + } + if ( + !run.spawned || + run.siteUrl === undefined || + !writtenForBackend(content, { url: run.url, siteUrl: run.siteUrl }) + ) + return "kept"; + await rm(path, { force: true }); + return "removed"; +} + +export interface WatchSteps { + resolveExecutable(signal: AbortSignal): Promise; + createFixtureIssuer(): Promise; + startBackend( + options: StartOptions, + ): Promise>; + // The site URL the backend reports, which convex dev writes beside its URL. + siteUrl( + backend: { url: string; adminKey: string }, + signal: AbortSignal, + ): Promise; + makeHome(): Promise; + removeHome(home: string): Promise; + spawn(call: CliCall): ChildProcess; +} +export interface WatchOptions { + envFile: string; + signal: AbortSignal; + steps: WatchSteps; + stdout: Writable; + stderr: Writable; + // How long the CLI has to exit after SIGINT before it is killed. + stopGraceMs?: number; +} + +function ended(child: ChildProcess): boolean { + return child.exitCode !== null || child.signalCode !== null; +} +function exitOf(child: ChildProcess): Promise { + if (ended(child)) return Promise.resolve(); + return new Promise((done) => { + child.once("exit", () => done()); + child.once("error", () => done()); + }); +} +function aborted(signal: AbortSignal): Promise { + if (signal.aborted) return Promise.resolve(); + return new Promise((done) => + signal.addEventListener("abort", () => done(), { once: true }), + ); +} +function untilAborted(work: Promise, signal: AbortSignal): Promise { + return Promise.race([ + work, + aborted(signal).then(() => { + throw signal.reason as Error; + }), + ]); +} +function within(work: Promise, ms: number): Promise { + return new Promise((done) => { + const timer = setTimeout(done, ms); + void work.then(() => { + clearTimeout(timer); + done(); + }); + }); +} + +// Runs the watch and returns the exit code. Every phase observes the signal. Whatever ends the run, +// each cleanup step is tried, and the run ends with one line saying why it ended. +export async function watch(options: WatchOptions): Promise { + const { envFile, signal, steps } = options; + const say = (line: string) => options.stderr.write(`npm run dev: ${line}\n`); + const refusal = `${envFile} exists. convex dev would write this backend's URL into it. Move it away and run again.`; + if (existsSync(envFile)) { + say(refusal); + return 1; + } + let backend: Pick | undefined; + let home: string | undefined; + let homeCreation: Promise | undefined; + let siteUrl: string | undefined; + let child: ChildProcess | undefined; + const relays: Promise[] = []; + const secrets: string[] = []; + let failure: string | undefined; + try { + const executable = await steps.resolveExecutable(signal); + const issuer = await untilAborted(steps.createFixtureIssuer(), signal); + backend = await steps.startBackend({ executable, issuer, signal }); + secrets.push(backend.adminKey); + homeCreation = steps.makeHome(); + home = await untilAborted(homeCreation, signal); + siteUrl = await untilAborted(steps.siteUrl(backend, signal), signal); + signal.throwIfAborted(); + options.stdout.write( + `npm run dev: backend ready at ${backend.url}. Press Ctrl-C to stop.\n`, + ); + // A file that appeared while the backend started is the developer's, as at the start. + if (existsSync(envFile)) throw new Error(refusal); + signal.throwIfAborted(); + child = steps.spawn( + convexCli({ url: backend.url, adminKey: backend.adminKey, home }, "dev"), + ); + let spawnError: Error | undefined; + child.once("error", (error) => (spawnError = error)); + if (child.stdout !== null) + relays.push(relay(child.stdout, options.stdout, secrets)); + if (child.stderr !== null) + relays.push(relay(child.stderr, options.stderr, secrets)); + await Promise.race([exitOf(child), aborted(signal)]); + if (!signal.aborted) + failure = + spawnError !== undefined + ? `convex dev could not start: ${spawnError.message}` + : `convex dev exited with ${child.exitCode === null ? `signal ${child.signalCode}` : `code ${child.exitCode}`}`; + } catch (error) { + if (!signal.aborted) failure = (error as Error).message; + } + const problems: string[] = []; + async function step(name: string, action: () => Promise) { + try { + await action(); + } catch (error) { + problems.push(`${name}: ${(error as Error).message}`); + } + } + const cli = child; + await step("stopping convex dev", async () => { + if (cli === undefined || ended(cli)) return; + cli.kill("SIGINT"); + const timer = setTimeout( + () => cli.kill("SIGKILL"), + options.stopGraceMs ?? 5000, + ); + await exitOf(cli); + clearTimeout(timer); + }); + // The relays get a grace period for the CLI's last lines. Then its streams are let go of, so a + // descendant that keeps the other ends open cannot keep this process alive. + await within(Promise.all(relays).then(), 1000); + cli?.stdout?.destroy(); + cli?.stderr?.destroy(); + await step("removing the backend", async () => backend?.dispose()); + const madeHome = home; + const creation = homeCreation; + await step("removing the CLI home", async () => { + // A creation the interrupt cut short still makes its directory, so it is awaited. + const made = madeHome ?? (await creation?.catch(() => undefined)); + if (made !== undefined) await steps.removeHome(made); + }); + const url = backend?.url ?? ""; + await step("settling .env.local", async () => { + const outcome = await settleEnvFile(envFile, { + spawned: cli !== undefined, + url, + siteUrl, + }); + if (outcome === "kept") + say( + `left ${envFile} in place: it is not the file convex dev wrote for this backend.`, + ); + }); + if (problems.length > 0) { + say(`cleanup failed: ${redact(problems.join("; "), secrets)}`); + return 1; + } + if (failure !== undefined) { + say(redact(failure, secrets)); + return 1; + } + if (signal.aborted) { + const reason = signal.reason as unknown; + const stopSignal = reason instanceof Interrupt ? reason.signal : "SIGINT"; + say(`stopped by ${stopSignal}.`); + return exitCodes[stopSignal]; + } + return 0; +} diff --git a/package-lock.json b/package-lock.json index ab6f273..20146a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,67 @@ "name": "libar-platform", "version": "0.0.0", "devDependencies": { - "@libar-dev/software-delivery-protocol": "file:vendor/libar-dev-software-delivery-protocol-0.0.0-5993da7.tgz" + "@convex-dev/eslint-plugin": "5.0.0", + "@edge-runtime/vm": "5.0.0", + "@eslint/js": "10.0.1", + "@libar-dev/software-delivery-protocol": "file:vendor/libar-dev-software-delivery-protocol-0.0.0-5993da7.tgz", + "@types/node": "24.19.0", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@typescript/native": "npm:typescript@7.0.2", + "convex": "1.46.0", + "convex-helpers": "0.1.124", + "convex-test": "0.0.60", + "eslint": "10.11.0", + "happy-dom": "20.14.5", + "jose": "6.2.12", + "prettier": "3.9.9", + "react": "19.3.0", + "react-dom": "19.3.0", + "typescript": "npm:@typescript/typescript6@6.0.2", + "typescript-eslint": "8.71.0", + "vitest": "5.0.3" }, "engines": { - "node": ">=22" + "node": "^24.0.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@convex-dev/eslint-plugin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@convex-dev/eslint-plugin/-/eslint-plugin-5.0.0.tgz", + "integrity": "sha512-UFhZ7bJBo4mTfa829/mntoAwyC9aKwXe6dxCV7PianSdAfFIbdCwCjWPBmhqn1hNlpqhVU385C/vjL8CXfxA2Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@typescript-eslint/types": "~8.58.0", + "@typescript-eslint/utils": "~8.58.0" + }, + "peerDependencies": { + "convex": "^1.43.0" } }, "node_modules/@cucumber/gherkin": { @@ -21,165 +78,4234 @@ "dev": true, "license": "MIT", "dependencies": { - "@cucumber/messages": ">=34.0.0 <35" + "@cucumber/messages": ">=34.0.0 <35" + } + }, + "node_modules/@cucumber/messages": { + "version": "34.2.1", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-34.2.1.tgz", + "integrity": "sha512-aj2iCAG9ZOpMVcMxcgShifv5fbR96b7SMgdWcPXXh2QbFp7OEcAnQ5MC5YfWUDSDasmw7FU5NJEsOrXdTQOU1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@edge-runtime/primitives": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/primitives/-/primitives-6.0.0.tgz", + "integrity": "sha512-FqoxaBT+prPBHBwE1WXS1ocnu/VLTQyZ6NMUBAdbP7N2hsFTTxMC/jMu2D/8GAlMQfxeuppcPuCUk/HO3fpIvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@edge-runtime/vm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/vm/-/vm-5.0.0.tgz", + "integrity": "sha512-NKBGBSIKUG584qrS1tyxVpX/AKJKQw5HgjYEnPLC0QsTw79JrGn+qUr8CXFb955Iy7GUdiiUv1rJ6JBGvaKb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@edge-runtime/primitives": "6.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@libar-dev/software-delivery-protocol": { + "version": "0.0.0", + "resolved": "file:vendor/libar-dev-software-delivery-protocol-0.0.0-5993da7.tgz", + "integrity": "sha512-NR0Cyq7AIepjToh1uR/2D4BwmCGDDWeIHXDh2yb60+sBvdRpdYZTrOmBs/p0S2HPxnpZzSLxtP/fkfoLoW48ng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cucumber/gherkin": "42.0.1", + "@cucumber/messages": "34.2.1", + "ts-morph": "~28.0.0", + "yaml": "2.9.0" + }, + "bin": { + "sdp": "dist/cli/sdp.js" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "vitest": ">=2" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@ts-morph/common": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.29.0.tgz", + "integrity": "sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.14" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz", + "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.0.tgz", + "integrity": "sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/type-utils": "8.71.0", + "@typescript-eslint/utils": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", + "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.0", + "@typescript-eslint/types": "^8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", + "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", + "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", + "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", + "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.0", + "@typescript-eslint/tsconfig-utils": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.0.tgz", + "integrity": "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", + "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.11.tgz", + "integrity": "sha512-YChdK5txDjwGUvgR7oCJcLGkwi3LDh8Zx8tS7ndYciLEg/oOCL26VUBSUEtgS7EjiybcBqFxh5j0rAdDX7/bbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.0.tgz", + "integrity": "sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", + "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.0", + "@typescript-eslint/types": "^8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", + "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", + "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", + "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", + "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.0", + "@typescript-eslint/tsconfig-utils": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", + "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", + "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.2", + "@typescript-eslint/types": "^8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", + "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", + "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.0.tgz", + "integrity": "sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", + "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.0", + "@typescript-eslint/types": "^8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", + "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", + "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", + "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", + "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.0", + "@typescript-eslint/tsconfig-utils": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.0.tgz", + "integrity": "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", + "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", + "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", + "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.2", + "@typescript-eslint/tsconfig-utils": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", + "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", + "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript/native": { + "name": "typescript", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/@typescript/old": { + "name": "typescript", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.3", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convex": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.46.0.tgz", + "integrity": "sha512-dhBwDQTZDOhi5SUGAX17EWNehCdNq6/UFrSL0eLZuGfMkdsQbCVWVRZCzbZ5CF+/pgvoGxtApbEoTSipqL4JTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esbuild": "0.27.0", + "prettier": "^3.0.0", + "ws": "8.21.0" + }, + "bin": { + "convex": "bin/main.js" + }, + "engines": { + "node": ">=20.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@auth0/auth0-react": "^2.0.1", + "@clerk/clerk-react": "^4.12.8 || ^5.0.0", + "@clerk/react": "^6.4.3", + "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@auth0/auth0-react": { + "optional": true + }, + "@clerk/clerk-react": { + "optional": true + }, + "@clerk/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/convex-helpers": { + "version": "0.1.124", + "resolved": "https://registry.npmjs.org/convex-helpers/-/convex-helpers-0.1.124.tgz", + "integrity": "sha512-Gbu/jq6I3x5Y51cXP5c7+CI3t15vX5Xi2G0tOym3rxpBtYqrI+qWwxkqZgJYrHHccY42wZudB8h6kS3fak8Aeg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "convex-helpers": "bin.cjs" + }, + "peerDependencies": { + "@standard-schema/spec": "^1.0.0", + "convex": "^1.43.0", + "hono": "^4.0.5", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0", + "typescript": "^5.5 || ^6.0.0 || ^7.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@standard-schema/spec": { + "optional": true + }, + "hono": { + "optional": true + }, + "react": { + "optional": true + }, + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/convex-test": { + "version": "0.0.60", + "resolved": "https://registry.npmjs.org/convex-test/-/convex-test-0.0.60.tgz", + "integrity": "sha512-qCAFx+CO6bmpXNc9s1jZvw8NJPS4yM074ib87PUY675FsiaRRW4ALotEB+RbVCYDRMYxh623KMnYZBlrMJIv1Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "convex": "^1.43.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", + "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/happy-dom": { + "version": "20.14.5", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.5.tgz", + "integrity": "sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/@cucumber/messages": { - "version": "34.2.1", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-34.2.1.tgz", - "integrity": "sha512-aj2iCAG9ZOpMVcMxcgShifv5fbR96b7SMgdWcPXXh2QbFp7OEcAnQ5MC5YfWUDSDasmw7FU5NJEsOrXdTQOU1A==", + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } }, - "node_modules/@libar-dev/software-delivery-protocol": { - "version": "0.0.0", - "resolved": "file:vendor/libar-dev-software-delivery-protocol-0.0.0-5993da7.tgz", - "integrity": "sha512-NR0Cyq7AIepjToh1uR/2D4BwmCGDDWeIHXDh2yb60+sBvdRpdYZTrOmBs/p0S2HPxnpZzSLxtP/fkfoLoW48ng==", + "node_modules/ts-morph": { + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-28.0.0.tgz", + "integrity": "sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.29.0", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "name": "@typescript/typescript6", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript6/-/typescript6-6.0.2.tgz", + "integrity": "sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@cucumber/gherkin": "42.0.1", - "@cucumber/messages": "34.2.1", - "ts-morph": "~28.0.0", - "yaml": "2.9.0" + "@typescript/old": "npm:typescript@^6" }, "bin": { - "sdp": "dist/cli/sdp.js" + "tsc6": "bin/tsc6" + } + }, + "node_modules/typescript-eslint": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.0.tgz", + "integrity": "sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.0", + "@typescript-eslint/parser": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0" }, "engines": { - "node": ">=20" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "vitest": ">=2" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "vitest": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@ts-morph/common": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.29.0.tgz", - "integrity": "sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==", + "node_modules/typescript-eslint/node_modules/@typescript-eslint/project-service": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", + "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", "dev": true, "license": "MIT", "dependencies": { - "minimatch": "^10.0.1", - "path-browserify": "^1.0.1", - "tinyglobby": "^0.2.14" + "@typescript-eslint/tsconfig-utils": "^8.71.0", + "@typescript-eslint/types": "^8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", + "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0" + }, "engines": { - "node": "18 || 20 || >=22" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "node_modules/typescript-eslint/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", + "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, "engines": { - "node": "20 || >=22" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/code-block-writer": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", - "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", + "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", + "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.0", + "@typescript-eslint/tsconfig-utils": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">=12.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.0.tgz", + "integrity": "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", + "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.8" + "@typescript-eslint/types": "8.71.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": "18 || 20 || >=22" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", "dev": true, "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "node_modules/vitest": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.3", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" }, "engines": { - "node": ">=12.0.0" + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" }, "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } } }, - "node_modules/ts-morph": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-28.0.0.tgz", - "integrity": "sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==", + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", "dependencies": { - "@ts-morph/common": "~0.29.0", - "code-block-writer": "^13.0.3" + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/yaml": { @@ -197,6 +4323,19 @@ "funding": { "url": "https://github.com/sponsors/eemeli" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 885f062..7fee206 100644 --- a/package.json +++ b/package.json @@ -5,16 +5,46 @@ "type": "module", "description": "The Convex transactional domain platform: its design as a Software Delivery Protocol corpus, and the code built from it.", "engines": { - "node": ">=22" + "node": "^24.0.0" }, "scripts": { "sdp": "sdp", "check": "python3 design/tools/check.py", - "validate": "sdp validate design", - "view": "sdp view design", - "census": "sdp census design" + "validate": "sdp validate", + "view": "sdp view", + "census": "sdp census", + "sdp:build": "npx sdp build", + "typecheck": "npm run sdp:build && tsc", + "lint": "eslint fixture harness tests scripts vitest.config.ts eslint.config.js", + "format:check": "prettier --check fixture harness tests scripts evidence/README.md README.md package.json tsconfig.json convex.json vitest.config.ts eslint.config.js .github/workflows/ci.yml .prettierrc.json", + "test:types": "npm run sdp:build && vitest run --project types", + "test:pure": "vitest run --project pure", + "test:simulator": "vitest run --project simulator", + "test:native": "npm run sdp:build && vitest run --project native", + "test": "npm run sdp:build && vitest run --project types --project pure --project simulator", + "codegen": "node scripts/codegen.mjs", + "dev": "node scripts/dev.mjs" }, "devDependencies": { - "@libar-dev/software-delivery-protocol": "file:vendor/libar-dev-software-delivery-protocol-0.0.0-5993da7.tgz" + "@convex-dev/eslint-plugin": "5.0.0", + "@edge-runtime/vm": "5.0.0", + "@eslint/js": "10.0.1", + "@libar-dev/software-delivery-protocol": "file:vendor/libar-dev-software-delivery-protocol-0.0.0-5993da7.tgz", + "@types/node": "24.19.0", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@typescript/native": "npm:typescript@7.0.2", + "convex": "1.46.0", + "convex-helpers": "0.1.124", + "convex-test": "0.0.60", + "eslint": "10.11.0", + "happy-dom": "20.14.5", + "jose": "6.2.12", + "prettier": "3.9.9", + "react": "19.3.0", + "react-dom": "19.3.0", + "typescript": "npm:@typescript/typescript6@6.0.2", + "typescript-eslint": "8.71.0", + "vitest": "5.0.3" } } diff --git a/scripts/codegen.mjs b/scripts/codegen.mjs new file mode 100644 index 0000000..2d4f7b8 --- /dev/null +++ b/scripts/codegen.mjs @@ -0,0 +1,45 @@ +// Rewrites fixture/convex/_generated and fixture/convex/annex/_generated. +// Convex's codegen analyzes the functions on a deployment, so this starts a disposable backend, +// runs codegen against it and removes it. +import { registerHooks } from "node:module"; + +// Node 24 runs the harness's TypeScript by stripping its types. The harness imports its own +// modules as ".js", which is what the compiler wants, so those resolve to the ".ts" files here. +registerHooks({ + resolve(specifier, context, nextResolve) { + if (specifier.startsWith(".") && specifier.endsWith(".js")) { + try { + return nextResolve(`${specifier.slice(0, -3)}.ts`, context); + } catch { + // A real .js file. Resolve it as written. + } + } + return nextResolve(specifier, context); + }, +}); + +const { resolveExecutable } = await import("../harness/executable.ts"); +const { createFixtureIssuer } = await import("../harness/identity.ts"); +const { startBackend } = await import("../harness/backend.ts"); + +const controller = new AbortController(); +const interrupt = (signal) => + controller.abort(new Error(`Codegen interrupted by ${signal}`)); +const onInt = () => interrupt("SIGINT"); +const onTerm = () => interrupt("SIGTERM"); +process.on("SIGINT", onInt); +process.on("SIGTERM", onTerm); +let backend; +try { + backend = await startBackend({ + executable: await resolveExecutable({ signal: controller.signal }), + issuer: await createFixtureIssuer(), + signal: controller.signal, + }); + controller.signal.throwIfAborted(); + await backend.admin.codegen(); +} finally { + await backend?.dispose(); + process.off("SIGINT", onInt); + process.off("SIGTERM", onTerm); +} diff --git a/scripts/dev.mjs b/scripts/dev.mjs new file mode 100644 index 0000000..10df174 --- /dev/null +++ b/scripts/dev.mjs @@ -0,0 +1,75 @@ +// Watch mode. Starts one disposable backend and runs `convex dev` against it, so that +// fixture/convex/_generated and fixture/convex/annex/_generated follow every change to a fixture +// file. Runs until SIGINT or SIGTERM, or until convex dev exits, then removes what it started. +// The lifecycle is harness/watch.ts; this file supplies the real steps. +import { spawn } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { registerHooks } from "node:module"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// Node 24 runs the harness's TypeScript by stripping its types. The harness imports its own +// modules as ".js", which is what the compiler wants, so those resolve to the ".ts" files here. +registerHooks({ + resolve(specifier, context, nextResolve) { + if (specifier.startsWith(".") && specifier.endsWith(".js")) { + try { + return nextResolve(`${specifier.slice(0, -3)}.ts`, context); + } catch { + // A real .js file. Resolve it as written. + } + } + return nextResolve(specifier, context); + }, +}); + +const { resolveExecutable } = await import("../harness/executable.ts"); +const { createFixtureIssuer } = await import("../harness/identity.ts"); +const { startBackend } = await import("../harness/backend.ts"); +const { Interrupt, watch } = await import("../harness/watch.ts"); + +const controller = new AbortController(); +const onInt = () => controller.abort(new Interrupt("SIGINT")); +const onTerm = () => controller.abort(new Interrupt("SIGTERM")); +process.on("SIGINT", onInt); +process.on("SIGTERM", onTerm); +try { + process.exitCode = await watch({ + envFile: join(import.meta.dirname, "..", ".env.local"), + signal: controller.signal, + stdout: process.stdout, + stderr: process.stderr, + steps: { + resolveExecutable: (signal) => resolveExecutable({ signal }), + createFixtureIssuer, + startBackend, + // The same read convex dev makes before it writes the site URL to .env.local. + async siteUrl(backend, signal) { + const response = await fetch( + `${backend.url}/api/v1/get_canonical_urls`, + { + headers: { Authorization: `Convex ${backend.adminKey}` }, + signal: AbortSignal.any([signal, AbortSignal.timeout(10000)]), + }, + ); + if (!response.ok) + throw new Error( + `Reading the backend's site URL failed with status ${response.status}`, + ); + return (await response.json()).convexSiteUrl; + }, + // The Backend keeps its own home inside; the CLI gets a home of its own beside it. + makeHome: () => mkdtemp(join(tmpdir(), "libar-dev-home-")), + removeHome: (home) => rm(home, { recursive: true, force: true }), + spawn: (call) => + spawn(call.file, call.args, { + cwd: call.cwd, + env: call.env, + stdio: ["ignore", "pipe", "pipe"], + }), + }, + }); +} finally { + process.off("SIGINT", onInt); + process.off("SIGTERM", onTerm); +} diff --git a/tests/native/admin-key-acting-as-identity-reaches-internal-function.test.ts b/tests/native/admin-key-acting-as-identity-reaches-internal-function.test.ts new file mode 100644 index 0000000..571f9c1 --- /dev/null +++ b/tests/native/admin-key-acting-as-identity-reaches-internal-function.test.ts @@ -0,0 +1,82 @@ +import { ConvexHttpClient } from "convex/browser"; +import { getFunctionName, makeFunctionReference } from "convex/server"; +import { expect } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { api, internal } from "../../fixture/convex/_generated/api.js"; +import { adminKeyActingAsIdentityReachesInternalFunctionContract as contract } from "../../generated/contracts/platform.native-harness.admin-key-acting-as-identity-reaches-internal-function.contract.js"; +import type { Backend } from "../../harness/backend.js"; +import { fixtureBackend, required } from "../../harness/native.js"; +const anchor = specTest({ + id: testAnchorId( + "test:platform.native-harness.admin-key-acting-as-identity-reaches-internal-function", + ), + verifies: ref( + "spec:platform.native-harness.admin-key-acting-as-identity-reaches-internal-function", + ), +}); +void anchor; +const target = getFunctionName(internal.notes.addInternal); +interface World { + backend?: Backend; + subject?: string; + client?: ConvexHttpClient; + outcome?: { returned: unknown } | { threw: unknown }; +} +bindExample(contract, (): World => ({}), { + "a disposable backend running the fixture composition, which registers an internal mutation that writes one row": + async (world) => { + world.backend = await fixtureBackend(); + }, + "a client holding the admin key and acting as the identity with subject {subject}": + (world, { subject }) => { + const backend = required(world.backend, "the backend"); + const client = new ConvexHttpClient(backend.url); + // This is the caller the harness rules forbid. It exists in this test only. + // Convex 1.46.0 implements setAdminAuth and strips it from its declarations. + ( + client as unknown as { + setAdminAuth( + key: string, + actingAs: { issuer: string; subject: string }, + ): void; + } + ).setAdminAuth(backend.adminKey, { + issuer: backend.issuer.issuer, + subject, + }); + world.subject = subject; + world.client = client; + }, + "the client calls the internal mutation by name": async (world) => { + world.outcome = await required(world.client, "the client") + .mutation(makeFunctionReference<"mutation">(target), {}) + .then( + (returned: unknown) => ({ returned }), + (threw: unknown) => ({ threw }), + ); + }, + "the call returns without an error": (world) => { + expect(world.outcome).toEqual({ returned: null }); + }, + "the table the internal mutation writes holds {rows} row": async ( + world, + { rows }, + ) => { + const backend = required(world.backend, "the backend"); + expect(await backend.admin.readTable("notes")).toHaveLength(rows); + // Inside a handler this client is the identity it acts as. + const subject = required(world.subject, "the subject"); + expect( + await required(world.client, "the client").query(api.identity.caller, {}), + ).toMatchObject({ + issuer: backend.issuer.issuer, + subject, + tokenIdentifier: `${backend.issuer.issuer}|${subject}`, + }); + }, +}); diff --git a/tests/native/component-auth.test.ts b/tests/native/component-auth.test.ts new file mode 100644 index 0000000..cfc3fdc --- /dev/null +++ b/tests/native/component-auth.test.ts @@ -0,0 +1,23 @@ +import { test } from "vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import { ordinaryClient } from "../../harness/clients.js"; +import { fixtureBackend, measure } from "../../harness/native.js"; +// Convex's documentation says ctx.auth is not available inside a component. The pinned backend +// answered with the caller's identity on 2026-10-01. Neither answer is a rule of the design, so +// this test records the answer and asserts only that the call completes. +test("native: a component query that calls ctx.auth.getUserIdentity() completes, and its answer is recorded", async () => { + const backend = await fixtureBackend(); + const client = ordinaryClient(backend.url, { + token: await backend.issuer.token("user-1"), + }); + const answer: unknown = await client.query( + api.identity.callerSeenByAnnex, + {}, + ); + const identity = answer as { issuer?: string; subject?: string } | null; + measure("identitySeenInsideComponent", { + answered: identity === null ? "null" : "an identity", + issuer: identity?.issuer ?? null, + subject: identity?.subject ?? null, + }); +}); diff --git a/tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.test.ts b/tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.test.ts new file mode 100644 index 0000000..f6bd072 --- /dev/null +++ b/tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.test.ts @@ -0,0 +1,35 @@ +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { probe3ComponentCallFromMutationContract as contract } from "../../generated/contracts/facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation.contract.js"; +import { + prepareCost, + sampleCost, + assertCost, + assertRecords, +} from "./read-cost.js"; +import type { CostWorld } from "./read-cost.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation", + ), + verifies: ref( + "spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-mutation", + ), +}); +void anchor; +type World = CostWorld; +bindExample(contract, (): World => ({}), { + "a parent mutation that reads the same document {reads} times in a row through one of three paths: a helper function, a nested query, a component query": + async (world, { reads }) => prepareCost(world, reads), + "a client calls the parent mutation once for each path, several times over": + async (world) => sampleCost(world, "mutation"), + "the median execution time through the component is {componentAgainstHelper} than through the helper": + (world, { componentAgainstHelper }) => + assertCost(world, componentAgainstHelper), + "the local function log holds {recordsPerCall} completion record for one parent call through the component, the parent's own": + (world, { recordsPerCall }) => assertRecords(world, recordsPerCall), +}); diff --git a/tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.test.ts b/tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.test.ts new file mode 100644 index 0000000..02cabc4 --- /dev/null +++ b/tests/native/f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.test.ts @@ -0,0 +1,35 @@ +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { probe3ComponentCallFromQueryContract as contract } from "../../generated/contracts/facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query.contract.js"; +import { + prepareCost, + sampleCost, + assertCost, + assertRecords, +} from "./read-cost.js"; +import type { CostWorld } from "./read-cost.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query", + ), + verifies: ref( + "spec:facts.f04-nested-calls-cost-more-than-helpers.probe-3-component-call-from-query", + ), +}); +void anchor; +type World = CostWorld; +bindExample(contract, (): World => ({}), { + "a parent query that reads the same document {reads} times in a row through one of three paths: a helper function, a nested query, a component query": + async (world, { reads }) => prepareCost(world, reads), + "a client calls the parent query once for each path, several times over, with arguments that defeat the query cache": + async (world) => sampleCost(world, "query"), + "the median execution time through the component is {componentAgainstHelper} than through the helper": + (world, { componentAgainstHelper }) => + assertCost(world, componentAgainstHelper), + "the local function log holds {recordsPerCall} completion record for one parent call through the component, the parent's own": + (world, { recordsPerCall }) => assertRecords(world, recordsPerCall), +}); diff --git a/tests/native/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.test.ts b/tests/native/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.test.ts new file mode 100644 index 0000000..588e5d5 --- /dev/null +++ b/tests/native/f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.test.ts @@ -0,0 +1,156 @@ +import { setTimeout as sleep } from "node:timers/promises"; +import { expect, onTestFinished } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import { probe1BackendRestartWithPendingMutationContract as contract } from "../../generated/contracts/facts.f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation.contract.js"; +import type { Backend } from "../../harness/backend.js"; +import { heldWebSocket, ordinarySocketClient } from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +import { within } from "../../harness/wait.js"; +import { connected, markerRows } from "./marker-rows.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation", + ), + verifies: ref( + "spec:facts.f05-react-client-retries-until-confirmed.probe-1-backend-restart-with-pending-mutation", + ), +}); +void anchor; +interface Trial { + trial: string; + // Milliseconds between the send and the SIGKILL. + delayMs: number; + // Whether the row was stored when the backend came back, before the client could send again. + committedBeforeKill: boolean; + // Whether the mutation's promise was still unresolved once the killed process had exited. + pendingAtKill: boolean; + // Whether it was still unresolved just before the held responses were released. + pendingAtRelease: boolean; + // Whether it resolved at all, within 15 seconds of the release. + resolved: boolean; + rows: number; +} +// The promise resolved after the restart: it was unresolved at the kill and at the release. +function resolvedAfterRestart(trial: Trial): boolean { + return trial.pendingAtKill && trial.pendingAtRelease && trial.resolved; +} +interface World { + backend?: Backend; + trials?: Trial[]; +} +// No delay for the first three trials in ten, then 1, 2, 4 and so on. On the pinned release a +// local mutation commits a few milliseconds after the send, so the trials fall on both sides. +function killDelayMs(index: number, trials: number): number { + const immediate = Math.ceil(trials * 0.3); + return index < immediate ? 0 : 2 ** (index - immediate); +} +bindExample(contract, (): World => ({}), { + "a WebSocket client, the ConvexClient of convex/browser, that sends a mutation which inserts one marker row": + async (world) => { + const backend = await fixtureBackend(); + world.backend = backend; + // With no restart, such a client gets its result and leaves one row. + const client = ordinarySocketClient(backend.url); + await client.mutation(api.markers.insert, { trial: "no restart" }); + await client.close(); + expect((await markerRows(backend)).get("no restart")).toBe(1); + }, + "the backend process is killed and restarted on the same storage while the client stays open, in each of {trials} trials": + async (world, { trials }) => { + const backend = required(world.backend, "the backend"); + const restarted: Trial[] = []; + for (let index = 0; index < trials; index++) { + const held = heldWebSocket(); + const client = ordinarySocketClient(backend.url, { + webSocket: held.WebSocket, + }); + onTestFinished(() => client.close()); + await connected(client); + // No response reaches the client, and it cannot reconnect, until the release below. + held.holdResponses(); + held.pauseConnections(); + const trial: Trial = { + trial: `restart ${index}`, + delayMs: killDelayMs(index, trials), + committedBeforeKill: false, + pendingAtKill: false, + pendingAtRelease: false, + resolved: false, + rows: 0, + }; + const result = client + .mutation(api.markers.insert, { trial: trial.trial }) + .then(() => { + trial.resolved = true; + }); + if (trial.delayMs > 0) await sleep(trial.delayMs); + await backend.kill(); + trial.pendingAtKill = !trial.resolved; + await backend.restart(); + trial.committedBeforeKill = + ((await markerRows(backend)).get(trial.trial) ?? 0) > 0; + trial.pendingAtRelease = !trial.resolved; + held.release(); + await within(result, `${trial.trial} to resolve`, 15000).catch( + () => undefined, + ); + trial.rows = (await markerRows(backend)).get(trial.trial) ?? 0; + restarted.push(trial); + await client.close(); + } + world.trials = restarted; + measure( + "trials", + restarted.map((trial) => ({ ...trial })), + ); + measure("restartSummary", { + trials: restarted.length, + resolvedAfterRestart: restarted.filter(resolvedAfterRestart).length, + resolvedBeforeRelease: restarted.filter( + (trial) => !trial.pendingAtRelease, + ).length, + killedBeforeCommit: restarted.filter( + (trial) => !trial.committedBeforeKill, + ).length, + killedAfterCommit: restarted.filter( + (trial) => trial.committedBeforeKill, + ).length, + rowCounts: restarted.map((trial) => trial.rows), + }); + }, + "the mutation's promise resolves after the restart in {resolvedTrials} trials": + (world, { resolvedTrials }) => { + const trials = required(world.trials, "the trials"); + // A promise that resolved before the kill, or before the release, did not resolve after + // the restart, and does not count. + expect( + trials.filter(resolvedAfterRestart).length, + JSON.stringify(trials), + ).toBe(resolvedTrials); + }, + "every trial leaves exactly {markerRows} marker row": ( + world, + { markerRows }, + ) => { + for (const trial of required(world.trials, "the trials")) + expect(trial.rows, JSON.stringify(trial)).toBe(markerRows); + }, + "at least {leastKilledBeforeCommit} trial was killed before its commit and at least {leastKilledAfterCommit} after it": + (world, { leastKilledBeforeCommit, leastKilledAfterCommit }) => { + const trials = required(world.trials, "the trials"); + expect( + trials.filter((trial) => !trial.committedBeforeKill).length, + JSON.stringify(trials), + ).toBeGreaterThanOrEqual(leastKilledBeforeCommit); + expect( + trials.filter((trial) => trial.committedBeforeKill).length, + JSON.stringify(trials), + ).toBeGreaterThanOrEqual(leastKilledAfterCommit); + }, +}); diff --git a/tests/native/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.test.ts b/tests/native/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.test.ts new file mode 100644 index 0000000..34067f2 --- /dev/null +++ b/tests/native/f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.test.ts @@ -0,0 +1,111 @@ +import { setTimeout as sleep } from "node:timers/promises"; +import { expect } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import { probe1ClientClosedWithPendingMutationContract as contract } from "../../generated/contracts/facts.f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation.contract.js"; +import type { Backend } from "../../harness/backend.js"; +import { heldWebSocket, ordinarySocketClient } from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +import { connected, markerRows } from "./marker-rows.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation", + ), + verifies: ref( + "spec:facts.f05-react-client-retries-until-confirmed.probe-1-client-closed-with-pending-mutation", + ), +}); +void anchor; +interface Trial { + trial: string; + // Milliseconds between the send and the close. + gapMs: number; + // Whether the client received the mutation's result. + confirmed: boolean; + rows: number; +} +interface World { + backend?: Backend; + trials?: Trial[]; +} +bindExample(contract, (): World => ({}), { + "a WebSocket client, the ConvexClient of convex/browser, that sends a mutation which inserts one marker row": + async (world) => { + const backend = await fixtureBackend(); + world.backend = backend; + // Left open, such a client gets its result and leaves one row. + const client = ordinarySocketClient(backend.url); + await client.mutation(api.markers.insert, { trial: "left open" }); + await client.close(); + expect((await markerRows(backend)).get("left open")).toBe(1); + }, + "the client is closed before the mutation's result reaches it, in each of {trials} trials": + async (world, { trials }) => { + const backend = required(world.backend, "the backend"); + const closed: Trial[] = []; + for (let index = 0; index < trials; index++) { + const held = heldWebSocket(); + const client = ordinarySocketClient(backend.url, { + webSocket: held.WebSocket, + }); + await connected(client); + // From here no response reaches the client, so the mutation stays pending until the close. + held.holdResponses(); + const trial: Trial = { + trial: `closed ${index}`, + gapMs: index % 6, + confirmed: false, + rows: 0, + }; + void client.mutation(api.markers.insert, { trial: trial.trial }).then( + () => { + trial.confirmed = true; + }, + () => undefined, + ); + if (trial.gapMs > 0) await sleep(trial.gapMs); + await client.close(); + closed.push(trial); + } + // A closed client reports nothing more, so there is no event to wait for. Give the backend + // time to finish what it had received, then read once. + await sleep(500); + const rows = await markerRows(backend); + for (const trial of closed) trial.rows = rows.get(trial.trial) ?? 0; + world.trials = closed; + measure( + "trials", + closed.map((trial) => ({ ...trial })), + ); + measure("closedClientSummary", { + trials: closed.length, + confirmed: closed.filter((trial) => trial.confirmed).length, + noRow: closed.filter((trial) => trial.rows === 0).length, + oneRow: closed.filter((trial) => trial.rows === 1).length, + }); + }, + "no trial leaves more than {mostMarkerRows} marker row": ( + world, + { mostMarkerRows }, + ) => { + for (const trial of required(world.trials, "the trials")) + expect(trial.rows, JSON.stringify(trial)).toBeLessThanOrEqual( + mostMarkerRows, + ); + }, + "at least {leastCommittedUnconfirmed} trial leaves its row although its client never received the result": + (world, { leastCommittedUnconfirmed }) => { + const trials = required(world.trials, "the trials"); + // Every close came with the mutation pending. + expect(trials.filter((trial) => trial.confirmed)).toEqual([]); + expect( + trials.filter((trial) => trial.rows > 0).length, + JSON.stringify(trials), + ).toBeGreaterThanOrEqual(leastCommittedUnconfirmed); + }, +}); diff --git a/tests/native/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.test.ts b/tests/native/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.test.ts new file mode 100644 index 0000000..914e0e8 --- /dev/null +++ b/tests/native/f05-react-client-retries-until-confirmed.probe-1-http-client-retry.test.ts @@ -0,0 +1,96 @@ +import type { ConvexHttpClient } from "convex/browser"; +import { expect } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import { probe1HttpClientRetryContract as contract } from "../../generated/contracts/facts.f05-react-client-retries-until-confirmed.probe-1-http-client-retry.contract.js"; +import type { Backend } from "../../harness/backend.js"; +import { countingFetch, ordinaryClient } from "../../harness/clients.js"; +import type { CountingFetch } from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +import { markerRows } from "./marker-rows.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f05-react-client-retries-until-confirmed.probe-1-http-client-retry", + ), + verifies: ref( + "spec:facts.f05-react-client-retries-until-confirmed.probe-1-http-client-retry", + ), +}); +void anchor; +interface World { + backend?: Backend; + transport?: CountingFetch; + client?: ConvexHttpClient; + // Requests the client had sent when each of the caller's two calls had failed. + requestsAfterCall?: number[]; +} +const failureOf = (call: Promise) => + call.then( + () => undefined, + (error: unknown) => error, + ); +bindExample(contract, (): World => ({}), { + "an HTTP client, ConvexHttpClient, whose transport delivers each request to the backend and then fails as a lost response would": + async (world) => { + const backend = await fixtureBackend(); + const transport = countingFetch({ loseResponses: true }); + Object.assign(world, { + backend, + transport, + client: ordinaryClient(backend.url, { fetch: transport.fetch }), + }); + }, + "its caller sends a mutation which inserts one marker row, sees the call fail, and sends the same mutation again": + async (world) => { + const client = required(world.client, "the client"); + const transport = required(world.transport, "the transport"); + world.requestsAfterCall = []; + for (let call = 0; call < 2; call++) { + const failure = await failureOf( + client.mutation(api.markers.insert, { trial: "response lost" }), + ); + expect(String(failure)).toContain("The response was lost"); + world.requestsAfterCall.push(transport.requests()); + } + }, + "the client sent {requestsPerCall} request for each call": ( + world, + { requestsPerCall }, + ) => { + const [afterFirst, afterSecond] = required( + world.requestsAfterCall, + "the request counts", + ); + measure("requestsAfterEachCall", [afterFirst ?? null, afterSecond ?? null]); + expect(afterFirst).toBe(requestsPerCall); + expect((afterSecond ?? 0) - (afterFirst ?? 0)).toBe(requestsPerCall); + }, + "the backend holds {markerRows} marker rows": async ( + world, + { markerRows: expected }, + ) => { + const backend = required(world.backend, "the backend"); + const rows = (await markerRows(backend)).get("response lost") ?? 0; + measure("httpRetrySummary", { + rows, + requestsAfterCall: required( + world.requestsAfterCall, + "the request counts", + ), + }); + expect(rows).toBe(expected); + // With no fault in the transport, one call sends one request and leaves one row. + const kept = countingFetch({ loseResponses: false }); + await ordinaryClient(backend.url, { fetch: kept.fetch }).mutation( + api.markers.insert, + { trial: "response kept" }, + ); + expect(kept.requests()).toBe(1); + expect((await markerRows(backend)).get("response kept")).toBe(1); + }, +}); diff --git a/tests/native/f13-transactions-have-limits.probe-4-component-call-shares-limits.test.ts b/tests/native/f13-transactions-have-limits.probe-4-component-call-shares-limits.test.ts new file mode 100644 index 0000000..b30055d --- /dev/null +++ b/tests/native/f13-transactions-have-limits.probe-4-component-call-shares-limits.test.ts @@ -0,0 +1,39 @@ +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { probe4ComponentCallSharesLimitsContract as contract } from "../../generated/contracts/facts.f13-transactions-have-limits.probe-4-component-call-shares-limits.contract.js"; +import { + seedLimits, + readTogether, + assertTogether, + readAlone, + writeLimits, +} from "./shared-limits.js"; +import type { LimitsWorld } from "./shared-limits.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f13-transactions-have-limits.probe-4-component-call-shares-limits", + ), + verifies: ref( + "spec:facts.f13-transactions-have-limits.probe-4-component-call-shares-limits", + ), +}); +void anchor; +type World = LimitsWorld; +bindExample(contract, (): World => ({}), { + "documents of {documentSize} each, {parentDocuments} that a parent mutation reads and {childDocuments} more that a component query reads": + async (world, { documentSize, parentDocuments, childDocuments }) => + seedLimits(world, documentSize, parentDocuments, childDocuments, "annex"), + "a client calls the parent mutation, which reads its documents and then calls the component query": + async (world) => readTogether(world), + "the call {together}": (world, { together }) => + assertTogether(world, together), + "the parent reading alone {parentAlone} and the component query called alone {childAlone}": + async (world, { parentAlone, childAlone }) => + readAlone(world, parentAlone, childAlone), + "the same split of bytes written by a parent mutation and a component mutation {writesTogether}": + async (world, { writesTogether }) => writeLimits(world, writesTogether), +}); diff --git a/tests/native/f13-transactions-have-limits.probe-4-nested-call-shares-limits.test.ts b/tests/native/f13-transactions-have-limits.probe-4-nested-call-shares-limits.test.ts new file mode 100644 index 0000000..5d50094 --- /dev/null +++ b/tests/native/f13-transactions-have-limits.probe-4-nested-call-shares-limits.test.ts @@ -0,0 +1,45 @@ +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { probe4NestedCallSharesLimitsContract as contract } from "../../generated/contracts/facts.f13-transactions-have-limits.probe-4-nested-call-shares-limits.contract.js"; +import { + seedLimits, + readTogether, + assertTogether, + readAlone, + writeLimits, +} from "./shared-limits.js"; +import type { LimitsWorld } from "./shared-limits.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f13-transactions-have-limits.probe-4-nested-call-shares-limits", + ), + verifies: ref( + "spec:facts.f13-transactions-have-limits.probe-4-nested-call-shares-limits", + ), +}); +void anchor; +type World = LimitsWorld; +bindExample(contract, (): World => ({}), { + "documents of {documentSize} each, {parentDocuments} that a parent mutation reads and {childDocuments} more that a nested query reads": + async (world, { documentSize, parentDocuments, childDocuments }) => + seedLimits( + world, + documentSize, + parentDocuments, + childDocuments, + "nested", + ), + "a client calls the parent mutation, which reads its documents and then calls the nested query through ctx.runQuery": + async (world) => readTogether(world), + "the call {together}": (world, { together }) => + assertTogether(world, together), + "the parent reading alone {parentAlone} and the nested query called alone {childAlone}": + async (world, { parentAlone, childAlone }) => + readAlone(world, parentAlone, childAlone), + "the same split of bytes written by a parent mutation and a nested mutation {writesTogether}": + async (world, { writesTogether }) => writeLimits(world, writesTogether), +}); diff --git a/tests/native/f13-transactions-have-limits.probe-4-nesting-depth.test.ts b/tests/native/f13-transactions-have-limits.probe-4-nesting-depth.test.ts new file mode 100644 index 0000000..8c4874b --- /dev/null +++ b/tests/native/f13-transactions-have-limits.probe-4-nesting-depth.test.ts @@ -0,0 +1,85 @@ +import { expect } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { probe4NestingDepthContract as contract } from "../../generated/contracts/facts.f13-transactions-have-limits.probe-4-nesting-depth.contract.js"; +import { api } from "../../fixture/convex/_generated/api.js"; +import type { Backend } from "../../harness/backend.js"; +import { ordinaryClient } from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f13-transactions-have-limits.probe-4-nesting-depth", + ), + verifies: ref( + "spec:facts.f13-transactions-have-limits.probe-4-nesting-depth", + ), +}); +void anchor; +interface World { + backend?: Backend; + deepest?: number; + failed?: number; + failure?: unknown; +} +bindExample(contract, (): World => ({}), { + "a mutation that calls itself through ctx.runMutation until its call stack holds a requested number of functions": + async (world) => { + world.backend = await fixtureBackend(); + expect(api.limits.recurse).toBeDefined(); + }, + "a client asks for each number from 1 upward": async (world) => { + const backend = required(world.backend, "the backend"); + const client = ordinaryClient(backend.url); + world.deepest = 0; + for (let stack = 1; stack <= 16; stack++) { + const label = `stack ${stack}`; + let answer: number | undefined; + let error: unknown; + try { + answer = await client.mutation(api.limits.recurse, { label, stack }); + } catch (caught) { + error = caught; + } + const rows = (await backend.admin.readTable("depthRows")).filter( + (row) => row.trial === label, + ).length; + measure(`stack${stack}Trial`, { + stack, + answer: answer ?? null, + error: error === undefined ? null : String(error), + rows, + }); + if (error !== undefined) { + expect(rows, String(error)).toBe(0); + world.failed = stack; + world.failure = error; + break; + } + expect(answer).toBe(stack); + expect(rows).toBe(stack); + world.deepest = stack; + } + measure("depthBoundary", { + deepest: world.deepest, + firstFailure: world.failed ?? null, + refusal: world.failure === undefined ? null : String(world.failure), + }); + expect(world.failed, "No stack failed through depth 16").toBeDefined(); + }, + "the first stack that does not commit is refused with an error whose text holds {depthRefusal}": + (world, { depthRefusal }) => { + expect(world.failure).toBeInstanceOf(Error); + expect(String(world.failure)).toContain(depthRefusal); + }, + "the deepest call stack that commits holds {deepestStack} functions, the top-level mutation included": + (world, { deepestStack }) => { + expect( + world.deepest, + `Deepest committed stack: ${world.deepest}; first failure: ${world.failed}`, + ).toBe(deepestStack); + }, +}); diff --git a/tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.test.ts b/tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.test.ts new file mode 100644 index 0000000..e451f36 --- /dev/null +++ b/tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.test.ts @@ -0,0 +1,40 @@ +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { probe2ComponentBoundaryContract as contract } from "../../generated/contracts/facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary.contract.js"; +import { + prepare, + parents, + callParents, + assertCaught, + assertPassed, + assertRows, +} from "./thrown-data.js"; +import type { ThrownWorld } from "./thrown-data.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary", + ), + verifies: ref( + "spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-component-boundary", + ), +}); +void anchor; +type World = ThrownWorld; +bindExample(contract, (): World => ({}), { + "a mutation inside a component that writes one row and then throws a ConvexError carrying structured data with code {code}": + async (world, { code }) => prepare(world, code, "annex"), + "two parent mutations that call it through the component API, one that writes a row of its own, catches the error and returns its data, and one that lets the error pass": + (world) => parents(world), + "a client calls each parent": async (world) => callParents(world), + "the catching parent returns the thrown data unchanged": (world) => + assertCaught(world), + "the client of the other parent reads the thrown data unchanged from the error it receives": + (world) => assertPassed(world), + "after the catching parent committed, the component mutation's table holds {childRows} rows and the parent's own table holds {parentRows} row": + async (world, { childRows, parentRows }) => + assertRows(world, childRows, parentRows), +}); diff --git a/tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.test.ts b/tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.test.ts new file mode 100644 index 0000000..6fe1bf9 --- /dev/null +++ b/tests/native/f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.test.ts @@ -0,0 +1,40 @@ +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { probe2NestedMutationContract as contract } from "../../generated/contracts/facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation.contract.js"; +import { + prepare, + parents, + callParents, + assertCaught, + assertPassed, + assertRows, +} from "./thrown-data.js"; +import type { ThrownWorld } from "./thrown-data.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation", + ), + verifies: ref( + "spec:facts.f14-convex-error-survives-nested-and-component-boundary.probe-2-nested-mutation", + ), +}); +void anchor; +type World = ThrownWorld; +bindExample(contract, (): World => ({}), { + "a mutation that writes one row and then throws a ConvexError carrying structured data with code {code}": + async (world, { code }) => prepare(world, code, "nested"), + "two parent mutations that call it through ctx.runMutation, one that writes a row of its own, catches the error and returns its data, and one that lets the error pass": + (world) => parents(world), + "a client calls each parent": async (world) => callParents(world), + "the catching parent returns the thrown data unchanged": (world) => + assertCaught(world), + "the client of the other parent reads the thrown data unchanged from the error it receives": + (world) => assertPassed(world), + "after the catching parent committed, the throwing mutation's table holds {childRows} rows and the parent's own table holds {parentRows} row": + async (world, { childRows, parentRows }) => + assertRows(world, childRows, parentRows), +}); diff --git a/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.test.ts b/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.test.ts new file mode 100644 index 0000000..1770101 --- /dev/null +++ b/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.test.ts @@ -0,0 +1,250 @@ +import type { ConvexHttpClient } from "convex/browser"; +import { setTimeout as sleep } from "node:timers/promises"; +import { expect, onTestFinished } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import type { ListPage, ListRow } from "../../fixture/convex/list.js"; +import { probe5HookSplitsAcrossBoundaryContract as contract } from "../../generated/contracts/facts.f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary.contract.js"; +import type { Backend } from "../../harness/backend.js"; +import { + ordinaryClient, + ordinarySocketClient, + watchQuery, +} from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +import { + idsOf, + insertInsideFirstRange, + loadPage, + seedList, + wholeList, +} from "./list-pages.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary", + ), + verifies: ref( + "spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-hook-splits-across-boundary", + ), +}); +void anchor; +interface Hook { + results: ListRow[]; + status: "LoadingFirstPage" | "CanLoadMore" | "LoadingMore" | "Exhausted"; + loadMore(numItems: number): void; +} +interface World { + backend?: Backend; + client?: ConvexHttpClient; + rows?: number; + pageSize?: number; + maximumRowsRead?: number; + // What the hook returned on its latest render. + hook?: () => Hook | undefined; + splitRequired?: () => Promise; + endCursor?: string; + capped?: ListPage; + missing?: ListRow[]; +} +// Waits until the hook's latest render satisfies `done`. React renders on its own schedule here, +// as it does in a browser, so this polls. +async function settled( + world: World, + description: string, + done: (hook: Hook) => boolean, +): Promise { + const latest = required(world.hook, "the hook"); + const deadline = Date.now() + 5000; + for (;;) { + const hook = latest(); + if (hook !== undefined && done(hook)) return hook; + if (Date.now() >= deadline) + throw new Error( + `Timed out after 5000 ms waiting for ${description}: the hook shows ${hook?.results.length ?? "no"} rows with status ${hook?.status ?? "none"}`, + ); + await sleep(20); + } +} +bindExample(contract, (): World => ({}), { + "a component list of {rows} rows read through the parent by the usePaginatedQuery hook of convex-helpers/react, in pages of {pageSize} with maximumRowsRead {maximumRowsRead}": + async (world, { rows, pageSize, maximumRowsRead }) => { + const backend = await fixtureBackend(); + const client = ordinaryClient(backend.url); + await seedList(client, rows); + Object.assign(world, { + backend, + client, + rows, + pageSize, + maximumRowsRead, + }); + // The DOM is a happy-dom window made here. Only window and document become globals, so + // fetch and WebSocket stay Node's and the harness works as in every other native test. + // React DOM reads them when it loads, so it is imported after they are set. + const { Window } = await import("happy-dom"); + const dom = new Window({ url: "http://localhost/" }); + Object.assign(globalThis, { window: dom, document: dom.document }); + const { createElement } = await import("react"); + const { createRoot } = await import("react-dom/client"); + const { ConvexProvider, ConvexReactClient } = + await import("convex/react"); + const { usePaginatedQuery } = await import("convex-helpers/react"); + const react = new ConvexReactClient(backend.url, { + unsavedChangesWarning: false, + }); + let latest: Hook | undefined; + function Shown() { + latest = usePaginatedQuery( + api.list.page, + { maximumRowsRead }, + { initialNumItems: pageSize }, + ) as Hook; + return null; + } + const container = dom.document.createElement("div"); + dom.document.body.appendChild(container); + const root = createRoot(container as unknown as Element); + onTestFinished(async () => { + root.unmount(); + await react.close(); + await dom.happyDOM.close(); + for (const name of ["window", "document"]) + Reflect.deleteProperty(globalThis, name); + }); + root.render( + createElement(ConvexProvider, { client: react }, createElement(Shown)), + ); + world.hook = () => latest; + await settled( + world, + "the first page", + (hook) => hook.status === "CanLoadMore", + ); + }, + "the hook has loaded every page": async (world) => { + const pageSize = required(world.pageSize, "the page size"); + let hook = await settled(world, "the hook to be idle", (shown) => + ["CanLoadMore", "Exhausted"].includes(shown.status), + ); + for (let more = 0; more < 10 && hook.status !== "Exhausted"; more++) { + const shown = hook.results.length; + hook.loadMore(pageSize); + hook = await settled( + world, + "the next page", + (next) => + next.status === "Exhausted" || + (next.status === "CanLoadMore" && next.results.length > shown), + ); + } + measure("hookBeforeInsert", { + rows: hook.results.length, + status: hook.status, + }); + expect(hook.status).toBe("Exhausted"); + expect(hook.results).toHaveLength(required(world.rows, "the row count")); + }, + "rows are inserted inside the first page's range until it holds {rowsInRange} rows": + async (world, { rowsInRange }) => { + const backend = required(world.backend, "the backend"); + const client = required(world.client, "the client"); + const pageSize = required(world.pageSize, "the page size"); + // A second subscription to the first page as the hook pinned it. It shows that the capped + // page came back SplitRequired, so a pass cannot come from a page that never needed the split. + const first = await loadPage(client, { + cursor: null, + numItems: pageSize, + }); + world.endCursor = first.continueCursor; + const socket = ordinarySocketClient(backend.url); + onTestFinished(() => socket.close()); + const capped = watchQuery(socket, api.list.page, { + maximumRowsRead: required(world.maximumRowsRead, "the read cap"), + paginationOpts: { + cursor: null, + numItems: pageSize, + endCursor: first.continueCursor, + }, + }); + await capped.until( + (page) => page.page.length === pageSize, + "the pinned first page", + ); + world.splitRequired = () => + capped.until( + (page) => page.pageStatus === "SplitRequired", + "the capped first page to come back SplitRequired", + ); + // One mutation, so the page jumps past its cap in one step. + await insertInsideFirstRange(client, rowsInRange - pageSize); + }, + "the hook's results hold {rowsShown} rows, in order and each once": async ( + world, + { rowsShown }, + ) => { + world.capped = await required( + world.splitRequired, + "the second subscription", + )(); + measure("hookCappedPage", { + status: world.capped.pageStatus ?? null, + rows: world.capped.page.length, + continueCursor: world.capped.continueCursor, + endCursor: required(world.endCursor, "the pinned end cursor"), + }); + expect(world.capped.pageStatus).toBe("SplitRequired"); + const list = await wholeList(required(world.backend, "the backend")); + // Give the hook the full wait to reach the bound count, then read what it shows whether or + // not it got there, so the measurement is recorded before the assertion. + await settled( + world, + `${rowsShown} rows`, + (hook) => + hook.status === "Exhausted" && hook.results.length === rowsShown, + ).catch(() => undefined); + const hook = required( + required(world.hook, "the hook")(), + "the hook's first render", + ); + const shown = new Set(idsOf(hook.results)); + world.missing = list.filter((row) => !shown.has(row._id)); + measure("hookSplitSummary", { + rowsInList: list.length, + rowsShown: hook.results.length, + status: hook.status, + missingPositions: list + .filter((row) => !shown.has(row._id)) + .map((row) => row.position), + }); + expect(hook.status, `Observed hook status: ${hook.status}`).toBe( + "Exhausted", + ); + expect(hook.results).toHaveLength(rowsShown); + expect(idsOf(hook.results)).toEqual( + idsOf(list.filter((row) => shown.has(row._id))), + ); + }, + "{rowsMissing} rows of the list are not shown, the ones between the capped page's continue cursor and its end cursor": + async (world, { rowsMissing }) => { + const capped = required(world.capped, "the capped page"); + const gap = await loadPage(required(world.client, "the client"), { + cursor: capped.continueCursor, + numItems: required(world.pageSize, "the page size"), + endCursor: required(world.endCursor, "the pinned end cursor"), + }); + const missing = required(world.missing, "the missing rows"); + measure("hookMissingCursorRange", { + missing: idsOf(missing), + betweenCursors: idsOf(gap.page), + }); + expect(missing, `Observed ${missing.length} missing rows`).toHaveLength( + rowsMissing, + ); + expect(idsOf(missing)).toEqual(idsOf(gap.page)); + }, +}); diff --git a/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.test.ts b/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.test.ts new file mode 100644 index 0000000..bcc0834 --- /dev/null +++ b/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.test.ts @@ -0,0 +1,169 @@ +import { expect } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { probe5PagesStayContiguousContract as contract } from "../../generated/contracts/facts.f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous.contract.js"; +import { onTestFinished } from "vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import type { Backend } from "../../harness/backend.js"; +import type { ListPage } from "../../fixture/convex/list.js"; +import { + ordinaryClient, + ordinarySocketClient, + watchQuery, +} from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +import { + idsOf, + insertInsideFirstRange, + loadPage, + seedList, + wholeList, + observedPage, +} from "./list-pages.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous", + ), + verifies: ref( + "spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-pages-stay-contiguous", + ), +}); +void anchor; +type Watch = ReturnType>; +interface World { + backend?: Backend; + pageSize?: number; + rows?: number; + cap?: number; + watches?: Watch[]; + pages?: ListPage[]; + ends?: string[]; + split?: ListPage; +} +bindExample(contract, (): World => ({}), { + "a component list of {rows} rows that a parent query relays from paginator, read in pages of {pageSize}": + async (world, { rows, pageSize }) => { + const backend = await fixtureBackend(); + await seedList(ordinaryClient(backend.url), rows); + Object.assign(world, { backend, rows, pageSize }); + }, + "a client subscribed to every page, each bounded by the end cursor its first load returned": + async (world) => { + const backend = required(world.backend, "the backend"); + const client = ordinaryClient(backend.url); + const socket = ordinarySocketClient(backend.url); + onTestFinished(() => socket.close()); + world.watches = []; + world.pages = []; + world.ends = []; + let cursor: string | null = null; + for (let index = 0; index < 100; index++) { + const numItems = required(world.pageSize, "the page size"); + const first = await loadPage(client, { cursor, numItems }); + world.ends.push(first.continueCursor); + const watch = watchQuery(socket, api.list.page, { + paginationOpts: { cursor, numItems, endCursor: first.continueCursor }, + }); + world.watches.push(watch); + world.pages.push( + await observedPage( + watch, + (page) => idsOf(page.page).join() === idsOf(first.page).join(), + "the initial pinned page", + `initialPinnedPage${index}`, + ), + ); + if (first.isDone) break; + cursor = first.continueCursor; + } + measure( + "initialPinnedPages", + world.pages.map((page) => ({ + rows: page.page.length, + isDone: page.isDone, + })), + ); + expect(required(world.pages.at(-1), "the last page").isDone).toBe(true); + expect(idsOf(world.pages.flatMap((page) => page.page))).toEqual( + idsOf(await wholeList(backend)), + ); + }, + "{inserted} rows are inserted inside the first page's range": async ( + world, + { inserted }, + ) => { + const backend = required(world.backend, "the backend"); + await insertInsideFirstRange(ordinaryClient(backend.url), inserted); + const watches = required(world.watches, "the subscriptions"); + world.pages = await Promise.all( + watches.map((watch, index) => + observedPage( + watch, + (page) => + page.page.length === + required(world.pages?.[index], "the initial page").page.length + + (index === 0 ? inserted : 0), + "the pages after the insert", + `pinnedPageAfterInsert${index}`, + ), + ), + ); + measure( + "pagesAfterInsert", + world.pages.map((page) => page.page.map((row) => row._id)), + ); + }, + "the subscribed pages together hold {rowsHeld} rows, every row of the list once and in order": + async (world, { rowsHeld }) => { + const pages = required(world.pages, "the updated pages"); + const list = await wholeList(required(world.backend, "the backend")); + measure("contiguityAfterInsert", { + actual: idsOf(pages.flatMap((page) => page.page)), + expected: idsOf(list), + }); + expect(pages.flatMap((page) => page.page)).toHaveLength(rowsHeld); + expect(idsOf(pages.flatMap((page) => page.page))).toEqual(idsOf(list)); + }, + "the first page now holds {firstPageRows} rows": async ( + world, + { firstPageRows }, + ) => { + const pages = required(world.pages, "the pages"); + expect(required(pages[0], "the first page").page).toHaveLength( + firstPageRows, + ); + const backend = required(world.backend, "the backend"); + const removed = required(pages[1]?.page[1], "a row inside the second page"); + await ordinaryClient(backend.url).mutation(api.list.remove, { + id: removed._id, + }); + const after = await Promise.all( + required(world.watches, "the subscriptions").map((watch, index) => + observedPage( + watch, + (page) => + page.page.length === + required(pages[index], "the old page").page.length - + (index === 1 ? 1 : 0), + "the pages after the deletion", + `pinnedPageAfterDelete${index}`, + ), + ), + ); + const list = await wholeList(backend); + measure("contiguitySummary", { + pages: after.length, + rowsAfterInsert: pages.flatMap((page) => page.page).length, + rowsAfterDelete: list.length, + removed: removed._id, + actual: idsOf(after.flatMap((page) => page.page)), + expected: idsOf(list), + }); + expect(idsOf(after.flatMap((page) => page.page))).toEqual(idsOf(list)); + expect(list.some((row) => row._id === removed._id)).toBe(false); + }, +}); diff --git a/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.test.ts b/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.test.ts new file mode 100644 index 0000000..ce318e5 --- /dev/null +++ b/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.test.ts @@ -0,0 +1,149 @@ +import { expect } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { probe5SplitRequiredRelayedContract as contract } from "../../generated/contracts/facts.f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed.contract.js"; +import { onTestFinished } from "vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import type { Backend } from "../../harness/backend.js"; +import type { ListPage } from "../../fixture/convex/list.js"; +import { + ordinaryClient, + ordinarySocketClient, + watchQuery, +} from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +import { + idsOf, + insertInsideFirstRange, + loadPage, + seedList, + wholeList, + observedPage, +} from "./list-pages.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed", + ), + verifies: ref( + "spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-split-required-relayed", + ), +}); +void anchor; +type Watch = ReturnType>; +interface World { + backend?: Backend; + pageSize?: number; + rows?: number; + cap?: number; + watches?: Watch[]; + pages?: ListPage[]; + ends?: string[]; + split?: ListPage; +} +bindExample(contract, (): World => ({}), { + "a component list of {rows} rows and a subscribed page of {pageSize}, bounded by its end cursor and read through the parent with maximumRowsRead {maximumRowsRead}": + async (world, { rows, pageSize, maximumRowsRead }) => { + const backend = await fixtureBackend(); + const client = ordinaryClient(backend.url); + await seedList(client, rows); + const first = await loadPage( + client, + { cursor: null, numItems: pageSize }, + maximumRowsRead, + ); + const socket = ordinarySocketClient(backend.url); + onTestFinished(() => socket.close()); + const watch = watchQuery(socket, api.list.page, { + maximumRowsRead, + paginationOpts: { + cursor: null, + numItems: pageSize, + endCursor: first.continueCursor, + }, + }); + const pinned = await observedPage( + watch, + (page) => page.page.length === pageSize, + "the initial pinned page", + "initialPinnedPage", + ); + Object.assign(world, { + backend, + rows, + pageSize, + cap: maximumRowsRead, + watches: [watch], + pages: [pinned], + ends: [first.continueCursor], + }); + }, + "rows are inserted inside the page's range until it holds {rowsInRange} rows": + async (world, { rowsInRange }) => { + const client = ordinaryClient(required(world.backend, "the backend").url); + await insertInsideFirstRange( + client, + rowsInRange - required(world.pageSize, "the page size"), + ); + world.split = await observedPage( + required(world.watches?.[0], "the subscription"), + (page) => page.pageStatus !== null && page.pageStatus !== undefined, + "a page with split status", + "splitPage", + ); + measure("relayedSplit", { + status: world.split.pageStatus ?? null, + splitCursor: world.split.splitCursor ?? null, + continueCursor: world.split.continueCursor, + endCursor: required(world.ends?.[0], "the pinned end"), + rows: world.split.page.length, + }); + }, + "the page the parent relays carries pageStatus {pageStatus} and a split cursor": + (world, { pageStatus }) => { + const split = required(world.split, "the relayed page"); + expect(split.pageStatus, JSON.stringify(split)).toBe(pageStatus); + expect(split.splitCursor, JSON.stringify(split)).toBeTypeOf("string"); + expect(split.splitCursor).not.toBe(""); + }, + "the page from the start to the split cursor and the page from the split cursor to the first page's end cursor together hold {rowsHeld} rows, every row of the range once and in order": + async (world, { rowsHeld }) => { + const backend = required(world.backend, "the backend"); + const client = ordinaryClient(backend.url); + const splitCursor = required( + required(world.split, "the split page").splitCursor ?? undefined, + "the split cursor", + ); + const endCursor = required(world.ends?.[0], "the pinned end cursor"); + const numItems = required(world.pageSize, "the page size"); + const left = await loadPage( + client, + { cursor: null, numItems, endCursor: splitCursor }, + world.cap, + ); + const right = await loadPage( + client, + { cursor: splitCursor, numItems, endCursor }, + world.cap, + ); + const wholeRange = await loadPage(client, { + cursor: null, + numItems, + endCursor, + }); + const joined = [...left.page, ...right.page]; + measure("splitSummary", { + pageStatus: world.split?.pageStatus ?? null, + left: idsOf(left.page), + right: idsOf(right.page), + wholeRange: idsOf(wholeRange.page), + }); + expect(joined).toHaveLength(rowsHeld); + expect(idsOf(joined)).toEqual(idsOf(wholeRange.page)); + const list = await wholeList(backend); + expect(idsOf(joined)).toEqual(idsOf(list.slice(0, rowsHeld))); + }, +}); diff --git a/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.test.ts b/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.test.ts new file mode 100644 index 0000000..84f32c0 --- /dev/null +++ b/tests/native/f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.test.ts @@ -0,0 +1,98 @@ +import { expect } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { probe5SubscriptionUpdatesContract as contract } from "../../generated/contracts/facts.f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates.contract.js"; +import { onTestFinished } from "vitest"; +import { setTimeout as sleep } from "node:timers/promises"; +import { api } from "../../fixture/convex/_generated/api.js"; +import type { Backend } from "../../harness/backend.js"; +import { + ordinaryClient, + ordinarySocketClient, + watchQuery, +} from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +import { seedList } from "./list-pages.js"; +const anchor = specTest({ + id: testAnchorId( + "test:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates", + ), + verifies: ref( + "spec:facts.f15-parent-query-over-component-query-stays-reactive.probe-5-subscription-updates", + ), +}); +void anchor; +type Watch = ReturnType>; +interface World { + backend?: Backend; + watch?: Watch; + before?: number; + id?: string; + values?: { label: string }[]; +} +bindExample(contract, (): World => ({}), { + "a client subscribed to a parent query that reads no table of its own and returns what a component query reads": + async (world) => { + const backend = await fixtureBackend(); + await seedList(ordinaryClient(backend.url), 1); + const socket = ordinarySocketClient(backend.url); + onTestFinished(() => socket.close()); + const watch = watchQuery(socket, api.list.first, {}); + const initial = await watch.until( + (row) => row?.label === "row 0", + "the initial component row", + 5000, + ); + expect(initial).not.toBeNull(); + Object.assign(world, { + backend, + watch, + id: required(initial ?? undefined, "the initial row")._id, + before: watch.values.length, + }); + }, + "a mutation changes the document inside the component": async (world) => { + await ordinaryClient(required(world.backend, "the backend").url).mutation( + api.list.relabel, + { id: required(world.id, "the component row id"), label: "changed" }, + ); + }, + "the subscription delivers {updates} changed value without the client asking again": + async (world, { updates }) => { + const watch = required(world.watch, "the subscription"); + let failure: unknown; + try { + await watch.until( + (row) => row?.label === "changed", + "the subscription update", + 5000, + ); + } catch (error) { + failure = error; + } + await sleep(100); + const values = watch.values.slice( + required(world.before, "the initial delivery count"), + ); + measure("subscriptionSummary", { + waitMs: 5000, + observationAfterDeliveryMs: 100, + values: values.map((row) => + row === null ? null : { id: row._id, label: row.label }, + ), + error: failure === undefined ? null : String(failure), + }); + expect(failure, String(failure)).toBeUndefined(); + expect(values, `Observed ${values.length} changed values`).toHaveLength( + updates, + ); + for (const row of values) { + expect(row?._id).toBe(world.id); + expect(row?.label).toBe("changed"); + } + }, +}); diff --git a/tests/native/fixture-issuer-token-yields-identity.test.ts b/tests/native/fixture-issuer-token-yields-identity.test.ts new file mode 100644 index 0000000..97b7502 --- /dev/null +++ b/tests/native/fixture-issuer-token-yields-identity.test.ts @@ -0,0 +1,81 @@ +import type { ConvexHttpClient } from "convex/browser"; +import type { UserIdentity } from "convex/server"; +import { expect } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import { fixtureIssuerTokenYieldsIdentityContract as contract } from "../../generated/contracts/platform.native-harness.fixture-issuer-token-yields-identity.contract.js"; +import type { Backend } from "../../harness/backend.js"; +import { ordinaryClient } from "../../harness/clients.js"; +import { createFixtureIssuer } from "../../harness/identity.js"; +import { fixtureBackend, required } from "../../harness/native.js"; +const anchor = specTest({ + id: testAnchorId( + "test:platform.native-harness.fixture-issuer-token-yields-identity", + ), + verifies: ref( + "spec:platform.native-harness.fixture-issuer-token-yields-identity", + ), +}); +void anchor; +interface World { + backend?: Backend; + subject?: string; + client?: ConvexHttpClient; + identity?: UserIdentity | null; +} +bindExample(contract, (): World => ({}), { + "a disposable backend whose environment variables name the fixture issuer {issuer} and its data-URI key set": + async (world, { issuer }) => { + world.backend = await fixtureBackend({ issuer }); + const environment = await world.backend.admin.environment(); + expect(environment.AUTH_ISSUER).toBe(issuer); + expect(environment.AUTH_JWKS).toMatch(/^data:/); + }, + "an ordinary client carrying a token the harness signed for subject {subject}": + async (world, { subject }) => { + const backend = required(world.backend, "the backend"); + world.subject = subject; + world.client = ordinaryClient(backend.url, { + token: await backend.issuer.token(subject), + }); + }, + "the client calls a public query that returns the caller's identity": async ( + world, + ) => { + world.identity = (await required(world.client, "the client").query( + api.identity.caller, + {}, + )) as UserIdentity | null; + }, + "the identity names issuer {identityIssuer} and subject {identitySubject}": ( + world, + { identityIssuer, identitySubject }, + ) => { + expect(world.identity).toMatchObject({ + issuer: identityIssuer, + subject: identitySubject, + tokenIdentifier: `${identityIssuer}|${identitySubject}`, + }); + }, + "the same query from a client with no token returns no identity": async ( + world, + ) => { + const backend = required(world.backend, "the backend"); + expect( + await ordinaryClient(backend.url).query(api.identity.caller, {}), + ).toBeNull(); + // A key outside the key set: the same issuer URL, another key pair. + const outsider = await createFixtureIssuer(backend.issuer.issuer); + const outside = ordinaryClient(backend.url, { + token: await outsider.token(required(world.subject, "the subject")), + }); + await expect(outside.query(api.identity.caller, {})).rejects.toThrow( + "InvalidAuthHeader", + ); + }, +}); diff --git a/tests/native/harness-backend.test.ts b/tests/native/harness-backend.test.ts new file mode 100644 index 0000000..8361a28 --- /dev/null +++ b/tests/native/harness-backend.test.ts @@ -0,0 +1,397 @@ +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { spawn } from "node:child_process"; +import { waitUntil } from "../../harness/wait.js"; +import { sweep } from "../../harness/native-run.js"; +import { + chmod, + mkdtemp, + readFile, + readdir, + rm, + stat, + writeFile, +} from "node:fs/promises"; +import { join } from "node:path"; +import { expect, inject, onTestFinished, test, TestRunner } from "vitest"; +import { sha256OfFile } from "../../harness/executable.js"; +import { fixtureBackend, measure } from "../../harness/native.js"; +// Binds this file to the harness Spec, whose rules it checks directly. +const anchor = specTest({ + id: testAnchorId("test:platform.native-harness.backend-lifecycle"), + verifies: ref("spec:platform.native-harness"), +}); +void anchor; + +async function scratch() { + // The run directory comes from os.tmpdir(); keep these under it so the parent can sweep them. + const directory = await mkdtemp( + join(inject("nativeRun").directory, "libar-harness-"), + ); + onTestFinished(() => rm(directory, { recursive: true, force: true })); + return directory; +} +async function storageDirectory(parent: string) { + const directories = await readdir(parent); + expect(directories).toHaveLength(1); + return join(parent, directories[0]!); +} + +test("native: stop and kill keep stored rows across restart, and dispose removes storage twice", async () => { + const parent = await scratch(); + const backend = await fixtureBackend({ parentDirectory: parent }); + await backend.admin.run("notes:addInternal"); + const directory = await storageDirectory(parent); + for (const end of [() => backend.stop(), () => backend.kill()]) { + await end(); + expect((await stat(join(directory, "backend.sqlite3"))).isFile()).toBe( + true, + ); + // A successful stop must mean the owned process has exited. + const pid = ( + JSON.parse(await readFile(join(directory, "pid"), "utf8")) as { + pid: number; + } + ).pid; + expect(() => process.kill(pid, 0)).toThrow(); + await backend.restart(); + expect(await backend.admin.readTable("notes")).toMatchObject([ + { source: "internal" }, + ]); + } + await backend.admin.setEnvironment({ + AUTH_ISSUER: "https://changed-issuer.test", + }); + expect(await backend.admin.environment()).toMatchObject({ + AUTH_ISSUER: "https://changed-issuer.test", + }); + expect(backend.facts().identitySource.issuer).toBe( + "https://changed-issuer.test", + ); + await backend.dispose(); + expect(await readdir(parent)).toEqual([]); + await backend.dispose(); + expect(await readdir(parent)).toEqual([]); + for (const action of [ + () => backend.restart(), + () => backend.stop(), + () => backend.kill(), + ]) + await expect(action()).rejects.toThrow("The backend is disposed"); + expect(await readdir(parent)).toEqual([]); +}); + +// The wrapper occupies a selected port before starting the real binary. The successful attempt +// replaces the wrapper process, so the harness owns the real backend PID even after SIGKILL. +async function contendedExecutable( + directory: string, + failures: number, + flag: string, +) { + const binary = inject("nativeRun").executable.path; + const hash = await sha256OfFile(binary); + measure("realBackendExecutableSha256", hash); + expect(TestRunner.getCurrentTest()?.meta.native?.measurements).toContainEqual( + { name: "realBackendExecutableSha256", value: hash }, + ); + const attempts = join(directory, "attempts.json"); + const path = join(directory, "contended.mjs"); + await writeFile( + path, + `#!/usr/bin/env node +import { spawnSync, spawn } from 'node:child_process'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { createServer } from 'node:net'; +const binary = ${JSON.stringify(binary)}; +const args = process.argv.slice(2); +if (args[0] === 'keygen') { + const result = spawnSync(binary, args, { encoding: 'utf8' }); + process.stdout.write(result.stdout); + process.stderr.write(result.stderr); + process.exit(result.status ?? 1); +} +const file = ${JSON.stringify(attempts)}; +let seen = []; +try { seen = JSON.parse(readFileSync(file, 'utf8')); } catch {} +seen.push(args); +writeFileSync(file, JSON.stringify(seen)); +if (seen.length > ${failures}) { + process.execve(binary, [binary, ...args], process.env); +} else { + const port = Number(args[args.indexOf(${JSON.stringify(flag)}) + 1]); + const listener = createServer(); + await new Promise((resolve, reject) => { + listener.once('error', reject); + listener.listen(port, '127.0.0.1', resolve); + }); + const child = spawn(binary, args, { stdio: 'inherit' }); + child.once('exit', (code) => listener.close(() => process.exit(code ?? 1))); +} +`, + ); + await chmod(path, 0o755); + return { + executable: { + path, + sha256: await sha256OfFile(path), + release: null, + source: "CONVEX_BACKEND_BINARY" as const, + }, + attempts, + }; +} +interface AttemptPorts { + main: string; + site: string; +} +async function portsOf(attempts: string): Promise { + const rows = JSON.parse(await readFile(attempts, "utf8")) as string[][]; + return rows.map((args) => ({ + main: args[args.indexOf("--port") + 1]!, + site: args[args.indexOf("--site-proxy-port") + 1]!, + })); +} +test.each(["--port", "--site-proxy-port"])( + "native: a taken %s causes fresh port pairs and the third start succeeds", + async (flag) => { + const directory = await scratch(); + const parent = await scratch(); + const { executable, attempts } = await contendedExecutable( + directory, + 2, + flag, + ); + const backend = await fixtureBackend({ + parentDirectory: parent, + executable, + deploy: false, + }); + const ports = await portsOf(attempts); + expect(ports).toHaveLength(3); + expect(new Set(ports.map((pair) => pair.main)).size).toBe(3); + expect(new Set(ports.map((pair) => pair.site)).size).toBe(3); + expect(ports.every((pair) => pair.main !== pair.site)).toBe(true); + expect(backend.url).toBe(`http://127.0.0.1:${ports[2]!.main}`); + expect(await backend.admin.environment()).toHaveProperty("AUTH_ISSUER"); + }, +); +test("native: three taken ports fail clearly and remove the failed start's storage", async () => { + const directory = await scratch(); + const parent = await scratch(); + const { executable, attempts } = await contendedExecutable( + directory, + 3, + "--port", + ); + await expect( + fixtureBackend({ parentDirectory: parent, executable, deploy: false }), + ).rejects.toThrow("The backend could not bind its ports in 3 attempts"); + expect(await portsOf(attempts)).toHaveLength(3); + expect(await readdir(parent)).toEqual([]); +}); + +test("native: stop escalates to SIGKILL when the owned process ignores SIGINT and keeps its storage", async () => { + const directory = await scratch(); + const parent = await scratch(); + const path = join(directory, "ignores-interrupt.mjs"); + const interrupted = join(directory, "interrupted"); + await writeFile( + path, + `#!/usr/bin/env node +import { createServer } from 'node:http'; +import { writeFileSync } from 'node:fs'; +const args = process.argv.slice(2); +if (args[0] === 'keygen') { + console.log('synthetic-admin-key'); + process.exit(0); +} +const option = (name) => args[args.indexOf(name) + 1]; +writeFileSync(args.at(-1), 'storage retained'); +process.on('SIGINT', () => writeFileSync(${JSON.stringify(interrupted)}, 'SIGINT')); +createServer((request, response) => { + response.end(request.url === '/instance_name' ? option('--instance-name') : '{}'); +}).listen(Number(option('--port')), '127.0.0.1'); +`, + ); + await chmod(path, 0o755); + const backend = await fixtureBackend({ + parentDirectory: parent, + deploy: false, + executable: { + path, + sha256: await sha256OfFile(path), + release: null, + source: "CONVEX_BACKEND_BINARY", + }, + }); + const storage = await storageDirectory(parent); + const started = Date.now(); + await backend.stop(); + expect(Date.now() - started).toBeGreaterThanOrEqual(5000); + expect(Date.now() - started).toBeLessThan(15000); + expect(await readFile(interrupted, "utf8")).toBe("SIGINT"); + const pid = ( + JSON.parse(await readFile(join(storage, "pid"), "utf8")) as { pid: number } + ).pid; + expect(() => process.kill(pid, 0)).toThrow(); + expect(await readFile(join(storage, "backend.sqlite3"), "utf8")).toBe( + "storage retained", + ); + await backend.dispose(); + expect(await readdir(parent)).toEqual([]); +}); + +test("native: a log mark from before a kill cannot be read after restart", async () => { + const backend = await fixtureBackend(); + const mark = await backend.admin.logMark(); + await backend.admin.run("notes:addInternal"); + await backend.kill(); + await backend.restart(); + await backend.admin.run("notes:addInternal"); + await expect( + backend.admin.completionsSince(mark, (records) => records.length > 0), + ).rejects.toThrow("another backend process"); +}); + +// The second process stays alive but never reports this run's instance name. +// A failed restart must reap it before rejecting. +test("native: a restart that stalls after spawning reaps its process", async () => { + const directory = await scratch(); + const parent = await scratch(); + const path = join(directory, "stalled-restart.mjs"); + const starts = join(directory, "starts"); + await writeFile( + path, + `#!/usr/bin/env node +import { createServer } from 'node:http'; +import { existsSync, writeFileSync } from 'node:fs'; +const args = process.argv.slice(2); +if (args[0] === 'keygen') { console.log('synthetic-admin-key'); process.exit(0); } +const option = (name) => args[args.indexOf(name) + 1]; +const restarted = existsSync(${JSON.stringify(starts)}); +writeFileSync(${JSON.stringify(starts)}, 'started'); +createServer((request, response) => response.end(request.url === '/instance_name' ? (restarted ? 'wrong-instance' : option('--instance-name')) : '{}')).listen(Number(option('--port')), '127.0.0.1'); +`, + ); + await chmod(path, 0o755); + const backend = await fixtureBackend({ + parentDirectory: parent, + deploy: false, + executable: { + path, + sha256: await sha256OfFile(path), + release: null, + source: "CONVEX_BACKEND_BINARY", + }, + }); + await backend.kill(); + await expect(backend.restart()).rejects.toThrow( + "The backend was not ready in 30000 ms", + ); + const storage = await storageDirectory(parent); + const pid = ( + JSON.parse(await readFile(join(storage, "pid"), "utf8")) as { pid: number } + ).pid; + expect(() => process.kill(pid, 0)).toThrow(); + await backend.dispose(); + expect(await readdir(parent)).toEqual([]); +}); + +test.each(["SIGINT", "SIGTERM"] as const)( + "native: codegen interrupted by %s removes its starting backend and storage", + async (signal) => { + const directory = await scratch(); + // TMPDIR is explicit so this test can inspect only its own codegen storage. + const storageRoot = await mkdtemp( + join(inject("nativeRun").directory, "libar-codegen-interrupt-"), + ); + onTestFinished(() => sweep(storageRoot)); + const path = join(directory, "not-ready.mjs"); + await writeFile( + path, + `#!/usr/bin/env node +const args = process.argv.slice(2); +if (args[0] === 'keygen') { console.log('synthetic-admin-key'); process.exit(0); } +setInterval(() => {}, 1000); +`, + ); + await chmod(path, 0o755); + const child = spawn(process.execPath, ["scripts/codegen.mjs"], { + cwd: join(import.meta.dirname, "../.."), + env: { ...process.env, TMPDIR: storageRoot, CONVEX_BACKEND_BINARY: path }, + stdio: "ignore", + }); + const exited = new Promise((done) => + child.once("exit", (code) => done(code)), + ); + onTestFinished(() => { + child.kill("SIGKILL"); + }); + let pid = 0; + await waitUntil( + "codegen's published backend ownership", + async () => { + for (const entry of await readdir(storageRoot)) { + try { + pid = ( + JSON.parse( + await readFile(join(storageRoot, entry, "pid"), "utf8"), + ) as { pid: number } + ).pid; + return pid > 0; + } catch { + /* Not yet published. */ + } + } + return false; + }, + 10000, + ); + child.kill(signal); + expect(await exited).not.toBe(0); + // This backend is a descendant spawned by this test's codegen process. + expect(() => process.kill(pid, 0)).toThrow(); + expect(await readdir(storageRoot)).toEqual([]); + }, +); + +test("native: backend output split across chunks is redacted before the rolling buffer clips it", async () => { + const directory = await scratch(); + const parent = await scratch(); + const path = join(directory, "secret-output.mjs"); + const secretFile = join(directory, "secret"); + await writeFile( + path, + `#!/usr/bin/env node +import { writeFileSync } from 'node:fs'; +const args = process.argv.slice(2); +if (args[0] === 'keygen') { console.log('synthetic-admin-key'); process.exit(0); } +const secret = args[args.indexOf('--instance-secret') + 1]; +writeFileSync(${JSON.stringify(secretFile)}, secret); +process.stderr.write(secret.slice(0, 20)); +setTimeout(() => { process.stderr.write(secret.slice(20) + 'x'.repeat(15990)); process.exit(1); }, 100); +`, + ); + await chmod(path, 0o755); + const error = await fixtureBackend({ + parentDirectory: parent, + deploy: false, + executable: { + path, + sha256: await sha256OfFile(path), + release: null, + source: "CONVEX_BACKEND_BINARY", + }, + }).then( + () => undefined, + (error: unknown) => error, + ); + expect(error).toBeInstanceOf(Error); + const secret = await readFile(secretFile, "utf8"); + expect((error as Error).message).not.toContain(secret.slice(-10)); + expect((error as Error).message).toContain("redacted]"); + expect(await readdir(parent)).toEqual([]); +}); diff --git a/tests/native/harness-sweep.test.ts b/tests/native/harness-sweep.test.ts new file mode 100644 index 0000000..758f94c --- /dev/null +++ b/tests/native/harness-sweep.test.ts @@ -0,0 +1,85 @@ +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { mkdtemp, readFile, readdir, rm, stat } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, inject, onTestFinished, test } from "vitest"; +import { fixtureBackend } from "../../harness/native.js"; +import { sweep } from "../../harness/native-run.js"; +import { waitUntil } from "../../harness/wait.js"; +// Binds this file to the harness Spec, whose rules it checks directly. +const anchor = specTest({ + id: testAnchorId("test:platform.native-harness.sweep"), + verifies: ref("spec:platform.native-harness"), +}); +void anchor; + +test("native: the parent sweep verifies and kills two real owned backend processes", async () => { + const parent = await mkdtemp(join(inject("nativeRun").directory, "sweep-")); + onTestFinished(() => rm(parent, { recursive: true, force: true })); + await fixtureBackend({ parentDirectory: parent, deploy: false }); + await fixtureBackend({ parentDirectory: parent, deploy: false }); + const directories = await readdir(parent); + expect(directories).toHaveLength(2); + const pids = await Promise.all( + directories.map(async (directory) => { + const record = JSON.parse( + await readFile(join(parent, directory, "pid"), "utf8"), + ) as { pid: number }; + expect(Number.isSafeInteger(record.pid) && record.pid > 0).toBe(true); + return record.pid; + }), + ); + sweep(parent); + await waitUntil("both swept backend processes to exit", () => + pids.every((pid) => { + try { + process.kill(pid, 0); + return false; + } catch { + return true; + } + }), + ); + await expect(stat(parent)).rejects.toThrow(); +}); + +// From review U, finding F2, with its assertions as written; the cleanup is this file's own. +test("native review: failed inspection must not silently discard ownership", async () => { + const parent = await mkdtemp( + join(inject("nativeRun").directory, "review-inspection-"), + ); + // With ps on the PATH again, this sweep kills the kept backend and removes the directory, in + // either order with the backend's own disposal. + onTestFinished(() => sweep(parent)); + await fixtureBackend({ parentDirectory: parent, deploy: false }); + + const [entry] = await readdir(parent); + const { pid } = JSON.parse( + await readFile(join(parent, entry!, "pid"), "utf8"), + ) as { pid: number }; + + const previousPath = process.env.PATH; + let failure: unknown; + try { + process.env.PATH = join(parent, "no-executables"); + try { + sweep(parent); + } catch (error) { + failure = error; + } + } finally { + if (previousPath === undefined) delete process.env.PATH; + else process.env.PATH = previousPath; + } + + expect(process.kill(pid, 0)).toBe(true); + expect(failure).toBeInstanceOf(Error); + expect((await stat(parent)).isDirectory()).toBe(true); + // This file's additions: the record is kept and the error names it without a command line. + expect((await stat(join(parent, entry!, "pid"))).isFile()).toBe(true); + expect((failure as Error).message).toContain(join(parent, entry!)); + expect((failure as Error).message).not.toContain("--instance-secret"); +}); diff --git a/tests/native/list-pages.ts b/tests/native/list-pages.ts new file mode 100644 index 0000000..d410f56 --- /dev/null +++ b/tests/native/list-pages.ts @@ -0,0 +1,84 @@ +import { watchQuery } from "../../harness/clients.js"; +import { measure } from "../../harness/native.js"; +import type { ConvexHttpClient } from "convex/browser"; +import { api } from "../../fixture/convex/_generated/api.js"; +import type { ListPage, ListRow } from "../../fixture/convex/list.js"; +import type { Backend } from "../../harness/backend.js"; +// The annex list in the order the by_position index gives it. +export async function wholeList(backend: Backend): Promise { + const rows = (await backend.admin.readTable("rows", { + component: "annex", + })) as unknown as ListRow[]; + return rows.sort( + (a, b) => + a.position - b.position || + a._creationTime - b._creationTime || + (a._id < b._id ? -1 : 1), + ); +} +export const idsOf = (rows: readonly ListRow[]) => rows.map((row) => row._id); +// Rows at positions 0, 1, 2 and so on, written in one mutation. +export async function seedList(client: ConvexHttpClient, rows: number) { + await client.mutation(api.list.insert, { + rows: Array.from({ length: rows }, (_, position) => ({ + position, + label: `row ${position}`, + })), + }); +} +// Rows between position 0 and position 1, written in one mutation. They fall inside the first +// page's range whatever the page size. +export async function insertInsideFirstRange( + client: ConvexHttpClient, + count: number, +) { + await client.mutation(api.list.insert, { + rows: Array.from({ length: count }, (_, index) => ({ + position: (index + 1) / (count + 1), + label: `inserted ${index}`, + })), + }); +} +// One page read once through the parent. +export function loadPage( + client: ConvexHttpClient, + paginationOpts: { + cursor: string | null; + numItems: number; + endCursor?: string; + }, + maximumRowsRead?: number, +): Promise { + return client.query(api.list.page, { + paginationOpts, + ...(maximumRowsRead === undefined ? {} : { maximumRowsRead }), + }); +} + +// Record all deliveries before asserting or rethrowing a wait failure. +export async function observedPage( + watch: ReturnType>, + matches: (page: ListPage) => boolean, + description: string, + measurementName: string, +): Promise { + try { + const page = await watch.until(matches, description, 5000); + measure(measurementName, { + rows: page.page.length, + status: page.pageStatus ?? null, + ids: idsOf(page.page), + }); + return page; + } catch (error) { + measure(measurementName, { + deliveries: watch.values.map((page) => ({ + rows: page.page.length, + status: page.pageStatus ?? null, + ids: idsOf(page.page), + })), + error: String(error), + }); + throw error; + } +} diff --git a/tests/native/marker-rows.ts b/tests/native/marker-rows.ts new file mode 100644 index 0000000..299b895 --- /dev/null +++ b/tests/native/marker-rows.ts @@ -0,0 +1,20 @@ +import type { ConvexClient } from "convex/browser"; +import type { Backend } from "../../harness/backend.js"; +import { waitUntil } from "../../harness/wait.js"; +// How many marker rows each trial left, read with the harness's admin access. +export async function markerRows( + backend: Backend, +): Promise> { + const rows = new Map(); + for (const marker of await backend.admin.readTable("markers")) { + const trial = String(marker.trial); + rows.set(trial, (rows.get(trial) ?? 0) + 1); + } + return rows; +} +export function connected(client: ConvexClient): Promise { + return waitUntil( + "the client's WebSocket to connect", + () => client.connectionState().isWebSocketConnected, + ); +} diff --git a/tests/native/ordinary-client-refused-component-function.test.ts b/tests/native/ordinary-client-refused-component-function.test.ts new file mode 100644 index 0000000..bec4357 --- /dev/null +++ b/tests/native/ordinary-client-refused-component-function.test.ts @@ -0,0 +1,200 @@ +import type { ConvexHttpClient } from "convex/browser"; +import { setTimeout as sleep } from "node:timers/promises"; +import { makeFunctionReference } from "convex/server"; +import { expect, onTestFinished } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { ordinaryClientRefusedComponentFunctionContract as contract } from "../../generated/contracts/platform.native-harness.ordinary-client-refused-component-function.contract.js"; +import type { Backend } from "../../harness/backend.js"; +import { ordinaryClient, ordinarySocketClient } from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +const anchor = specTest({ + id: testAnchorId( + "test:platform.native-harness.ordinary-client-refused-component-function", + ), + verifies: ref( + "spec:platform.native-harness.ordinary-client-refused-component-function", + ), +}); +void anchor; +// All three refused routes and the admin call use these names. +const component = "annex"; +const target = "notes:add"; +const refusalOf = (call: Promise) => + call.then( + () => undefined, + (error: unknown) => error, + ); +// The public endpoint, POST /api/mutation, given the component's name as a path prefix. +const byQualifiedName = (client: ConvexHttpClient) => + refusalOf( + client.mutation( + makeFunctionReference<"mutation">(`${component}/${target}`), + {}, + ), + ); +// POST /api/function takes a component path. Convex 1.46.0 implements this method and strips it +// from its declarations. +const byComponentPath = (client: ConvexHttpClient) => + refusalOf( + ( + client as unknown as { + function(name: string, componentPath: string, args: object): unknown; + } + ).function(target, component, {}) as Promise, + ); +interface World { + subject?: string; + backend?: Backend; + client?: ConvexHttpClient; + publicRouteError?: unknown; + componentRouteError?: unknown; + socketCall?: Promise<"answered" | "rejected">; + socketObservation?: { sends: number; backendCloses: number }; +} +bindExample(contract, (): World => ({}), { + "a disposable backend running the fixture composition, which mounts a component with a mutation that writes one row": + async (world) => { + world.backend = await fixtureBackend(); + }, + "an ordinary client carrying a token the harness signed for subject {subject}": + async (world, { subject }) => { + world.subject = subject; + const backend = required(world.backend, "the backend"); + world.client = ordinaryClient(backend.url, { + token: await backend.issuer.token(subject), + }); + }, + "the client calls the component's mutation by each of the three routes a client has": + async (world) => { + const client = required(world.client, "the client"); + world.publicRouteError = await byQualifiedName(client); + world.componentRouteError = await byComponentPath(client); + const backend = required(world.backend, "the backend"); + const observation = { sends: 0, backendCloses: 0 }; + world.socketObservation = observation; + class ObservedWebSocket extends globalThis.WebSocket { + private sentTarget = false; + private locallyClosed = false; + constructor(url: string | URL, protocols?: string | string[]) { + super(url, protocols); + this.addEventListener("close", () => { + if (this.sentTarget && !this.locallyClosed) + observation.backendCloses++; + }); + } + override send(data: Parameters[0]) { + super.send(data); + if (typeof data !== "string") return; + const message = JSON.parse(data) as { + type?: string; + udfPath?: string; + componentPath?: string; + args?: unknown[]; + }; + if ( + message.type === "Mutation" && + message.udfPath === target && + message.componentPath === component && + JSON.stringify(message.args) === "[{}]" + ) { + this.sentTarget = true; + observation.sends++; + } + } + override close(code?: number, reason?: string) { + this.locallyClosed = true; + super.close(code, reason); + } + } + const socket = ordinarySocketClient(backend.url, { + token: await backend.issuer.token( + required(world.subject, "the subject"), + ), + webSocket: ObservedWebSocket, + }); + onTestFinished(() => socket.close()); + // Convex 1.46.0 strips this internal protocol entry from its declarations. + world.socketCall = ( + socket.client as unknown as { + mutationInternal( + name: string, + args: object, + options: undefined, + componentPath: string, + ): Promise; + } + ) + .mutationInternal(target, {}, undefined, component) + .then( + () => "answered" as const, + () => "rejected" as const, + ); + }, + "the call to the public endpoint with a component-qualified name fails with an error whose text holds {publicRouteRefusal}": + async (world, { publicRouteRefusal }) => { + expect(world.publicRouteError).toBeInstanceOf(Error); + expect(String(world.publicRouteError)).toContain( + `${publicRouteRefusal} for '${component}/${target}'`, + ); + const noToken = ordinaryClient( + required(world.backend, "the backend").url, + ); + expect(String(await byQualifiedName(noToken))).toContain( + `${publicRouteRefusal} for '${component}/${target}'`, + ); + }, + "the call to the endpoint that takes a component path fails with an error whose text holds {componentRouteRefusal}": + async (world, { componentRouteRefusal }) => { + expect(world.componentRouteError).toBeInstanceOf(Error); + expect(String(world.componentRouteError)).toContain( + componentRouteRefusal, + ); + const noToken = ordinaryClient( + required(world.backend, "the backend").url, + ); + expect(String(await byComponentPath(noToken))).toContain( + componentRouteRefusal, + ); + }, + "the call over the WebSocket protocol with a component path is sent, gets no answer within {socketWaitMs} ms, and the backend closes the connection at least {leastSocketCloses} time": + async (world, { socketWaitMs, leastSocketCloses }) => { + const answer = await Promise.race([ + required(world.socketCall, "the socket call"), + sleep(socketWaitMs, "no answer"), + ]); + const observation = required( + world.socketObservation, + "the socket observations", + ); + measure("componentSocketSummary", { + ...observation, + answer, + socketWaitMs, + }); + expect(observation.sends).toBeGreaterThan(0); + expect(answer).toBe("no answer"); + expect(observation.backendCloses).toBeGreaterThanOrEqual( + leastSocketCloses, + ); + }, + "the table the component's mutation writes holds {rowsAfterRefusal} rows": + async (world, { rowsAfterRefusal }) => { + const { admin } = required(world.backend, "the backend"); + expect(await admin.readTable("notes", { component })).toHaveLength( + rowsAfterRefusal, + ); + }, + "the same mutation called with the harness's admin access writes {rowsAfterAdminCall} row": + async (world, { rowsAfterAdminCall }) => { + const { admin } = required(world.backend, "the backend"); + await admin.run(target, {}, { component }); + const rows = await admin.readTable("notes", { component }); + expect(rows).toHaveLength(rowsAfterAdminCall); + expect(rows[0]).toMatchObject({ source: "annex" }); + }, +}); diff --git a/tests/native/ordinary-client-refused-internal-function.test.ts b/tests/native/ordinary-client-refused-internal-function.test.ts new file mode 100644 index 0000000..a1be1ef --- /dev/null +++ b/tests/native/ordinary-client-refused-internal-function.test.ts @@ -0,0 +1,74 @@ +import type { ConvexHttpClient } from "convex/browser"; +import { getFunctionName, makeFunctionReference } from "convex/server"; +import { expect } from "vitest"; +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { bindExample } from "@libar-dev/software-delivery-protocol/vitest"; +import { internal } from "../../fixture/convex/_generated/api.js"; +import { ordinaryClientRefusedInternalFunctionContract as contract } from "../../generated/contracts/platform.native-harness.ordinary-client-refused-internal-function.contract.js"; +import type { Backend } from "../../harness/backend.js"; +import { ordinaryClient } from "../../harness/clients.js"; +import { fixtureBackend, required } from "../../harness/native.js"; +const anchor = specTest({ + id: testAnchorId( + "test:platform.native-harness.ordinary-client-refused-internal-function", + ), + verifies: ref( + "spec:platform.native-harness.ordinary-client-refused-internal-function", + ), +}); +void anchor; +// The refused call and the admin call name the same function. +const target = getFunctionName(internal.notes.addInternal); +const callByName = (client: ConvexHttpClient) => + client.mutation(makeFunctionReference<"mutation">(target), {}); +interface World { + backend?: Backend; + client?: ConvexHttpClient; + error?: unknown; +} +bindExample(contract, (): World => ({}), { + "a disposable backend running the fixture composition, which registers an internal mutation that writes one row": + async (world) => { + world.backend = await fixtureBackend(); + }, + "an ordinary client carrying a token the harness signed for subject {subject}": + async (world, { subject }) => { + const backend = required(world.backend, "the backend"); + world.client = ordinaryClient(backend.url, { + token: await backend.issuer.token(subject), + }); + }, + "the client calls the internal mutation by name": async (world) => { + world.error = await callByName(required(world.client, "the client")).then( + () => undefined, + (error: unknown) => error, + ); + }, + "the call fails with an error whose text holds {refusal}": async ( + world, + { refusal }, + ) => { + expect(world.error).toBeInstanceOf(Error); + expect(String(world.error)).toContain(refusal); + expect(String(world.error)).toContain(target); + const noToken = ordinaryClient(required(world.backend, "the backend").url); + await expect(callByName(noToken)).rejects.toThrow(refusal); + }, + "the table the internal mutation writes holds {rowsAfterRefusal} rows": + async (world, { rowsAfterRefusal }) => { + const { admin } = required(world.backend, "the backend"); + expect(await admin.readTable("notes")).toHaveLength(rowsAfterRefusal); + }, + "the same mutation called with the harness's admin access writes {rowsAfterAdminCall} row": + async (world, { rowsAfterAdminCall }) => { + const { admin } = required(world.backend, "the backend"); + await admin.run(target); + const rows = await admin.readTable("notes"); + expect(rows).toHaveLength(rowsAfterAdminCall); + expect(rows[0]).toMatchObject({ source: "internal" }); + }, +}); diff --git a/tests/native/read-cost.ts b/tests/native/read-cost.ts new file mode 100644 index 0000000..1a5c103 --- /dev/null +++ b/tests/native/read-cost.ts @@ -0,0 +1,143 @@ +import { expect } from "vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import { ordinaryClient } from "../../harness/clients.js"; +import type { Backend } from "../../harness/backend.js"; +import type { Id } from "../../fixture/convex/_generated/dataModel.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +export interface CostWorld { + backend?: Backend; + reads?: number; + seeds?: { own: Id<"samples">; annex: string }; + medians?: Record; + componentRecords?: number[]; +} +type Path = "helper" | "nested" | "component"; +const paths: Path[] = ["helper", "nested", "component"]; +const median = (values: number[]) => + required( + [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)], + "the median sample", + ); +export async function prepareCost(world: CostWorld, reads: number) { + const backend = await fixtureBackend(); + const seeds = await ordinaryClient(backend.url).mutation( + api.readCost.seed, + {}, + ); + Object.assign(world, { backend, seeds, reads }); +} +export async function sampleCost(world: CostWorld, kind: "mutation" | "query") { + const backend = required(world.backend, "the backend"); + const client = ordinaryClient(backend.url); + const samples: Record = { + helper: [], + nested: [], + component: [], + }; + const observations: Parameters[1][] = []; + world.componentRecords = []; + try { + for (let round = 0; round < 9; round++) { + for (const [index, path] of paths.entries()) { + const mark = await backend.admin.logMark(); + const args = { + ...required(world.seeds, "the seeded documents"), + path, + reads: required(world.reads, "the read count"), + cacheBuster: (round * 3 + index + 1) * 1000, + }; + let callError: unknown; + let result: { documentsRead: number; id: string } | undefined; + try { + if (kind === "mutation") + result = await client.mutation(api.readCost.viaMutation, args); + else result = await client.query(api.readCost.viaQuery, args); + } catch (error) { + callError = error; + } + const identifier = + kind === "mutation" ? "readCost:viaMutation" : "readCost:viaQuery"; + const records = await backend.admin.completionsSince(mark, (entries) => + entries.some( + (entry) => + entry.identifier === identifier && entry.componentPath === null, + ), + ); + const parent = required( + records.find( + (entry) => + entry.identifier === identifier && entry.componentPath === null, + ), + "the parent's completion", + ); + const sharingRequest = records.filter( + (entry) => entry.requestId === parent.requestId, + ); + observations.push({ + round, + path, + cacheBuster: args.cacheBuster, + result: result ?? null, + callError: callError === undefined ? null : String(callError), + executionTimeSeconds: parent.executionTime, + cachedResult: parent.cachedResult, + error: parent.error, + records: sharingRequest.map((entry) => ({ + identifier: entry.identifier, + componentPath: entry.componentPath, + executionTime: entry.executionTime, + cachedResult: entry.cachedResult, + })), + }); + expect(callError, String(callError)).toBeUndefined(); + expect(parent.error, JSON.stringify(parent)).toBeNull(); + expect(parent.cachedResult, JSON.stringify(parent)).toBe(false); + // The sample counts only when the backend's own count of documents read on the path, from + // the parent's transaction metrics, is the bound number. + expect(result, JSON.stringify(result)).toEqual({ + documentsRead: args.reads, + id: path === "component" ? args.annex : args.own, + }); + samples[path].push(parent.executionTime); + if (path === "component") + world.componentRecords.push(sharingRequest.length); + } + } + } finally { + measure("costSamples", observations); + } + world.medians = { + helper: median(samples.helper), + nested: median(samples.nested), + component: median(samples.component), + }; + measure("readCostSummary", { + kind, + readsPerSample: required(world.reads, "the read count"), + acceptedSamples: Object.fromEntries( + paths.map((path) => [path, samples[path].length]), + ), + componentCompletionRecords: world.componentRecords, + ...world.medians, + componentMinusHelperSeconds: world.medians.component - world.medians.helper, + componentMinusHelperPerReadSeconds: + (world.medians.component - world.medians.helper) / + required(world.reads, "the read count"), + }); +} +export function assertCost(world: CostWorld, expected: string) { + const medians = required(world.medians, "the medians"); + const observed = + medians.component > medians.helper + ? "higher" + : medians.component < medians.helper + ? "lower" + : "equal"; + expect(observed, JSON.stringify(medians)).toBe(expected); +} +export function assertRecords(world: CostWorld, records: number) { + for (const count of required(world.componentRecords, "the record counts")) + expect(count, `Observed ${count} records for a component parent call`).toBe( + records, + ); +} diff --git a/tests/native/shared-limits.ts b/tests/native/shared-limits.ts new file mode 100644 index 0000000..4935731 --- /dev/null +++ b/tests/native/shared-limits.ts @@ -0,0 +1,230 @@ +import { expect } from "vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import type { BlobRead, ReadThenCall } from "../../fixture/convex/limits.js"; +import type { Backend } from "../../harness/backend.js"; +import { ordinaryClient } from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +import { paceAfterWrite } from "../../harness/wait.js"; +export interface LimitsWorld { + backend?: Backend; + through?: "nested" | "annex"; + size?: number; + parentCount?: number; + childCount?: number; + together?: string; +} +function bytes(size: string) { + const match = /^(\d+) KiB$/.exec(size); + if (match === null) throw new Error(`Unsupported document size: ${size}`); + return Number(match[1]) * 1024; +} +async function outcome( + call: Promise, + success: string, + name: string, + limit?: "read" | "written", +) { + try { + await call; + measure(name, { outcome: success, error: null }); + return success; + } catch (error) { + const text = String(error); + measure(name, { outcome: "failed", error: text }); + return limit !== undefined && + text.includes(`Too many bytes ${limit} in a single function execution`) + ? `fails on the bytes-${limit} limit` + : `fails with: ${text}`; + } +} +export async function seedLimits( + world: LimitsWorld, + size: string, + parentCount: number, + childCount: number, + through: "nested" | "annex", +) { + const backend = await fixtureBackend(); + const payloadSize = bytes(size); + Object.assign(world, { + backend, + through, + size: payloadSize, + parentCount, + childCount, + }); + const client = ordinaryClient(backend.url); + await client.mutation(api.limits.insertBlobs, { + group: "parent", + count: parentCount, + size: payloadSize, + }); + await paceAfterWrite(parentCount * (payloadSize + 256)); + await client.mutation( + through === "nested" ? api.limits.insertBlobs : api.limits.insertAnnexBlobs, + { group: "child", count: childCount, size: payloadSize }, + ); + await paceAfterWrite(childCount * (payloadSize + 256)); + measure("seededPayloads", { size: payloadSize, parentCount, childCount }); +} +export async function readTogether(world: LimitsWorld) { + const client = ordinaryClient(required(world.backend, "the backend").url); + world.together = await outcome( + client.mutation(api.limits.readThenCall, { + through: required(world.through, "the boundary"), + parentCount: required(world.parentCount, "the parent count"), + childCount: required(world.childCount, "the child count"), + }), + "commits", + "combinedReadOutcome", + "read", + ); + measure("readTogether", world.together); +} +export function assertTogether(world: LimitsWorld, expected: string) { + expect(world.together, world.together).toBe(expected); +} +export async function readAlone( + world: LimitsWorld, + parentAlone: string, + childAlone: string, +) { + const client = ordinaryClient(required(world.backend, "the backend").url); + const through = required(world.through, "the boundary"); + const parentCount = required(world.parentCount, "the parent count"); + const childCount = required(world.childCount, "the child count"); + let parent: ReadThenCall | undefined; + let child: BlobRead | undefined; + const ownOutcome = await outcome( + client + .mutation(api.limits.readThenCall, { + through, + parentCount, + childCount: 0, + }) + .then((value) => { + parent = value; + }), + "commits", + "parentReadOutcome", + ); + const childOutcome = await outcome( + client + .query(api.limits.childAlone, { through, count: childCount }) + .then((value) => { + child = value; + }), + "returns", + "childReadOutcome", + ); + measure("readsAlone", { + parent: ownOutcome, + child: childOutcome, + parentDocuments: parent?.parentDocuments ?? null, + childDocuments: child?.documents ?? null, + childPayloadBytes: child?.payloadBytes ?? null, + }); + expect(ownOutcome, ownOutcome).toBe(parentAlone); + expect(childOutcome, childOutcome).toBe(childAlone); + expect(required(parent, "the parent read").parentDocuments).toBe(parentCount); + expect(required(child, "the child read").documents).toBe(childCount); + expect(required(child, "the child read").payloadBytes).toBe( + childCount * required(world.size, "the size"), + ); + const fitting: ReadThenCall = await client.mutation(api.limits.readThenCall, { + through, + parentCount: 1, + childCount: 1, + }); + const byteGrowth = + fitting.after.bytesRead.used - fitting.before.bytesRead.used; + const documentGrowth = + fitting.after.documentsRead.used - fitting.before.documentsRead.used; + const childBytes = + fitting.child.after.bytesRead.used - fitting.child.before.bytesRead.used; + const childDocuments = + fitting.child.after.documentsRead.used - + fitting.child.before.documentsRead.used; + measure("sharedReadMetrics", { + byteGrowth, + documentGrowth, + childBytes, + childDocuments, + payloadBytes: fitting.child.payloadBytes, + }); + expect(fitting.parentDocuments).toBe(1); + expect(fitting.child.documents).toBe(1); + expect(fitting.child.payloadBytes).toBe(required(world.size, "the size")); + expect(childBytes).toBeGreaterThanOrEqual(fitting.child.payloadBytes); + expect(childDocuments).toBe(1); + expect(byteGrowth).toBe(childBytes); + expect(documentGrowth).toBe(childDocuments); +} +async function writtenRows(world: LimitsWorld, group: string) { + const backend = required(world.backend, "the backend"); + // Each admin page reads at most ten payloads, safely below the read limit. + const own = await backend.admin.readTable("blobs", { pageSize: 10 }); + const child = + world.through === "annex" + ? await backend.admin.readTable("blobs", { + component: "annex", + pageSize: 10, + }) + : []; + return [...own, ...child].filter((row) => row.group === group).length; +} +export async function writeLimits(world: LimitsWorld, expected: string) { + const client = ordinaryClient(required(world.backend, "the backend").url); + const through = required(world.through, "the boundary"); + const parentCount = required(world.parentCount, "the parent count"); + const childCount = required(world.childCount, "the child count"); + const size = required(world.size, "the size"); + const together = await outcome( + client.mutation(api.limits.writeThenCall, { + through, + group: "failed-write", + parentCount, + childCount, + size, + }), + "commits", + "combinedWriteOutcome", + "written", + ); + measure("writesTogether", together); + // Pace even a failed attempt before testing the two committing halves. + await paceAfterWrite((parentCount + childCount) * (size + 256)); + const failedRows = await writtenRows(world, "failed-write"); + measure("failedWriteRows", failedRows); + expect(together, together).toBe(expected); + expect(failedRows).toBe(0); + for (const half of ["parent", "child"] as const) { + const committed = await outcome( + client.mutation(api.limits.writeThenCall, { + through, + group: "halves", + parentCount: half === "parent" ? parentCount : 0, + childCount: half === "child" ? childCount : 0, + size, + }), + "commits", + `${half}WriteOutcome`, + ); + await paceAfterWrite( + (half === "parent" ? parentCount : childCount) * (size + 256), + ); + const rows = await writtenRows(world, "halves"); + measure(`${half}WriteRows`, rows); + expect(committed, committed).toBe("commits"); + expect(rows).toBe( + half === "parent" ? parentCount : parentCount + childCount, + ); + } + measure("limitSummary", { + boundary: through, + combinedRead: world.together ?? null, + combinedWrite: together, + failedWriteRows: failedRows, + committedRows: await writtenRows(world, "halves"), + }); +} diff --git a/tests/native/thrown-data.ts b/tests/native/thrown-data.ts new file mode 100644 index 0000000..6781533 --- /dev/null +++ b/tests/native/thrown-data.ts @@ -0,0 +1,134 @@ +import { randomUUID } from "node:crypto"; +import { ConvexError, convexToJson } from "convex/values"; +import { expect } from "vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import type { Caught } from "../../fixture/convex/failures.js"; +import type { Backend } from "../../harness/backend.js"; +import { ordinaryClient } from "../../harness/clients.js"; +import { fixtureBackend, measure, required } from "../../harness/native.js"; +export interface ThrownWorld { + backend?: Backend; + boundary?: "nested" | "annex"; + data?: ReturnType; + caught?: Caught; + error?: unknown; + childRows?: number; + parentRows?: number; +} +export function thrownData(code: string) { + return { + code, + number: 17.25, + boolean: true, + nil: null, + array: ["one", 2, false, null], + nested: { label: "nested", count: 3 }, + int64: 9007199254740993n, + }; +} +export async function prepare( + world: ThrownWorld, + code: string, + boundary: "nested" | "annex", +) { + Object.assign(world, { + backend: await fixtureBackend(), + boundary, + data: thrownData(code), + }); +} +export function parents(world: ThrownWorld) { + expect(required(world.boundary, "the boundary")).toMatch(/^(nested|annex)$/); + expect(api.failures.catching).toBeDefined(); + expect(api.failures.passing).toBeDefined(); +} +export async function callParents(world: ThrownWorld) { + const backend = required(world.backend, "the backend"); + const client = ordinaryClient(backend.url); + const args = { + boundary: required(world.boundary, "the boundary"), + kind: "convexError" as const, + data: required(world.data, "the thrown data"), + }; + world.caught = await client.mutation(api.failures.catching, args); + measure("catchingParent", { + isConvexError: world.caught.isConvexError, + data: convexToJson(world.caught.data as ReturnType), + }); + world.childRows = ( + await backend.admin.readTable( + "throwerWrites", + args.boundary === "annex" ? { component: "annex" } : {}, + ) + ).length; + world.parentRows = (await backend.admin.readTable("parentWrites")).length; + measure("rowsAfterCatching", { + child: world.childRows, + parent: world.parentRows, + }); + world.error = await failureOf(client.mutation(api.failures.passing, args)); + measure("passingParent", { + isConvexError: world.error instanceof ConvexError, + data: + world.error instanceof ConvexError + ? convexToJson(world.error.data) + : null, + text: String(world.error), + }); +} +export const failureOf = (call: Promise): Promise => + call.then( + () => undefined, + (error: unknown) => error, + ); +export function assertCaught(world: ThrownWorld) { + const caught = required(world.caught, "the catching parent's answer"); + expect(caught.isConvexError, caught.message).toBe(true); + expect(caught.data, caught.message).toEqual(required(world.data, "the data")); +} +export function assertPassed(world: ThrownWorld) { + expect(world.error, String(world.error)).toBeInstanceOf(ConvexError); + expect( + (world.error as ConvexError>).data, + String(world.error), + ).toEqual(required(world.data, "the data")); +} +export async function assertRows( + world: ThrownWorld, + childRows: number, + parentRows: number, +) { + expect(world.childRows).toBe(childRows); + expect(world.parentRows).toBe(parentRows); + const client = ordinaryClient(required(world.backend, "the backend").url); + const args = { + boundary: required(world.boundary, "the boundary"), + kind: "plainError" as const, + data: `thrower-${world.boundary}-${randomUUID()}`, + }; + const caught = await client.mutation(api.failures.catching, args); + expect(caught.message).toContain(`plain failure: ${args.data}`); + measure("ordinaryErrorAtParent", { + property: caught.addedProperty, + message: caught.message, + }); + const error = await failureOf(client.mutation(api.failures.passing, args)); + expect(error, String(error)).toBeInstanceOf(Error); + expect(String(error)).toContain(`plain failure: ${args.data}`); + measure("ordinaryErrorAtClient", { + property: + (error as { addedProperty?: string } | undefined)?.addedProperty ?? null, + message: String(error), + }); + expect(caught.isConvexError, caught.message).toBe(false); + expect(caught.message).toContain("plain failure"); + measure("throwBoundarySummary", { + boundary: args.boundary, + caughtDataUnchanged: world.caught?.isConvexError === true, + passedDataUnchanged: world.error instanceof ConvexError, + childRows: world.childRows ?? null, + parentRows: world.parentRows ?? null, + plainErrorFromThrower: true, + writeGuardReached: true, + }); +} diff --git a/tests/pure/child.test.ts b/tests/pure/child.test.ts new file mode 100644 index 0000000..8aefe0c --- /dev/null +++ b/tests/pure/child.test.ts @@ -0,0 +1,309 @@ +import { ChildProcess } from "node:child_process"; +import { getEventListeners } from "node:events"; +import { existsSync } from "node:fs"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; +import { expect, onTestFinished, test, vi } from "vitest"; +import { redact, redactedBuffer, runChild } from "../../harness/child.js"; +const secret = "0123456789abcdef-instance-secret"; +test("pure: a failed child's error holds its exit code and its output, and neither its arguments nor a secret", async () => { + const error: unknown = await runChild( + "the child", + process.execPath, + [ + "-e", + "console.error('refused ' + process.argv[1]); process.exit(3)", + secret, + ], + { timeoutMs: 10000, secrets: [secret] }, + ).then( + () => undefined, + (thrown: unknown) => thrown, + ); + expect(error).toBeInstanceOf(Error); + const seen = JSON.stringify( + Object.getOwnPropertyNames(error).map((name) => [ + name, + String((error as Record)[name]), + ]), + ); + expect(seen).toContain("the child failed with exit code 3"); + expect(seen).toContain("refused [redacted]"); + expect(seen).not.toContain(secret); + expect(seen).not.toContain("process.exit"); +}); +test("pure: a child that outlives its deadline is killed and reported", async () => { + await expect( + runChild( + "the slow child", + process.execPath, + ["-e", "setInterval(() => {}, 1000)"], + { timeoutMs: 300 }, + ), + ).rejects.toThrow("the slow child did not finish in 300 ms"); +}); +test("pure: a child that cannot start is reported without its arguments", async () => { + await expect( + runChild("the missing child", "/no/such/executable", [secret], { + timeoutMs: 1000, + }), + ).rejects.toThrow("the missing child could not start: ENOENT"); +}); +test("pure: redact replaces every occurrence of every secret", () => { + expect(redact("a KEY b KEY c OTHER", ["KEY", "OTHER"])).toBe( + "a [redacted] b [redacted] c [redacted]", + ); +}); + +test("pure: redaction precedes the child error's truncation", async () => { + const key = "REVIEW_DUMMY_SECRET_0123456789abcdef"; + const error = await runChild( + "synthetic child", + process.execPath, + [ + "-e", + "process.stderr.write(process.argv[1] + 'x'.repeat(3990)); process.exit(1)", + key, + ], + { timeoutMs: 10000, secrets: [key] }, + ).catch((error: Error) => error); + expect(String(error)).not.toContain("6789abcdef"); + expect(String(error)).toContain("redacted]"); +}); + +test("pure: a failed child redacts a split stderr secret with stdout between its halves", async () => { + const key = "DUMMYSECRET0123456789abcdef"; + const error = await runChild( + "interleaved child", + process.execPath, + [ + "-e", + "process.stderr.write(process.argv[1].slice(0, 13), () => process.stdout.write('status ok\\n', () => process.stderr.write(process.argv[1].slice(13), () => process.exit(1))))", + key, + ], + { timeoutMs: 10000, secrets: [key] }, + ).catch((error: Error) => error); + expect(String(error)).toBe( + "Error: interleaved child failed with exit code 1: [redacted]", + ); +}); + +const overlappingSecrets = ["DUMMY", "DUMMYSECRET0123456789abcdef"]; +for (const secrets of [overlappingSecrets, [...overlappingSecrets].reverse()]) { + test(`pure: redaction chooses the longest secret with ${secrets[0]} configured first`, () => { + const text = `${overlappingSecrets[1]} DUMMY ${overlappingSecrets[1]}`; + expect(redact(text, [...secrets, ""])).toBe( + "[redacted] [redacted] [redacted]", + ); + const whole = redactedBuffer([...secrets, ""], 1000); + whole.append(text); + // A trailing delimiter resolves any prefix still held by the buffer. + whole.append("!"); + expect(whole.text()).toBe("[redacted] [redacted] [redacted]!"); + for (let split = 1; split < overlappingSecrets[1]!.length; split++) { + const buffer = redactedBuffer(secrets, 1000); + buffer.append(overlappingSecrets[1]!.slice(0, split)); + expect(buffer.text()).toBe(""); + buffer.append(overlappingSecrets[1]!.slice(split) + "!"); + expect(buffer.text()).toBe("[redacted]!"); + } + const shorter = redactedBuffer(secrets, 1000); + shorter.append("DUMMY"); + expect(shorter.text()).toBe(""); + shorter.append("!"); + expect(shorter.text()).toBe("[redacted]!"); + }); +} + +test("pure: aborting the caller's signal rejects with its reason and kills the child", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-child-abort-")); + const pidFile = join(directory, "pid"); + const controller = new AbortController(); + let pid: number | undefined; + onTestFinished(async () => { + controller.abort(); + if (pid !== undefined) { + try { + process.kill(pid, "SIGKILL"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; + } + } + await rm(directory, { recursive: true, force: true }); + }); + const result = runChild( + "the aborted child", + process.execPath, + [ + "-e", + "require('node:fs').writeFileSync(process.argv[1], String(process.pid)); setInterval(() => {}, 1000)", + pidFile, + ], + { timeoutMs: 10000, signal: controller.signal }, + ).then( + () => undefined, + (error: unknown) => error, + ); + const readyDeadline = Date.now() + 2000; + while (pid === undefined && Date.now() < readyDeadline) { + const contents = await readFile(pidFile, "utf8").catch(() => ""); + if (/^\d+$/.test(contents)) pid = Number(contents); + else await delay(10); + } + expect(pid).toBeDefined(); + const reason = new Error("caller stopped the child"); + controller.abort(reason); + const deadline = Symbol("abort deadline"); + expect(await Promise.race([result, delay(2000, deadline)])).toBe(reason); + // The abort callback can precede the operating system reaping the child. + let gone = false; + const exitDeadline = Date.now() + 2000; + while (!gone && Date.now() < exitDeadline) { + try { + process.kill(pid!, 0); + await delay(10); + } catch (error) { + expect((error as NodeJS.ErrnoException).code).toBe("ESRCH"); + gone = true; + } + } + expect(gone).toBe(true); +}); + +test("pure: aborting the caller's signal ends a child that handles SIGTERM, by SIGKILL", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-child-abort-term-")); + const pidFile = join(directory, "pid"); + const controller = new AbortController(); + let pid: number | undefined; + onTestFinished(async () => { + if (pid !== undefined) { + try { + process.kill(pid, "SIGKILL"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; + } + } + await rm(directory, { recursive: true, force: true }); + }); + const result = runChild( + "the child that handles SIGTERM", + process.execPath, + [ + "-e", + // Only SIGKILL ends it: it handles SIGTERM and SIGINT and keeps running. + "process.on('SIGTERM', () => {}); process.on('SIGINT', () => {}); require('node:fs').writeFileSync(process.argv[1], String(process.pid)); setInterval(() => {}, 1000)", + pidFile, + ], + { timeoutMs: 10000, signal: controller.signal }, + ).then( + () => undefined, + (error: unknown) => error, + ); + const readyDeadline = Date.now() + 2000; + while (pid === undefined && Date.now() < readyDeadline) { + const contents = await readFile(pidFile, "utf8").catch(() => ""); + if (/^\d+$/.test(contents)) pid = Number(contents); + else await delay(10); + } + expect(pid).toBeDefined(); + const reason = new Error("caller stopped the child"); + controller.abort(reason); + expect(await result).toBe(reason); + let gone = false; + const exitDeadline = Date.now() + 2000; + while (!gone && Date.now() < exitDeadline) { + try { + process.kill(pid!, 0); + await delay(10); + } catch (error) { + expect((error as NodeJS.ErrnoException).code).toBe("ESRCH"); + gone = true; + } + } + expect(gone).toBe(true); +}); + +test("pure: three configured secrets, one a prefix of another, are all removed", () => { + const secrets = ["SECRET-A", "SECRET-A-LONGER", "OTHER-SECRET-B"]; + const text = + "one SECRET-A-LONGER two SECRET-A three OTHER-SECRET-B four SECRET-A-LONGERx"; + const expected = + "one [redacted] two [redacted] three [redacted] four [redacted]x"; + for (const order of [secrets, [...secrets].reverse()]) { + expect(redact(text, order)).toBe(expected); + const buffer = redactedBuffer(order, 1000); + for (const character of text) buffer.append(character); + buffer.append("\n"); + expect(buffer.text()).toBe(`${expected}\n`); + } +}); + +test("pure: a signal already aborted at the call ends a child that would survive SIGTERM, by SIGKILL", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-child-pre-abort-")); + const readyFile = join(directory, "ready"); + const spawned = vi.spyOn( + // Node's declarations leave this internal method out. + ChildProcess.prototype as unknown as { spawn(options: object): void }, + "spawn", + ); + const started: { pid: number | undefined } = { pid: undefined }; + onTestFinished(async () => { + spawned.mockRestore(); + if (started.pid !== undefined) + try { + process.kill(started.pid, "SIGKILL"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; + } + await rm(directory, { recursive: true, force: true }); + }); + const controller = new AbortController(); + const reason = new Error("stopped before the call"); + controller.abort(reason); + const result = runChild( + "the child that handles SIGTERM", + process.execPath, + [ + "-e", + "process.on('SIGTERM', () => {}); require('node:fs').writeFileSync(process.argv[1], ''); setInterval(() => {}, 1000)", + readyFile, + ], + { timeoutMs: 10000, signal: controller.signal }, + ).then( + () => undefined, + (error: unknown) => error, + ); + const pid = (spawned.mock.contexts[0] as ChildProcess | undefined)?.pid; + started.pid = pid; + expect(pid).toBeDefined(); + // Node sends its SIGTERM on the next tick. Hold this thread until the child handles SIGTERM, so + // only an immediate SIGKILL can end it; a child killed at once never gets ready. + const wait = new Int32Array(new SharedArrayBuffer(4)); + const readyDeadline = Date.now() + 1500; + while (!existsSync(readyFile) && Date.now() < readyDeadline) + Atomics.wait(wait, 0, 0, 10); + expect(await result).toBe(reason); + let gone = false; + const exitDeadline = Date.now() + 2000; + while (!gone && Date.now() < exitDeadline) { + try { + process.kill(pid!, 0); + await delay(10); + } catch (error) { + expect((error as NodeJS.ErrnoException).code).toBe("ESRCH"); + gone = true; + } + } + expect(gone).toBe(true); +}); + +test("pure: runChild removes its abort listener when the child exits", async () => { + const controller = new AbortController(); + await runChild("the quick child", process.execPath, ["-e", ""], { + timeoutMs: 10000, + signal: controller.signal, + }); + expect(getEventListeners(controller.signal, "abort")).toEqual([]); +}); diff --git a/tests/pure/clients.test.ts b/tests/pure/clients.test.ts new file mode 100644 index 0000000..c3cef4c --- /dev/null +++ b/tests/pure/clients.test.ts @@ -0,0 +1,91 @@ +import { afterEach, expect, test, vi } from "vitest"; +import { countingFetch, heldWebSocket } from "../../harness/clients.js"; +class FakeSocket { + static made: FakeSocket[] = []; + onopen: WebSocket["onopen"] = null; + onclose: WebSocket["onclose"] = null; + onerror: WebSocket["onerror"] = null; + onmessage: WebSocket["onmessage"] = null; + sent: string[] = []; + url: string; + constructor(url: string) { + this.url = url; + FakeSocket.made.push(this); + } + send(data: string) { + this.sent.push(data); + } + close() { + this.onclose?.call(this as unknown as WebSocket, {} as CloseEvent); + } + receive(data: string) { + this.onmessage?.call( + this as unknown as WebSocket, + { data } as MessageEvent, + ); + } +} +afterEach(() => { + vi.unstubAllGlobals(); + FakeSocket.made = []; +}); +function listen(socket: WebSocket): string[] { + const received: string[] = []; + socket.onmessage = (event) => received.push(event.data as string); + return received; +} +test("pure: a held socket sends requests at once and delivers held responses on release", () => { + vi.stubGlobal("WebSocket", FakeSocket); + const held = heldWebSocket(); + const socket = new held.WebSocket("ws://backend"); + const received = listen(socket); + held.holdResponses(); + socket.send("a mutation"); + FakeSocket.made[0]?.receive("its result"); + expect(FakeSocket.made[0]?.sent).toEqual(["a mutation"]); + expect(received).toEqual([]); + held.release(); + expect(received).toEqual(["its result"]); +}); +test("pure: a held socket that closes drops its held responses, and a paused socket connects on release", () => { + vi.stubGlobal("WebSocket", FakeSocket); + const held = heldWebSocket(); + const received = listen(new held.WebSocket("ws://backend")); + held.holdResponses(); + FakeSocket.made[0]?.receive("a result nobody will see"); + FakeSocket.made[0]?.close(); + held.pauseConnections(); + const second = new held.WebSocket("ws://backend"); + const replayed = listen(second); + expect(FakeSocket.made).toHaveLength(1); + expect(() => second.send("too early")).toThrow("The socket is not connected"); + held.release(); + expect(FakeSocket.made).toHaveLength(2); + expect(received).toEqual([]); + FakeSocket.made[1]?.receive("the replayed result"); + expect(replayed).toEqual(["the replayed result"]); +}); +function answering(body: string) { + const requested: string[] = []; + vi.stubGlobal("fetch", async (input: string) => { + requested.push(input); + return new Response(body); + }); + return requested; +} +test("pure: a counting fetch that loses responses delivers the request, then throws", async () => { + const requested = answering("done"); + const lossy = countingFetch({ loseResponses: true }); + await expect(lossy.fetch("http://backend/api/mutation")).rejects.toThrow( + "The response was lost", + ); + expect(lossy.requests()).toBe(1); + expect(requested).toEqual(["http://backend/api/mutation"]); +}); +test("pure: a counting fetch that keeps responses returns them and counts", async () => { + answering("done"); + const kept = countingFetch({ loseResponses: false }); + expect(await (await kept.fetch("http://backend/1")).text()).toBe("done"); + expect(await (await kept.fetch("http://backend/2")).text()).toBe("done"); + expect(kept.requests()).toBe(2); +}); diff --git a/tests/pure/convex-cli.test.ts b/tests/pure/convex-cli.test.ts new file mode 100644 index 0000000..5143192 --- /dev/null +++ b/tests/pure/convex-cli.test.ts @@ -0,0 +1,86 @@ +import { ChildProcess } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { afterEach, expect, test, vi } from "vitest"; +import { convexCli } from "../../harness/admin.js"; +import { fixtureComposition } from "../../harness/backend.js"; +import { runChild } from "../../harness/child.js"; +const target = { + url: "http://127.0.0.1:1", + adminKey: "the-admin-key", + home: "/the/backend/home", +}; +afterEach(() => vi.unstubAllEnvs()); +test.each(["deploy", "codegen", "dev"] as const)( + "pure: convex %s names the backend by --url and --admin-key and gets an environment of PATH, HOME and TMPDIR only", + (command) => { + vi.stubEnv("CONVEX_DEPLOY_KEY", "prod:somewhere|a-key"); + vi.stubEnv("CONVEX_DEPLOYMENT", "dev:somewhere"); + vi.stubEnv("CONVEX_SELF_HOSTED_URL", "http://127.0.0.1:2"); + const call = convexCli(target, command); + expect(call.args[1]).toBe(command); + expect(call.args.slice(-4)).toEqual([ + "--url", + target.url, + "--admin-key", + target.adminKey, + ]); + expect(Object.keys(call.env).sort()).toEqual(["HOME", "PATH", "TMPDIR"]); + expect(call.env.HOME).toBe(target.home); + }, +); +test.each(["deploy", "codegen", "dev"] as const)( + "pure: convex %s runs with the typecheck disabled", + (command) => { + const { args } = convexCli(target, command); + expect(args[args.indexOf("--typecheck") + 1]).toBe("disable"); + }, +); +test("pure: convex dev keeps codegen on, so a changed fixture file rewrites the generated files", () => { + const { args } = convexCli(target, "dev"); + expect(args).not.toContain("--codegen"); +}); +test("pure: convex dev keeps watching: it carries neither --once nor --until-success", () => { + const { args } = convexCli(target, "dev"); + expect(args).not.toContain("--once"); + expect(args).not.toContain("--until-success"); +}); +test("pure: deploy is limited to 60000 ms, codegen to 120000 ms, and dev runs until it is stopped", () => { + expect(convexCli(target, "deploy").timeoutMs).toBe(60000); + expect(convexCli(target, "codegen").timeoutMs).toBe(120000); + expect(convexCli(target, "dev").timeoutMs).toBe(Number.POSITIVE_INFINITY); +}); +test("pure: runChild refuses the long-lived dev call before it starts a process", async () => { + // Every child_process function starts its process through ChildProcess.prototype.spawn. + const spawned = vi.spyOn( + // Node's declarations leave this internal method out. + ChildProcess.prototype as unknown as { spawn(options: object): void }, + "spawn", + ); + try { + // The spy sees a process that runChild starts. + await runChild("node", process.execPath, ["-e", ""], { timeoutMs: 10000 }); + expect(spawned).toHaveBeenCalledTimes(1); + spawned.mockClear(); + const call = convexCli(target, "dev"); + // Without this guard a finite limit would let runChild start convex dev. + expect(call.timeoutMs).toBe(Number.POSITIVE_INFINITY); + await expect( + runChild("convex dev", call.file, call.args, { + cwd: call.cwd, + env: call.env, + timeoutMs: call.timeoutMs, + }), + ).rejects.toMatchObject({ code: "ERR_OUT_OF_RANGE" }); + expect(spawned).not.toHaveBeenCalled(); + } finally { + spawned.mockRestore(); + } +}); +test("pure: the CLI runs in the repository root, where convex.json names the fixture composition's functions", async () => { + const { cwd } = convexCli(target, "deploy"); + const config = JSON.parse( + await readFile(join(cwd, "convex.json"), "utf8"), + ) as { functions: string }; + expect(config.functions).toBe(`${fixtureComposition.functions}/`); +}); diff --git a/tests/pure/evidence.test.ts b/tests/pure/evidence.test.ts new file mode 100644 index 0000000..c774212 --- /dev/null +++ b/tests/pure/evidence.test.ts @@ -0,0 +1,93 @@ +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test, vi } from "vitest"; +vi.mock("node:crypto", async (original) => ({ + ...(await original()), + randomUUID: vi.fn(), +})); +vi.mock("node:fs", async (original) => { + const fs = await original(); + return { ...fs, writeFileSync: vi.fn(fs.writeFileSync) }; +}); +import { randomUUID } from "node:crypto"; +import { writeFileSync } from "node:fs"; +import { + failRunRecord, + openRunRecord, + writeRunRecord, +} from "../../harness/evidence.js"; +import type { NativeRunRecord } from "../../harness/evidence.js"; + +test("pure: simultaneous evidence records differ and an existing record cannot be overwritten", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-evidence-")); + const record: NativeRunRecord = { + tier: "native", + commit: "abcdefg", + clean: false, + command: "vitest run", + startedAt: "2026-10-01T16:29:36.247Z", + finishedAt: "", + result: "passed", + versions: {}, + tests: [], + unhandledErrors: [], + }; + try { + vi.mocked(randomUUID) + .mockReturnValueOnce("00000000-0000-0000-0000-000000000001") + .mockReturnValueOnce("00000000-0000-0000-0000-000000000002") + .mockReturnValueOnce("00000000-0000-0000-0000-000000000001"); + const first = await writeRunRecord(directory, record); + const second = await writeRunRecord(directory, record); + expect(first).not.toBe(second); + await expect( + writeRunRecord(directory, { ...record, result: "failed" }), + ).rejects.toThrow(); + expect( + JSON.parse(await readFile(join(directory, first), "utf8")).result, + ).toBe("passed"); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); + +test("pure: an amendment that fails leaves the old record whole", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-evidence-amend-")); + const record: NativeRunRecord = { + tier: "native", + commit: "abcdefg", + clean: true, + command: "vitest run --project native", + startedAt: "2026-10-01T16:29:36.247Z", + finishedAt: "2026-10-01T16:30:36.247Z", + result: "passed", + versions: {}, + tests: [], + unhandledErrors: [], + }; + const actual = await vi.importActual("node:fs"); + try { + vi.mocked(randomUUID).mockReturnValueOnce( + "00000000-0000-0000-0000-000000000003", + ); + const path = join(directory, await writeRunRecord(directory, record)); + const original = await readFile(path, "utf8"); + const run = openRunRecord(); + run.path = path; + // The write opens its file with truncation and then fails, as a full disk or a file size + // limit makes it. + vi.mocked(writeFileSync).mockImplementationOnce((file) => { + actual.writeFileSync(file, ""); + throw Object.assign(new Error("EFBIG: file too large"), { + code: "EFBIG", + }); + }); + expect(() => failRunRecord(run, "the sweep kept a backend")).toThrow( + "EFBIG", + ); + expect(await readFile(path, "utf8")).toBe(original); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/tests/pure/executable-signal.test.ts b/tests/pure/executable-signal.test.ts new file mode 100644 index 0000000..2e6e8f5 --- /dev/null +++ b/tests/pure/executable-signal.test.ts @@ -0,0 +1,212 @@ +import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import ts from "typescript"; +import { afterEach, expect, onTestFinished, test, vi } from "vitest"; +vi.mock("node:fs/promises", async (original) => { + const fs = await original(); + return { ...fs, mkdtemp: vi.fn(fs.mkdtemp) }; +}); +import { resolveExecutable } from "../../harness/executable.js"; +interface ReleasePin { + release: string; + assets: Record; +} +const pin = JSON.parse( + await readFile( + join(import.meta.dirname, "../../harness/backend-release.json"), + "utf8", + ), +) as ReleasePin; +const asset = pin.assets[`${process.platform}-${process.arch}`]; +async function scratch(): Promise { + const directory = await mkdtemp(join(tmpdir(), "libar-executable-signal-")); + onTestFinished(() => rm(directory, { recursive: true, force: true })); + return directory; +} +afterEach(() => vi.unstubAllEnvs()); + +// A download that never answers, as a stalled network does. +function stalled(phase: "headers" | "body") { + const seen: { signal: AbortSignal | undefined; started: () => void } = { + signal: undefined, + started: () => undefined, + }; + const started = new Promise((done) => (seen.started = done)); + const fetch: typeof globalThis.fetch = async (_input, init) => { + seen.signal = init?.signal ?? undefined; + if (phase === "headers") { + seen.started(); + return new Promise(() => undefined); + } + // Headers arrive. The body is first read after that and never ends; it ignores the fetch's + // signal, as a body handed over by another fetch implementation may. + return new Response( + new ReadableStream( + { + pull: () => { + seen.started(); + return new Promise(() => undefined); + }, + }, + { highWaterMark: 0 }, + ), + ); + }; + return { fetch, started, seen }; +} + +test.skipIf(asset === undefined).each(["headers", "body"] as const)( + "pure: an interrupt while the download waits for its %s stops it at once and removes the download's work directory", + async (phase) => { + vi.stubEnv("CONVEX_BACKEND_BINARY", ""); + const cacheRoot = await scratch(); + const download = stalled(phase); + const controller = new AbortController(); + const interrupt = new Error("Interrupted by SIGINT"); + const resolving = resolveExecutable({ + cacheRoot, + fetch: download.fetch, + signal: controller.signal, + }); + await download.started; + controller.abort(interrupt); + const started = Date.now(); + await expect(resolving).rejects.toBe(interrupt); + expect(Date.now() - started).toBeLessThan(1000); + // The download itself was told to stop, not only abandoned. + expect(download.seen.signal?.aborted).toBe(true); + expect( + await readdir(join(cacheRoot, pin.release, asset?.file ?? "")), + ).toEqual([]); + }, +); + +test.skipIf(asset === undefined)( + "pure: an interrupt before resolution starts no download", + async () => { + vi.stubEnv("CONVEX_BACKEND_BINARY", ""); + const cacheRoot = await scratch(); + const fetch = vi.fn(); + const controller = new AbortController(); + const interrupt = new Error("Interrupted by SIGTERM"); + controller.abort(interrupt); + await expect( + resolveExecutable({ cacheRoot, fetch, signal: controller.signal }), + ).rejects.toBe(interrupt); + expect(fetch).not.toHaveBeenCalled(); + }, +); + +// No pure test can make the scripts download cold, so this reads their source. It parses it, so a +// comment does not count, and compares each call to resolveExecutable, and in dev the step that +// makes it, as printed without comments. +async function resolutionCalls(name: string): Promise { + const file = join(import.meta.dirname, "../../scripts", name); + const source = ts.createSourceFile( + file, + await readFile(file, "utf8"), + ts.ScriptTarget.Latest, + true, + ); + const printer = ts.createPrinter({ removeComments: true }); + const found: string[] = []; + const visit = (node: ts.Node) => { + if ( + (ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === "resolveExecutable") || + (ts.isPropertyAssignment(node) && + ts.isIdentifier(node.name) && + node.name.text === "resolveExecutable") + ) + found.push(printer.printNode(ts.EmitHint.Unspecified, node, source)); + ts.forEachChild(node, visit); + }; + visit(source); + return found; +} +test("pure: reading their source, codegen and dev hand their interrupt to executable resolution", async () => { + expect(await resolutionCalls("codegen.mjs")).toEqual([ + "resolveExecutable({ signal: controller.signal })", + ]); + expect(await resolutionCalls("dev.mjs")).toEqual([ + "resolveExecutable: (signal) => resolveExecutable({ signal })", + "resolveExecutable({ signal })", + ]); +}); + +// Collects the unhandled rejections of one test, after letting the event loop turn. +async function unhandledDuring(run: () => Promise): Promise { + const unhandled: unknown[] = []; + const keep = (reason: unknown) => unhandled.push(reason); + process.on("unhandledRejection", keep); + try { + await run(); + await new Promise((done) => setTimeout(done, 50)); + } finally { + process.off("unhandledRejection", keep); + } + return unhandled; +} +// Like fetch, it rejects with the reason of a signal that is already aborted. +const fetchLike: typeof globalThis.fetch = async (_input, init) => { + init?.signal?.throwIfAborted(); + return new Promise(() => undefined); +}; + +test.skipIf(asset === undefined)( + "pure: an interrupt while the download directory is made ends in the interrupt, the directory removed and no unhandled rejection", + async () => { + vi.stubEnv("CONVEX_BACKEND_BINARY", ""); + const cacheRoot = await scratch(); + const controller = new AbortController(); + const interrupt = new Error("Interrupted by SIGINT"); + const actual = ( + await vi.importActual( + "node:fs/promises", + ) + ).mkdtemp; + vi.mocked(mkdtemp).mockImplementationOnce(async (prefix) => { + const made = await actual(prefix as string); + controller.abort(interrupt); + return made; + }); + const unhandled = await unhandledDuring(async () => { + await expect( + resolveExecutable({ + cacheRoot, + fetch: fetchLike, + signal: controller.signal, + }), + ).rejects.toBe(interrupt); + }); + expect(unhandled).toEqual([]); + expect( + await readdir(join(cacheRoot, pin.release, asset?.file ?? "")), + ).toEqual([]); + }, +); + +test.skipIf(asset === undefined)( + "pure: an interrupt as the download starts ends in the interrupt with the download's own rejection handled", + async () => { + vi.stubEnv("CONVEX_BACKEND_BINARY", ""); + const cacheRoot = await scratch(); + const controller = new AbortController(); + const interrupt = new Error("Interrupted by SIGTERM"); + const fetch: typeof globalThis.fetch = (input, init) => { + controller.abort(interrupt); + return fetchLike(input, init); + }; + const unhandled = await unhandledDuring(async () => { + await expect( + resolveExecutable({ cacheRoot, fetch, signal: controller.signal }), + ).rejects.toBe(interrupt); + }); + expect(unhandled).toEqual([]); + expect( + await readdir(join(cacheRoot, pin.release, asset?.file ?? "")), + ).toEqual([]); + }, +); diff --git a/tests/pure/executable.test.ts b/tests/pure/executable.test.ts new file mode 100644 index 0000000..f8daeeb --- /dev/null +++ b/tests/pure/executable.test.ts @@ -0,0 +1,115 @@ +import { createHash } from "node:crypto"; +import { + mkdir, + mkdtemp, + readFile, + readdir, + rm, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, onTestFinished, test, vi } from "vitest"; +import { resolveExecutable, sha256OfFile } from "../../harness/executable.js"; +interface ReleasePin { + release: string; + assets: Record< + string, + { file: string; zipSha256: string; executableSha256: string } + >; +} +async function scratch(): Promise { + const directory = await mkdtemp(join(tmpdir(), "libar-executable-")); + onTestFinished(() => rm(directory, { recursive: true, force: true })); + return directory; +} +const pin = JSON.parse( + await readFile( + join(import.meta.dirname, "../../harness/backend-release.json"), + "utf8", + ), +) as ReleasePin; +const asset = pin.assets[`${process.platform}-${process.arch}`]; +afterEach(() => vi.unstubAllEnvs()); +test("pure: an executable named by CONVEX_BACKEND_BINARY whose hash is not pinned is recorded by its hash and with no release", async () => { + const path = join(await scratch(), "some-backend"); + await writeFile(path, "not the pinned executable"); + vi.stubEnv("CONVEX_BACKEND_BINARY", path); + expect(await resolveExecutable()).toEqual({ + path, + sha256: await sha256OfFile(path), + release: null, + source: "CONVEX_BACKEND_BINARY", + }); +}); +test("pure: CONVEX_BACKEND_BINARY that names no file fails", async () => { + vi.stubEnv("CONVEX_BACKEND_BINARY", "/no/such/backend"); + await expect(resolveExecutable()).rejects.toThrow( + "CONVEX_BACKEND_BINARY names no readable file: /no/such/backend", + ); +}); +test.skipIf(asset === undefined)( + "pure: a cache entry whose hash is not the pinned one survives a failed repair and a concurrent publication", + async () => { + vi.stubEnv("CONVEX_BACKEND_BINARY", ""); + const cacheRoot = await scratch(); + const directory = join(cacheRoot, pin.release, asset?.file ?? ""); + const cached = join(directory, "convex-local-backend"); + await mkdir(directory, { recursive: true }); + await writeFile(cached, "a damaged executable"); + const requested: string[] = []; + const refuse: typeof fetch = async (input) => { + requested.push(String(input)); + expect(await readFile(cached, "utf8")).toBe("a damaged executable"); + await writeFile(cached, "another run published this executable"); + return new Response("", { status: 404 }); + }; + await expect( + resolveExecutable({ cacheRoot, fetch: refuse }), + ).rejects.toThrow("Backend download failed with status 404"); + expect(requested).toEqual([ + `https://github.com/get-convex/convex-backend/releases/download/${pin.release}/${asset?.file}`, + ]); + expect(await readFile(cached, "utf8")).toBe( + "another run published this executable", + ); + }, +); + +test.skipIf(asset === undefined)( + "pure: a downloaded archive with an unpinned hash fails before publishing an executable", + async () => { + vi.stubEnv("CONVEX_BACKEND_BINARY", ""); + const cacheRoot = await scratch(); + const directory = join(cacheRoot, pin.release, asset?.file ?? ""); + const bytes = Buffer.from("synthetic archive with the wrong hash"); + const hash = createHash("sha256").update(bytes).digest("hex"); + expect(hash).not.toBe(asset?.zipSha256); + const requested: string[] = []; + const download: typeof fetch = async (input) => { + requested.push(String(input)); + return new Response(bytes, { status: 200 }); + }; + const outcome = await resolveExecutable({ + cacheRoot, + fetch: download, + }).then( + (executable) => ({ executable, error: undefined }), + (error: unknown) => ({ executable: undefined, error }), + ); + expect(outcome.error).toBeInstanceOf(Error); + expect(String(outcome.error)).toBe( + `Error: Backend download ${asset?.file} has SHA-256 ${hash}; harness/backend-release.json pins ${asset?.zipSha256}`, + ); + expect(outcome.executable).toBeUndefined(); + expect(requested).toEqual([ + `https://github.com/get-convex/convex-backend/releases/download/${pin.release}/${asset?.file}`, + ]); + expect(await readdir(directory)).toEqual([]); + await expect( + readFile(join(directory, "convex-local-backend")), + ).rejects.toMatchObject({ + code: "ENOENT", + }); + }, +); diff --git a/tests/pure/fixture-issuer.test.ts b/tests/pure/fixture-issuer.test.ts new file mode 100644 index 0000000..ebcd668 --- /dev/null +++ b/tests/pure/fixture-issuer.test.ts @@ -0,0 +1,40 @@ +import { createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose"; +import { expect, test } from "vitest"; +import { + createFixtureIssuer, + fixtureApplicationId, + fixtureIssuerUrl, + fixtureKeyId, +} from "../../harness/identity.js"; +test("pure: a fixture issuer's token verifies against its data-URI ES256 key set, and another issuer's token does not", async () => { + const issuer = await createFixtureIssuer(); + expect(issuer.jwks).toMatch(/^data:text\/plain;charset=utf-8;base64,/); + const keys = createLocalJWKSet( + JSON.parse( + Buffer.from(issuer.jwks.split(",")[1] ?? "", "base64").toString(), + ) as Parameters[0], + ); + const token = await issuer.token("user-1"); + const { payload } = await jwtVerify(token, keys, { + issuer: fixtureIssuerUrl, + audience: fixtureApplicationId, + }); + expect(payload.sub).toBe("user-1"); + expect((payload.exp ?? 0) - (payload.iat ?? 0)).toBe(600); + expect(decodeProtectedHeader(token)).toEqual({ + alg: "ES256", + kid: fixtureKeyId, + typ: "JWT", + }); + const outsider = await createFixtureIssuer(); + await expect( + jwtVerify(await outsider.token("user-1"), keys), + ).rejects.toThrow(); +}); +test("pure: a fixture issuer signs for the issuer URL it was made with", async () => { + const issuer = await createFixtureIssuer("https://another-issuer.test"); + const token = await issuer.token("user-1"); + expect( + JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString()), + ).toMatchObject({ iss: "https://another-issuer.test", aud: "fixture-app" }); +}); diff --git a/tests/pure/harness-rules.test.ts b/tests/pure/harness-rules.test.ts new file mode 100644 index 0000000..f1baa68 --- /dev/null +++ b/tests/pure/harness-rules.test.ts @@ -0,0 +1,149 @@ +import { + ref, + specTest, + testAnchorId, +} from "@libar-dev/software-delivery-protocol"; +import { readdir, readFile } from "node:fs/promises"; +import { setTimeout as sleep } from "node:timers/promises"; +import { join } from "node:path"; +import { expect, test, vi } from "vitest"; +vi.mock("node:timers/promises", async (original) => ({ + ...(await original()), + setTimeout: vi.fn(async () => undefined), +})); +import { redactedBuffer } from "../../harness/child.js"; +import { invocation } from "../../harness/evidence.js"; +import { createAdminAccess } from "../../harness/admin.js"; +import { + localWriteRateBytesPerSecond, + paceAfterWrite, +} from "../../harness/local-write-rate.js"; +import * as wait from "../../harness/wait.js"; +// Binds this file to the harness Spec, whose rules it checks directly. +const anchor = specTest({ + id: testAnchorId("test:platform.native-harness.rules"), + verifies: ref("spec:platform.native-harness"), +}); +void anchor; + +test("pure: native sources read adminKey only in the acting-as demonstration", async () => { + const directory = join(import.meta.dirname, "../native"); + const violations: string[] = []; + async function inspect(relative = ""): Promise { + for (const entry of await readdir(join(directory, relative), { + withFileTypes: true, + })) { + const file = join(relative, entry.name); + if (entry.isDirectory()) { + await inspect(file); + continue; + } + if ( + !file.endsWith(".ts") || + file === + "admin-key-acting-as-identity-reaches-internal-function.test.ts" + ) + continue; + if (/\badminKey\b/.test(await readFile(join(directory, file), "utf8"))) + violations.push(file); + } + } + await inspect(); + expect(violations).toEqual([]); +}); + +test("pure: a rolling buffer redacts a secret split at every chunk boundary before clipping", () => { + const key = "REVIEW_DUMMY_SECRET_0123456789abcdef"; + for (let split = 1; split < key.length; split++) { + const buffer = redactedBuffer([key], 4000); + buffer.append(key.slice(0, split)); + expect(buffer.text()).toBe(""); + buffer.append(key.slice(split) + "x".repeat(3990)); + expect(buffer.text()).toContain("redacted]"); + expect(buffer.text()).not.toContain("6789abcdef"); + } +}); + +test("pure: the backend buffer redacts secrets across interleaved stdout, stderr and errors", () => { + const key = "DUMMYSECRET0123456789abcdef"; + const reproduction = redactedBuffer([key], 16000); + reproduction.append("DUMMYSECRET01", "stdout"); + reproduction.append("status ok\n", "stderr"); + expect(reproduction.text()).toBe("status ok\n"); + reproduction.append("23456789abcdef", "stdout"); + expect(reproduction.text()).toBe("status ok\n[redacted]"); + + // All six orders that preserve the order within each two-chunk stream. + const orders = ["ooss", "osos", "osso", "soos", "soso", "ssoo"]; + for (let split = 1; split < key.length; split++) { + for (const order of orders) { + for (let errorAt = 0; errorAt <= order.length; errorAt++) { + const buffer = redactedBuffer([key], 16000); + const offsets = { stdout: 0, stderr: 0 }; + for (let index = 0; index <= order.length; index++) { + if (index === errorAt) buffer.append("status ok\n", "error"); + if (index === order.length) break; + const stream = order[index] === "o" ? "stdout" : "stderr"; + const chunk = + offsets[stream]++ === 0 ? key.slice(0, split) : key.slice(split); + buffer.append(chunk, stream); + expect(buffer.text().replaceAll("[redacted]", "")).toBe( + index >= errorAt ? "status ok\n" : "", + ); + } + expect(buffer.text().match(/\[redacted\]/g)).toHaveLength(2); + expect(buffer.text().replaceAll("[redacted]", "")).toBe("status ok\n"); + } + } + } +}); + +test("pure: the effective vitest invocation keeps forwarded arguments under npm", () => { + vi.stubEnv("npm_command", "run-script"); + vi.stubEnv("npm_lifecycle_event", "test:native"); + try { + expect( + invocation([ + "run", + "--project", + "native", + "--maxWorkers=1", + "-t", + "name with spaces", + ]), + ).toBe("vitest run --project native --maxWorkers=1 -t 'name with spaces'"); + } finally { + vi.unstubAllEnvs(); + } +}); + +test("pure: function log marks reject a changed process even when the predicate already matches", async () => { + const state = { + deployed: false, + environment: new Set(), + logProcess: {}, + }; + const admin = createAdminAccess( + { + url: "http://127.0.0.1:1", + adminKey: "dummy", + home: "unused", + secrets: [], + }, + state, + ); + const mark = await admin.logMark(); + expect(await admin.completionsSince(mark, () => true)).toEqual([]); + state.logProcess = {}; + await expect(admin.completionsSince(mark, () => true)).rejects.toThrow( + "another backend process", + ); +}); + +test("pure: local write pacing is exported by its named module and remains compatible", async () => { + expect(localWriteRateBytesPerSecond).toBe(4 * 1024 * 1024); + expect(wait.paceAfterWrite).toBe(paceAfterWrite); + expect(wait.localWriteRateBytesPerSecond).toBe(localWriteRateBytesPerSecond); + await paceAfterWrite(localWriteRateBytesPerSecond + 1); + expect(sleep).toHaveBeenLastCalledWith(1251); +}); diff --git a/tests/pure/native-run.test.ts b/tests/pure/native-run.test.ts new file mode 100644 index 0000000..38f289c --- /dev/null +++ b/tests/pure/native-run.test.ts @@ -0,0 +1,468 @@ +import { execFileSync, spawn } from "node:child_process"; +import { + mkdir, + mkdtemp, + readFile, + readdir, + rm, + stat, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { randomUUID } from "node:crypto"; +import { expect, test, vi } from "vitest"; +vi.mock("node:child_process", async (original) => ({ + ...(await original()), + execFileSync: vi.fn(), +})); +import { finalSweep, sweep } from "../../harness/native-run.js"; +import EvidenceReporter, { + failRunRecord, + openRunRecord, +} from "../../harness/evidence.js"; +import type { NativeRunRecord } from "../../harness/evidence.js"; +import { publishOwnership } from "../../harness/ownership.js"; + +test("pure: sweep skips missing and stale ownership and kills multiple owned processes", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-sweep-")); + const children = []; + const exits = []; + const commands = new Map(); + vi.mocked(execFileSync).mockImplementation((_file, args) => { + const pid = Number((args as string[])[2]); + if (!commands.has(pid)) throw new Error("process is gone"); + return commands.get(pid)!; + }); + try { + for (const name of ["one", "nested/two"]) { + const storage = join(directory, name); + await mkdir(storage, { recursive: true }); + const instance = `libar-${randomUUID()}`; + const child = spawn( + process.execPath, + [ + "-e", + "setInterval(() => {}, 1000)", + "--", + "--instance-name", + instance, + "--local-storage", + join(storage, "storage"), + join(storage, "backend.sqlite3"), + ], + { stdio: "ignore" }, + ); + commands.set( + child.pid!, + `node --instance-name ${instance} --local-storage ${join(storage, "storage")} ${join(storage, "backend.sqlite3")}`, + ); + children.push(child); + exits.push( + new Promise((done) => + child.once("exit", (_code, signal) => done(signal)), + ), + ); + publishOwnership(storage, child.pid!, instance); + } + await mkdir(join(directory, "missing")); + await mkdir(join(directory, "stale")); + await writeFile( + join(directory, "stale", "pid"), + JSON.stringify({ + pid: children[0]!.pid, + instanceName: `libar-${randomUUID()}`, + directory: join(directory, "stale"), + }), + ); + const kill = vi.spyOn(process, "kill"); + // First signal fails; traversal still reaches the second owned process. + kill.mockImplementationOnce(() => { + throw Object.assign(new Error("already gone"), { code: "ESRCH" }); + }); + sweep(directory); + expect( + kill.mock.calls.filter((call) => call[1] === "SIGKILL"), + ).toHaveLength(2); + const failedPid = kill.mock.calls[0]![0]; + kill.mockRestore(); + children.find((child) => child.pid === failedPid)!.kill("SIGKILL"); + expect(await Promise.all(exits)).toEqual(["SIGKILL", "SIGKILL"]); + await expect(stat(directory)).rejects.toThrow(); + sweep(directory); + } finally { + vi.restoreAllMocks(); + for (const child of children) child.kill("SIGKILL"); + sweep(directory); + } +}); + +test("pure: empty, partial, invalid and stale pid files never signal", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-invalid-pid-")); + const kill = vi.spyOn(process, "kill").mockImplementation(() => true); + try { + const commands = new Map(); + vi.mocked(execFileSync).mockImplementation( + (_file, args) => + commands.get(Number((args as string[])[2])) ?? "unrelated process", + ); + for (const [index, value] of [ + "", + "{", + "0", + "-1", + "123", + 0, + -1, + 1.5, + ].entries()) { + const storage = join(directory, String(index)); + await mkdir(storage); + const instanceName = "libar-00000000-0000-0000-0000-000000000001"; + if (typeof value === "number") + commands.set( + value, + `node --instance-name ${instanceName} --local-storage ${join(storage, "storage")} ${join(storage, "backend.sqlite3")}`, + ); + await writeFile( + join(storage, "pid"), + typeof value === "number" + ? JSON.stringify({ pid: value, directory: storage, instanceName }) + : value, + ); + } + sweep(directory); + expect(kill).not.toHaveBeenCalled(); + expect(() => publishOwnership(directory, 0, "unused")).toThrow( + "Invalid backend process id", + ); + } finally { + kill.mockRestore(); + sweep(directory); + } +}); + +test("pure: a backend whose process cannot be inspected is kept with its record, the rest is swept, and the sweep fails naming it", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-sweep-kept-")); + const secret = "ab".repeat(32); + const instanceName = "libar-00000000-0000-0000-0000-000000000001"; + const command = (storage: string) => + `convex-local-backend --instance-name ${instanceName} --instance-secret ${secret} --local-storage ${join(storage, "storage")} ${join(storage, "backend.sqlite3")}`; + // pid -> what ps does for it, and whether the process table still has it. + const cases: Record< + string, + { pid: number; ps: () => string; alive: boolean } + > = { + "ps-missing": { + pid: 101, + ps: () => { + throw Object.assign(new Error("spawnSync ps ENOENT"), { + code: "ENOENT", + }); + }, + alive: true, + }, + "ps-failed": { + pid: 102, + ps: () => { + throw Object.assign(new Error("Command failed"), { + status: 2, + stdout: "", + stderr: `ps: ${secret}`, + }); + }, + alive: true, + }, + "ps-silent-but-alive": { + pid: 103, + ps: () => { + throw Object.assign(new Error("Command failed"), { + status: 1, + stdout: "", + stderr: "", + }); + }, + alive: true, + }, + "nested/ps-missing": { + pid: 104, + ps: () => { + throw Object.assign(new Error("spawnSync ps ENOENT"), { + code: "ENOENT", + }); + }, + alive: true, + }, + gone: { + pid: 105, + ps: () => { + throw Object.assign(new Error("Command failed"), { + status: 1, + stdout: "", + stderr: "", + }); + }, + alive: false, + }, + "another-program": { pid: 106, ps: () => "vim notes.txt", alive: true }, + owned: { + pid: 107, + ps: () => command(join(directory, "owned")), + alive: true, + }, + }; + const byPid = new Map( + Object.values(cases).map((entry) => [entry.pid, entry]), + ); + vi.mocked(execFileSync).mockImplementation( + (_file, args) => byPid.get(Number((args as string[])[2]))!.ps() as never, + ); + const kill = vi.spyOn(process, "kill").mockImplementation((pid) => { + if (byPid.get(pid)?.alive === false) + throw Object.assign(new Error("kill ESRCH"), { code: "ESRCH" }); + return true; + }); + try { + for (const [name, { pid }] of Object.entries(cases)) { + const storage = join(directory, name); + await mkdir(join(storage, "storage"), { recursive: true }); + publishOwnership(storage, pid, instanceName); + } + await writeFile(join(directory, "nested", "loose"), ""); + await mkdir(join(directory, "no-record")); + let failure: Error | undefined; + try { + sweep(directory); + } catch (error) { + failure = error as Error; + } + expect(failure).toBeInstanceOf(Error); + for (const name of [ + "ps-missing", + "ps-failed", + "ps-silent-but-alive", + "nested/ps-missing", + ]) { + expect(failure!.message).toContain(join(directory, name)); + expect((await stat(join(directory, name, "pid"))).isFile()).toBe(true); + expect((await stat(join(directory, name, "storage"))).isDirectory()).toBe( + true, + ); + } + expect(failure!.message).not.toContain(secret); + expect(failure!.message).not.toContain("--instance-name"); + for (const name of [ + "gone", + "another-program", + "owned", + "no-record", + "nested/loose", + ]) + await expect(stat(join(directory, name))).rejects.toThrow(); + expect(kill.mock.calls.filter((call) => call[1] === "SIGKILL")).toEqual([ + [107, "SIGKILL"], + ]); + } finally { + kill.mockRestore(); + vi.mocked(execFileSync).mockReset(); + await rm(directory, { recursive: true, force: true }); + } +}); + +// What ps does for a process id no process has: status 1 and no output. +function noSuchProcess(): string { + throw Object.assign(new Error("Command failed"), { + status: 1, + stdout: "", + stderr: "", + }); +} +test("pure: a name-only ps answer and a failed signal keep the backend; a zombie and a readable other command are absent", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-sweep-answers-")); + const instanceName = "libar-00000000-0000-0000-0000-000000000001"; + const owned = (name: string) => + `convex-local-backend --instance-name ${instanceName} --local-storage ${join(directory, name, "storage")} ${join(directory, name, "backend.sqlite3")}\n`; + // pid -> ps's command answer, its state answer, and how SIGKILL fails, if it does. + const cases: Record< + string, + { + pid: number; + command: string; + state: string | (() => string); + killCode?: string; + } + > = { + "empty-answer": { pid: 209, command: "\n", state: "S" }, + // The process is reaped between the two inspections: the state query finds no process. + "name-only-gone-before-state": { + pid: 210, + command: "(convex-local-ba)\n", + state: noSuchProcess, + killCode: "ESRCH", + }, + // The state query fails the same way, but the process table still has the process. + "name-only-state-unproven": { + pid: 211, + command: "(convex-local-ba)\n", + state: noSuchProcess, + }, + "linux-name-only": { pid: 201, command: "[MainThread]\n", state: "S" }, + "macos-name-only": { pid: 202, command: "(convex-local-ba)\n", state: "S" }, + "linux-zombie": { + pid: 203, + command: "[convex-local-ba] \n", + state: "Z", + }, + "macos-zombie": { pid: 204, command: "\n", state: "Z" }, + "name-only-zombie": { + pid: 205, + command: "(convex-local-ba)\n", + state: "Z", + }, + "another-program": { pid: 206, command: "node server.js\n", state: "S" }, + "signal-refused": { + pid: 207, + command: owned("signal-refused"), + state: "S", + killCode: "EPERM", + }, + "exited-before-signal": { + pid: 208, + command: owned("exited-before-signal"), + state: "S", + killCode: "ESRCH", + }, + }; + const byPid = new Map( + Object.values(cases).map((entry) => [entry.pid, entry]), + ); + vi.mocked(execFileSync).mockImplementation((_file, args) => { + const list = args as string[]; + const entry = byPid.get(Number(list[list.indexOf("-p") + 1]))!; + if (!list.includes("state=")) return entry.command as never; + return ( + typeof entry.state === "string" ? entry.state : entry.state() + ) as never; + }); + const kill = vi.spyOn(process, "kill").mockImplementation((pid) => { + const code = byPid.get(pid)?.killCode; + if (code !== undefined) + throw Object.assign(new Error(`kill ${code}`), { code }); + return true; + }); + try { + for (const [name, { pid }] of Object.entries(cases)) { + const storage = join(directory, name); + await mkdir(join(storage, "storage"), { recursive: true }); + publishOwnership(storage, pid, instanceName); + } + let failure: Error | undefined; + try { + sweep(directory); + } catch (error) { + failure = error as Error; + } + const kept = [ + "linux-name-only", + "macos-name-only", + "signal-refused", + "empty-answer", + "name-only-state-unproven", + ]; + expect(failure).toBeInstanceOf(Error); + for (const name of kept) { + expect(failure!.message).toContain(join(directory, name)); + expect((await stat(join(directory, name, "pid"))).isFile()).toBe(true); + } + expect(failure!.message).not.toContain("--instance-name"); + for (const name of Object.keys(cases).filter( + (name) => !kept.includes(name), + )) + await expect(stat(join(directory, name))).rejects.toThrow(); + } finally { + kill.mockRestore(); + vi.mocked(execFileSync).mockReset(); + await rm(directory, { recursive: true, force: true }); + } +}); + +// What the global setup and the reporter do for one native run: the setup opens the run, the +// reporter writes its record and hands the path over. +async function reportedRun(records: string) { + const run = openRunRecord(); + const reporter = new EvidenceReporter({ directory: records }); + reporter.onTestRunStart([{ project: { name: "native" } }] as never); + const before = new Set(await readdir(records)); + await reporter.onTestRunEnd([] as never, [] as never, "passed" as never); + const [name] = (await readdir(records)).filter((entry) => !before.has(entry)); + return { run, path: join(records, name!) }; +} +async function readRecord(path: string): Promise { + return JSON.parse(await readFile(path, "utf8")) as NativeRunRecord; +} + +test("pure: a failed final sweep fails its own run's record, through the reporter's hand-over, and no other run's", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-final-sweep-")); + const records = await mkdtemp(join(tmpdir(), "libar-final-sweep-records-")); + vi.mocked(execFileSync).mockImplementation(() => { + throw Object.assign(new Error("spawnSync ps ENOENT"), { code: "ENOENT" }); + }); + async function uninspectable(name: string): Promise { + const root = join(directory, name); + await mkdir(join(root, "backend", "storage"), { recursive: true }); + publishOwnership( + join(root, "backend"), + 301, + "libar-00000000-0000-0000-0000-000000000001", + ); + return root; + } + function failureOf(action: () => void): Error | undefined { + try { + action(); + } catch (error) { + return error as Error; + } + return undefined; + } + try { + // Run A: its sweep fails after its record was written. + const a = await reportedRun(records); + const passed = await readRecord(a.path); + expect(passed.result).toBe("passed"); + const rootA = await uninspectable("a"); + const failureA = failureOf(() => finalSweep(rootA, a.run)); + expect(failureA).toBeInstanceOf(Error); + expect(await readRecord(a.path)).toEqual({ + ...passed, + result: "failed", + unhandledErrors: [failureA!.message], + }); + // A's exit handler finds A's cleanup done. + expect(failureOf(() => finalSweep(rootA, a.run))).toBeUndefined(); + // Run B in the same process passes, and A's cleanup does not act again. + const b = await reportedRun(records); + const clean = join(directory, "b"); + await mkdir(clean); + finalSweep(clean, b.run); + expect(failureOf(() => finalSweep(rootA, a.run))).toBeUndefined(); + expect((await readRecord(b.path)).result).toBe("passed"); + // Run C's sweep fails before its record was written: it amends nothing. + const c = openRunRecord(); + const rootC = await uninspectable("c"); + expect(failureOf(() => finalSweep(rootC, c))).toBeInstanceOf(Error); + expect((await readRecord(b.path)).result).toBe("passed"); + expect((await readRecord(a.path)).unhandledErrors).toEqual([ + failureA!.message, + ]); + // Amending twice with the same error stores it once. + failRunRecord(a.run, failureA!.message); + expect((await readRecord(a.path)).unhandledErrors).toEqual([ + failureA!.message, + ]); + } finally { + vi.mocked(execFileSync).mockReset(); + await rm(directory, { recursive: true, force: true }); + await rm(records, { recursive: true, force: true }); + } +}); diff --git a/tests/pure/native.test.ts b/tests/pure/native.test.ts new file mode 100644 index 0000000..e21b168 --- /dev/null +++ b/tests/pure/native.test.ts @@ -0,0 +1,94 @@ +import { expect, test, vi } from "vitest"; +import type { Backend } from "../../harness/backend.js"; +const context = vi.hoisted(() => ({ + current: { + meta: {} as { native?: { backends: unknown[]; measurements: unknown[] } }, + context: { signal: new AbortController().signal }, + }, + finished: [] as (() => Promise)[], +})); +vi.mock("vitest", async (original) => ({ + ...(await original()), + TestRunner: { getCurrentTest: () => context.current }, + inject: () => ({ directory: "unused", executable: {} }), + onTestFinished: (finish: () => Promise) => + context.finished.push(finish), +})); +vi.mock("../../harness/identity.js", () => ({ + createFixtureIssuer: async () => ({}), +})); +vi.mock("../../harness/backend.js", () => ({ startBackend: vi.fn() })); +import { startBackend } from "../../harness/backend.js"; +import { fixtureBackend } from "../../harness/native.js"; + +test("pure: a backend whose deploy fails is disposed and still contributes its facts to its test", async () => { + context.current.meta = {}; + context.finished = []; + const dispose = vi.fn(async () => {}); + const facts = { + composition: null, + identitySource: { issuer: "actual-issuer" }, + }; + const backend = { + admin: { + deploy: vi.fn(async () => { + throw new Error("deployment failed"); + }), + }, + dispose, + facts: () => facts, + } as unknown as Backend; + vi.mocked(startBackend).mockImplementation(async () => { + await Promise.resolve(); + expect(context.finished).toHaveLength(1); + return backend; + }); + await expect(fixtureBackend()).rejects.toThrow("deployment failed"); + expect(dispose).toHaveBeenCalledOnce(); + expect(vi.mocked(startBackend).mock.calls[0]?.[0].signal).toBe( + context.current.context.signal, + ); + await context.finished[0]!(); + expect(context.current.meta.native?.backends).toEqual([facts]); + expect(dispose).toHaveBeenCalledTimes(2); +}); + +test("pure: fixtureBackend owns an overridden executable and can skip deploy", async () => { + context.current.meta = {}; + context.finished = []; + const deploy = vi.fn(async () => {}); + const dispose = vi.fn(async () => {}); + const backend = { + admin: { deploy }, + dispose, + facts: () => ({ composition: null }), + } as unknown as Backend; + vi.mocked(startBackend).mockResolvedValue(backend); + const executable = { + path: "synthetic", + sha256: "hash", + release: null, + source: "CONVEX_BACKEND_BINARY" as const, + }; + expect( + await fixtureBackend({ + executable, + parentDirectory: "owned-directory", + deploy: false, + }), + ).toBe(backend); + expect(startBackend).toHaveBeenLastCalledWith( + expect.objectContaining({ + executable, + parentDirectory: "owned-directory", + signal: context.current.context.signal, + }), + ); + expect(deploy).not.toHaveBeenCalled(); + expect(context.finished).toHaveLength(1); + await context.finished[0]!(); + expect(dispose).toHaveBeenCalledOnce(); + expect(context.current.meta.native?.backends).toEqual([ + { composition: null }, + ]); +}); diff --git a/tests/pure/ownership.test.ts b/tests/pure/ownership.test.ts new file mode 100644 index 0000000..b880bee --- /dev/null +++ b/tests/pure/ownership.test.ts @@ -0,0 +1,35 @@ +import { renameSync } from "node:fs"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test, vi } from "vitest"; +vi.mock("node:fs", async (original) => { + const fs = await original(); + return { ...fs, renameSync: vi.fn(fs.renameSync) }; +}); +import { publishOwnership } from "../../harness/ownership.js"; + +test("pure: a failed ownership publication leaves the previous complete pid record intact", async () => { + const directory = await mkdtemp(join(tmpdir(), "libar-ownership-")); + try { + publishOwnership( + directory, + 123, + "libar-00000000-0000-0000-0000-000000000001", + ); + const original = await readFile(join(directory, "pid"), "utf8"); + vi.mocked(renameSync).mockImplementationOnce(() => { + throw new Error("publication failed"); + }); + expect(() => + publishOwnership( + directory, + 456, + "libar-00000000-0000-0000-0000-000000000002", + ), + ).toThrow("publication failed"); + expect(await readFile(join(directory, "pid"), "utf8")).toBe(original); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/tests/pure/watch.test.ts b/tests/pure/watch.test.ts new file mode 100644 index 0000000..00807ac --- /dev/null +++ b/tests/pure/watch.test.ts @@ -0,0 +1,490 @@ +import type { ChildProcess } from "node:child_process"; +import { EventEmitter } from "node:events"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { PassThrough, Writable } from "node:stream"; +import { expect, onTestFinished, test, vi } from "vitest"; +import type { CliCall } from "../../harness/admin.js"; +import type { Executable } from "../../harness/executable.js"; +import type { FixtureIssuer } from "../../harness/identity.js"; +import { + Interrupt, + relay, + settleEnvFile, + watch, + writtenForBackend, +} from "../../harness/watch.js"; +import type { WatchSteps } from "../../harness/watch.js"; + +const secret = "SYNTHETIC-ADMIN-KEY-0123456789abcdef"; +const url = "http://127.0.0.1:61037"; +const siteUrl = "http://127.0.0.1:61038"; +const written = `CONVEX_URL=${url}\n\nCONVEX_SITE_URL=${siteUrl}\n`; + +async function scratch(): Promise { + const directory = await mkdtemp(join(tmpdir(), "libar-watch-")); + onTestFinished(() => rm(directory, { recursive: true, force: true })); + return directory; +} +function collector(onText: (text: string) => void = () => undefined) { + let text = ""; + const stream = new Writable({ + write(chunk: Buffer, _encoding, done) { + text += chunk.toString(); + onText(text); + done(); + }, + }); + return { stream, text: () => text }; +} +async function exists(path: string): Promise { + return readFile(path).then( + () => true, + () => false, + ); +} + +test("pure: the relay removes a secret split across chunks and on a last line with no newline", async () => { + const input = new PassThrough(); + const output = collector(); + const relayed = relay(input, output.stream, [secret]); + input.write(`first ${secret.slice(0, 7)}`); + input.write(`${secret.slice(7, 20)}`); + input.write(`${secret.slice(20)} done\nlast ${secret.slice(0, 30)}`); + input.end(secret.slice(30)); + await relayed; + expect(output.text()).toBe("first [redacted] done\nlast [redacted]\n"); + expect(output.text()).not.toContain(secret.slice(0, 12)); +}); + +test("pure: .env.local is convex dev's file only when every non-empty line names this run's backend", () => { + const backend = { url, siteUrl }; + expect(writtenForBackend(written, backend)).toBe(true); + expect(writtenForBackend(written.replaceAll("\n", "\r\n"), backend)).toBe( + true, + ); + expect(writtenForBackend(`CONVEX_URL=${url}\n`, backend)).toBe(true); + expect(writtenForBackend("", backend)).toBe(false); + expect(writtenForBackend("\n\n", backend)).toBe(false); + expect(writtenForBackend(`${written}DEVELOPER_OWNED=1\n`, backend)).toBe( + false, + ); + expect( + writtenForBackend(written.replace(url, "http://127.0.0.1:9"), backend), + ).toBe(false); + expect( + writtenForBackend(written.replace(siteUrl, "http://127.0.0.1:9"), backend), + ).toBe(false); + expect(writtenForBackend(`CONVEX_URL=${url} # a note\n`, backend)).toBe( + false, + ); +}); + +test("pure: settling .env.local removes only the file the spawned CLI wrote for this backend", async () => { + const directory = await scratch(); + const path = join(directory, ".env.local"); + const run = { spawned: true, url, siteUrl }; + expect(await settleEnvFile(path, run)).toBe("absent"); + await writeFile(path, written); + expect(await settleEnvFile(path, { ...run, spawned: false })).toBe("kept"); + expect(await settleEnvFile(path, { ...run, siteUrl: undefined })).toBe( + "kept", + ); + expect(await readFile(path, "utf8")).toBe(written); + await writeFile(path, "DEVELOPER_OWNED=1\n"); + expect(await settleEnvFile(path, run)).toBe("kept"); + expect(await readFile(path, "utf8")).toBe("DEVELOPER_OWNED=1\n"); + await writeFile(path, written); + expect(await settleEnvFile(path, run)).toBe("removed"); + expect(await exists(path)).toBe(false); +}); + +// A stand-in for the convex dev child: it writes .env.local as the CLI does, prints the admin key +// split across chunks, and exits on SIGINT unless it is told to ignore it. +class FakeCli extends EventEmitter { + exitCode: number | null = null; + signalCode: NodeJS.Signals | null = null; + stdout = new PassThrough(); + stderr = new PassThrough(); + signals: string[] = []; + ignoreSigint = false; + // A descendant that inherited the CLI's output keeps the streams open after the CLI exits. + holdStreams = false; + killError: Error | undefined; + kill(signal: NodeJS.Signals): boolean { + this.signals.push(signal); + if (this.killError !== undefined) throw this.killError; + if (signal === "SIGINT" && this.ignoreSigint) return true; + this.exit(signal === "SIGINT" ? 130 : null, signal); + return true; + } + exit(code: number | null, signal: NodeJS.Signals | null = null) { + if (this.exitCode !== null || this.signalCode !== null) return; + this.exitCode = code; + this.signalCode = code === null ? signal : null; + if (!this.holdStreams) { + this.stdout.end(); + this.stderr.end(); + } + this.emit("exit", code, this.signalCode); + } +} +interface Harness { + hooks: { onStdout: (text: string) => void }; + envFile: string; + controller: AbortController; + steps: WatchSteps; + calls: string[]; + cli: FakeCli; + spawnedCalls: CliCall[]; + disposed: () => boolean; + stdout: ReturnType; + stderr: ReturnType; + run(): Promise; +} +async function harness( + overrides: Partial = {}, + onSpawn: (cli: FakeCli, envFile: string) => Promise | void = ( + _cli, + envFile, + ) => writeFile(envFile, written), +): Promise { + const directory = await scratch(); + const envFile = join(directory, ".env.local"); + const controller = new AbortController(); + const calls: string[] = []; + const cli = new FakeCli(); + const spawnedCalls: CliCall[] = []; + let disposed = false; + const hooks: { onStdout: (text: string) => void } = { + onStdout: () => undefined, + }; + const steps: WatchSteps = { + resolveExecutable: async () => { + calls.push("resolveExecutable"); + return { path: "/fake" } as Executable; + }, + createFixtureIssuer: async () => { + calls.push("createFixtureIssuer"); + return {} as FixtureIssuer; + }, + startBackend: async () => { + calls.push("startBackend"); + return { + url, + adminKey: secret, + dispose: async () => { + calls.push("dispose"); + disposed = true; + }, + }; + }, + siteUrl: async () => { + calls.push("siteUrl"); + return siteUrl; + }, + makeHome: async () => { + calls.push("makeHome"); + return join(directory, "home"); + }, + removeHome: async () => { + calls.push("removeHome"); + }, + spawn: (call) => { + calls.push("spawn"); + spawnedCalls.push(call); + void onSpawn(cli, envFile); + setImmediate(() => { + cli.stdout.write(`pushed with ${secret.slice(0, 10)}`); + cli.stdout.write(`${secret.slice(10)}\n`); + }); + return cli as unknown as ChildProcess; + }, + ...overrides, + }; + const stdout = collector((text) => hooks.onStdout(text)); + const stderr = collector(); + return { + hooks, + envFile, + controller, + steps, + calls, + cli, + spawnedCalls, + disposed: () => disposed, + stdout, + stderr, + run: () => + watch({ + envFile, + signal: controller.signal, + steps, + stdout: stdout.stream, + stderr: stderr.stream, + stopGraceMs: 50, + }), + }; +} +// One line of our own, no stack trace and no secret. +function expectOneLine(text: string, line: string) { + expect(text).toBe(`npm run dev: ${line}\n`); +} +function waitFor(condition: () => boolean): Promise { + return vi.waitFor(() => expect(condition()).toBe(true), { timeout: 2000 }); +} + +test.each([ + ["SIGINT", 130], + ["SIGTERM", 143], +] as const)( + "pure: %s during executable resolution ends with exit code %i, one line, and the resolution told to stop", + async (signal, code) => { + let seen: AbortSignal | undefined; + const run = await harness({ + resolveExecutable: (stop) => { + seen = stop; + return new Promise((_done, fail) => + stop.addEventListener("abort", () => fail(stop.reason as Error)), + ); + }, + }); + const running = run.run(); + await waitFor(() => seen !== undefined); + run.controller.abort(new Interrupt(signal)); + expect(await running).toBe(code); + expect(seen?.aborted).toBe(true); + expectOneLine(run.stderr.text(), `stopped by ${signal}.`); + expect(run.calls).not.toContain("startBackend"); + }, +); + +test("pure: SIGTERM while the backend starts ends with exit code 143 after cleanup", async () => { + const run = await harness({ + startBackend: (options) => + new Promise((_done, fail) => + options.signal?.addEventListener("abort", () => + fail(options.signal?.reason as Error), + ), + ), + }); + const running = run.run(); + await waitFor(() => run.calls.includes("createFixtureIssuer")); + run.controller.abort(new Interrupt("SIGTERM")); + expect(await running).toBe(143); + expectOneLine(run.stderr.text(), "stopped by SIGTERM."); +}); + +test("pure: SIGINT just before ready ends with exit code 130, no CLI, the backend and home removed", async () => { + const controller = { abort: () => undefined as void }; + const run = await harness({ + siteUrl: async () => { + controller.abort(); + return siteUrl; + }, + }); + controller.abort = () => run.controller.abort(new Interrupt("SIGINT")); + expect(await run.run()).toBe(130); + expect(run.calls).not.toContain("spawn"); + expect(run.disposed()).toBe(true); + expect(run.calls).toContain("removeHome"); + expectOneLine(run.stderr.text(), "stopped by SIGINT."); +}); + +test("pure: SIGINT between the ready line and the spawn ends with exit code 130 and no CLI", async () => { + const run = await harness(); + run.hooks.onStdout = (text) => { + if (text.includes("backend ready")) + run.controller.abort(new Interrupt("SIGINT")); + }; + expect(await run.run()).toBe(130); + expect(run.calls).not.toContain("spawn"); + expect(run.disposed()).toBe(true); + expectOneLine(run.stderr.text(), "stopped by SIGINT."); +}); + +test("pure: SIGINT while the CLI runs stops it, removes the .env.local it wrote, and prints the ready line once and no secret", async () => { + const run = await harness(); + const running = run.run(); + await waitFor(() => run.stdout.text().includes("pushed with")); + expect(await exists(run.envFile)).toBe(true); + run.controller.abort(new Interrupt("SIGINT")); + expect(await running).toBe(130); + expect(run.cli.signals).toEqual(["SIGINT"]); + expect(await exists(run.envFile)).toBe(false); + expect(run.disposed()).toBe(true); + expect(run.stdout.text()).toBe( + `npm run dev: backend ready at ${url}. Press Ctrl-C to stop.\npushed with [redacted]\n`, + ); + expect(run.spawnedCalls[0]?.args.slice(-4)).toEqual([ + "--url", + url, + "--admin-key", + secret, + ]); + expectOneLine(run.stderr.text(), "stopped by SIGINT."); +}); + +test("pure: a CLI that ignores SIGINT is killed after the grace period", async () => { + const run = await harness(); + run.cli.ignoreSigint = true; + const running = run.run(); + await waitFor(() => run.calls.includes("spawn")); + run.controller.abort(new Interrupt("SIGINT")); + expect(await running).toBe(130); + expect(run.cli.signals).toEqual(["SIGINT", "SIGKILL"]); +}); + +test("pure: a .env.local the developer edited during the run stays, with one line saying so", async () => { + const run = await harness(); + const running = run.run(); + await waitFor(() => run.stdout.text().includes("pushed with")); + await writeFile(run.envFile, "DEVELOPER_OWNED=1\n"); + run.controller.abort(new Interrupt("SIGINT")); + expect(await running).toBe(130); + expect(await readFile(run.envFile, "utf8")).toBe("DEVELOPER_OWNED=1\n"); + expect(run.stderr.text()).toBe( + `npm run dev: left ${run.envFile} in place: it is not the file convex dev wrote for this backend.\nnpm run dev: stopped by SIGINT.\n`, + ); +}); + +test("pure: a .env.local that appears before the CLI is spawned is never removed, and no CLI starts", async () => { + let envFile = ""; + const run = await harness({ + startBackend: async () => { + // Exactly what convex dev would write, so only the timing makes it the developer's. + await writeFile(envFile, written); + return { url, adminKey: secret, dispose: async () => undefined }; + }, + }); + envFile = run.envFile; + expect(await run.run()).toBe(1); + expect(run.calls).not.toContain("spawn"); + expect(await readFile(run.envFile, "utf8")).toBe(written); + expect(run.stderr.text()).toContain("left"); + expect(run.stderr.text()).toContain("exists. convex dev would write"); +}); + +test("pure: a .env.local present at the start refuses the run before anything starts", async () => { + const run = await harness(); + await writeFile(run.envFile, "MINE=1\n"); + expect(await run.run()).toBe(1); + expect(run.calls).toEqual([]); + expect(await readFile(run.envFile, "utf8")).toBe("MINE=1\n"); +}); + +test("pure: every cleanup step runs when earlier ones fail, and the failures are reported together with exit code 1", async () => { + const run = await harness({ + startBackend: async () => ({ + url, + adminKey: secret, + dispose: async () => { + throw new Error(`dispose failed near ${secret}`); + }, + }), + removeHome: async () => { + throw new Error("home is busy"); + }, + }); + const running = run.run(); + await waitFor(() => run.stdout.text().includes("pushed with")); + run.controller.abort(new Interrupt("SIGINT")); + expect(await running).toBe(1); + expect(run.cli.signals).toEqual(["SIGINT"]); + // The step after both failures still ran. + expect(await exists(run.envFile)).toBe(false); + expectOneLine( + run.stderr.text(), + "cleanup failed: removing the backend: dispose failed near [redacted]; removing the CLI home: home is busy", + ); +}); + +test("pure: a CLI that exits by itself ends the run with exit code 1 and one line", async () => { + const run = await harness({}, (cli, envFile) => { + void writeFile(envFile, written).then(() => cli.exit(1)); + }); + expect(await run.run()).toBe(1); + expect(run.disposed()).toBe(true); + expect(await exists(run.envFile)).toBe(false); + expectOneLine(run.stderr.text(), "convex dev exited with code 1"); +}); + +test("pure: a startup failure that is not an interrupt ends with exit code 1 and its message, without the secret", async () => { + const run = await harness({ + siteUrl: async () => { + throw new Error(`site URL read failed for ${secret}`); + }, + }); + expect(await run.run()).toBe(1); + expect(run.disposed()).toBe(true); + expectOneLine(run.stderr.text(), "site URL read failed for [redacted]"); +}); + +test("pure: a .env.local with a comment line beside the CLI's lines is the developer's and stays", async () => { + const commented = `# my notes\n${written}`; + expect(writtenForBackend(commented, { url, siteUrl })).toBe(false); + const directory = await scratch(); + const path = join(directory, ".env.local"); + await writeFile(path, commented); + expect(await settleEnvFile(path, { spawned: true, url, siteUrl })).toBe( + "kept", + ); + expect(await readFile(path, "utf8")).toBe(commented); +}); + +test("pure: an interrupt while the CLI home is being made still removes the home", async () => { + const parent = await scratch(); + const actual = + await vi.importActual( + "node:fs/promises", + ); + let made: string | undefined; + const run = await harness({ + makeHome: async () => { + run.controller.abort(new Interrupt("SIGINT")); + // The directory appears after the interrupt. + await new Promise((done) => setTimeout(done, 100)); + made = await actual.mkdtemp(join(parent, "libar-dev-home-")); + return made; + }, + removeHome: (home) => actual.rm(home, { recursive: true, force: true }), + }); + expect(await run.run()).toBe(130); + expect(made).toBeDefined(); + await expect(actual.stat(made!)).rejects.toThrow(); + expect(await actual.readdir(parent)).toEqual([]); + expectOneLine(run.stderr.text(), "stopped by SIGINT."); +}); + +test("pure: after the grace period the CLI's streams are let go of, so a descendant holding them cannot keep the run alive", async () => { + const run = await harness(); + run.cli.holdStreams = true; + const running = run.run(); + await waitFor(() => run.stdout.text().includes("pushed with")); + run.controller.abort(new Interrupt("SIGINT")); + expect(await running).toBe(130); + expect(run.cli.stdout.destroyed).toBe(true); + expect(run.cli.stderr.destroyed).toBe(true); +}); + +test("pure: a failure of the first cleanup step, stopping the CLI, does not skip the later steps", async () => { + let removedHome = false; + const run = await harness({ + removeHome: async () => { + removedHome = true; + }, + }); + const running = run.run(); + await waitFor(() => run.stdout.text().includes("pushed with")); + run.cli.killError = new Error("kill refused"); + run.controller.abort(new Interrupt("SIGINT")); + expect(await running).toBe(1); + expect(run.disposed()).toBe(true); + expect(removedHome).toBe(true); + expect(await exists(run.envFile)).toBe(false); + expectOneLine( + run.stderr.text(), + "cleanup failed: stopping convex dev: kill refused", + ); +}); diff --git a/tests/simulator/boundaries.test.ts b/tests/simulator/boundaries.test.ts new file mode 100644 index 0000000..11c1b1e --- /dev/null +++ b/tests/simulator/boundaries.test.ts @@ -0,0 +1,48 @@ +import { convexTest } from "convex-test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { api } from "../../fixture/convex/_generated/api.js"; +import annexSchema from "../../fixture/convex/annex/schema.js"; +import schema from "../../fixture/convex/schema.js"; +const { version } = JSON.parse( + readFileSync( + join(import.meta.dirname, "../../node_modules/convex-test/package.json"), + "utf8", + ), +) as { version: string }; +function simulator() { + const t = convexTest( + schema, + import.meta.glob("../../fixture/convex/**/*.ts"), + ); + t.registerComponent( + "annex", + annexSchema, + import.meta.glob("../../fixture/convex/annex/**/*.ts"), + ); + return t; +} +const data = { code: "staleVersion", expected: 2, current: 3 }; +test.each(["nested", "annex"] as const)( + `convex-test ${version}: a parent's catch reads the ConvexError data thrown across the %s boundary`, + async (boundary) => { + const caught = await simulator().mutation(api.failures.catching, { + boundary, + kind: "convexError", + data, + }); + expect(caught).toMatchObject({ isConvexError: true, data }); + }, +); +test(`convex-test ${version}: ctx.auth.getUserIdentity() inside a component returns null under withIdentity`, async () => { + const t = simulator().withIdentity({ + issuer: "https://fixture-issuer.test", + subject: "user-1", + }); + expect(await t.query(api.identity.caller, {})).toMatchObject({ + issuer: "https://fixture-issuer.test", + subject: "user-1", + }); + expect(await t.query(api.identity.callerSeenByAnnex, {})).toBeNull(); +}); diff --git a/tests/types/api.test-d.ts b/tests/types/api.test-d.ts new file mode 100644 index 0000000..b647931 --- /dev/null +++ b/tests/types/api.test-d.ts @@ -0,0 +1,24 @@ +import { expectTypeOf, test } from "vitest"; +import type { FunctionReference } from "convex/server"; +import { + api, + components, + internal, +} from "../../fixture/convex/_generated/api.js"; +test("compiled: the generated API exposes the fixture's public functions and keeps its internal and component functions out of the public API", () => { + expectTypeOf(api.identity.caller).toExtend< + FunctionReference<"query", "public"> + >(); + expectTypeOf(api.failures.catching).toExtend< + FunctionReference<"mutation", "public"> + >(); + expectTypeOf(internal.notes.addInternal).toExtend< + FunctionReference<"mutation", "internal"> + >(); + expectTypeOf(components.annex.notes.add).toExtend< + FunctionReference<"mutation", "internal"> + >(); + expectTypeOf<"throwAfterWrite">().not.toExtend(); + expectTypeOf<"notes">().not.toExtend(); + expectTypeOf<"annex">().not.toExtend(); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2b9c0bd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "moduleSuffixes": [".d", ""], + "strict": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "erasableSyntaxOnly": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["node", "vite/client"] + }, + "include": [ + "fixture/**/*.ts", + "harness/**/*.ts", + "tests/**/*.ts", + "vitest.config.ts" + ], + "exclude": ["node_modules", "generated", "vendor"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..4503140 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from "vitest/config"; +export default defineConfig({ + test: { + reporters: ["default", "./harness/evidence.ts"], + projects: [ + { + test: { + name: "types", + include: [], + typecheck: { + enabled: true, + only: true, + checker: "tsc", + include: ["tests/types/**/*.test-d.ts"], + }, + }, + }, + { + test: { + name: "pure", + environment: "node", + include: ["tests/pure/**/*.test.ts"], + }, + }, + { + test: { + name: "simulator", + environment: "edge-runtime", + include: ["tests/simulator/**/*.test.ts"], + }, + }, + { + test: { + name: "native", + environment: "node", + include: ["tests/native/**/*.test.ts"], + globalSetup: ["./harness/native-run.ts"], + testTimeout: 120000, + hookTimeout: 60000, + }, + }, + ], + }, +});