diff --git a/.changeset/owner-id-anchor-and-bulk-write-scoping.md b/.changeset/owner-id-anchor-and-bulk-write-scoping.md index 748519ad04..f3353867a6 100644 --- a/.changeset/owner-id-anchor-and-bulk-write-scoping.md +++ b/.changeset/owner-id-anchor-and-bulk-write-scoping.md @@ -55,6 +55,6 @@ row-level scoping keys off to decide who may update/delete a record. now correctly affects zero rows instead of all of them. Proven end-to-end on the real showcase app -(`packages/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts`) and pinned +(`packages/qa/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts`) and pinned in the ADR-0096 authz-conformance ledger (`ownership-anchor-guard`, `bulk-write-owner-scoping`). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0844001db..3b351d59ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,18 +107,26 @@ jobs: # spec sits at the root of the dependency graph, so spec-touching PRs # still run (close to) everything — but the many PRs that don't touch # spec skip the bulk of the 75-package matrix. + # --concurrency=4: turbo's default (10) oversubscribes the 4-vCPU + # hosted runner. As the task graph grew past ~100 tasks (connector + # packages + qa gates), parallel vitest workers + tsup DTS builds + # started exhausting runner memory: plugin-audit#test died with ZERO + # output (kernel OOM-kill signature) deterministically on the runner + # while passing everywhere else — first seen on main @4f8c2d1, + # reproduced twice on #3037. Matching concurrency to the core count + # bounds peak memory; the job is CPU-bound anyway. - name: Run affected tests (PR) if: github.event_name == 'pull_request' env: TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }} - run: pnpm turbo run test --affected + run: pnpm turbo run test --affected --concurrency=4 # Push to main: full run, but exclude spec's plain test task — the # coverage step below executes the exact same 250-file spec suite once, # with coverage. Previously the spec suite ran twice per push. - name: Run all tests (push) if: github.event_name == 'push' - run: pnpm turbo run test --filter=!@objectstack/spec + run: pnpm turbo run test --filter=!@objectstack/spec --concurrency=4 - name: Generate coverage report if: github.event_name == 'push' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8ae16472fc..98615a61b1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -123,9 +123,15 @@ jobs: # bad signal to copy from. tsup transpiles them without a full typecheck, # so build alone will not catch type drift — typecheck them explicitly. # They import from built workspace packages, so the packages must be built - # first for cross-package type resolution to succeed. + # first for cross-package type resolution to succeed. The examples' + # dependency closure is requested EXPLICITLY (`./examples/*^...` = deps + # of the examples, not the examples themselves): the bare `./packages/*` + # glob only matches direct children, and the connector packages the + # showcase imports were previously built only by accident — through + # dogfood's dependency chain, which broke when dogfood moved to + # packages/qa/ (#3037). - name: Build workspace packages - run: pnpm exec turbo run build --filter='./packages/*' + run: pnpm exec turbo run build --filter='./packages/*' --filter='./examples/*^...' - name: Type check example apps run: pnpm --filter './examples/*' run typecheck diff --git a/.github/workflows/spec-liveness-check.yml b/.github/workflows/spec-liveness-check.yml index b442a60b1a..3259dd4c09 100644 --- a/.github/workflows/spec-liveness-check.yml +++ b/.github/workflows/spec-liveness-check.yml @@ -9,7 +9,7 @@ name: Spec Liveness Check # # ADR-0054 (prove-it-runs): bound high-risk 'live' properties must carry a `proof` # pointing to a dogfood test that declares the matching `@proof:` tag. The gate also -# triggers on packages/dogfood/** so deleting/renaming a proof re-runs this check and +# triggers on packages/qa/dogfood/** so deleting/renaming a proof re-runs this check and # the dangling reference is caught (the proof files live outside packages/spec/). on: @@ -17,7 +17,7 @@ on: types: [opened, synchronize, reopened] paths: - 'packages/spec/**' - - 'packages/dogfood/**' + - 'packages/qa/dogfood/**' permissions: contents: read diff --git a/content/docs/permissions/authorization.mdx b/content/docs/permissions/authorization.mdx index d4ebf86024..cfe813a974 100644 --- a/content/docs/permissions/authorization.mdx +++ b/content/docs/permissions/authorization.mdx @@ -318,7 +318,7 @@ Five mechanisms — four CI-time, one runtime — make the security posture a `--update-access-matrix` — the snapshot's git diff is the review artifact. Opt-in, format, and workflow: **[Access-Matrix Snapshot Gate](/docs/permissions/access-matrix)**. -- **Conformance matrix** (`packages/dogfood/test/authz-conformance.matrix.ts`, +- **Conformance matrix** (`packages/qa/dogfood/test/authz-conformance.matrix.ts`, ADR-0056 D10): every authorization primitive sits in exactly one honest state — `enforced` (must name its enforcement site; high-risk rows must reference an end-to-end dogfood proof), `experimental`, or `removed`. A new diff --git a/content/docs/permissions/delegated-administration.mdx b/content/docs/permissions/delegated-administration.mdx index bfe20c6e4e..7c9b8608f2 100644 --- a/content/docs/permissions/delegated-administration.mdx +++ b/content/docs/permissions/delegated-administration.mdx @@ -67,7 +67,7 @@ Writes to the governed tables (`sys_user_position`, | No scope, no admin | Holders of plain CRUD on the RBAC tables with **no** scope are refused: administration is a scoped capability now, not a side effect of table access | Every rule above is exercised end-to-end by the showcase permission zoo -(`packages/dogfood/test/showcase-permission-zoo.dogfood.test.ts`): the +(`packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts`): the in-subtree allowlisted assignment passes with a `granted_by` stamp; the out-of-subtree anchor, the off-allowlist grant (to self), and the `manageBindings: false` binding write are each refused. diff --git a/docs/adr/0054-runtime-proof-for-authorable-surface.md b/docs/adr/0054-runtime-proof-for-authorable-surface.md index 48844b110a..46bc7e304e 100644 --- a/docs/adr/0054-runtime-proof-for-authorable-surface.md +++ b/docs/adr/0054-runtime-proof-for-authorable-surface.md @@ -29,7 +29,7 @@ misbehaves at runtime. A metadata-driven platform whose authors are AI cannot ship unproven liveness for the primitives that matter. The static pointer must be upgradable to a -**runtime proof** — a [`@objectstack/dogfood`](../../packages/dogfood) test that +**runtime proof** — a [`@objectstack/dogfood`](../../packages/qa/dogfood) test that authors the property against the real, in-process stack and asserts the runtime result. diff --git a/docs/adr/0060-conformance-ledger-platform-pattern.md b/docs/adr/0060-conformance-ledger-platform-pattern.md index afb46a976d..b1a0e17e74 100644 --- a/docs/adr/0060-conformance-ledger-platform-pattern.md +++ b/docs/adr/0060-conformance-ledger-platform-pattern.md @@ -202,6 +202,6 @@ indirection. ## References ADRs 0020, 0049, 0054, 0056 (D10), 0058 (D7). Issue #1887. Existing instances: -`packages/dogfood/test/authz-conformance.{matrix,test}.ts`, -`packages/dogfood/test/expression-conformance.{ledger,test}.ts`. Helper home: +`packages/qa/dogfood/test/authz-conformance.{matrix,test}.ts`, +`packages/qa/dogfood/test/expression-conformance.{ledger,test}.ts`. Helper home: `packages/verify/`. Target surface: `packages/spec/src/data/validation.zod.ts`. diff --git a/docs/adr/0076-objectql-core-tiering.md b/docs/adr/0076-objectql-core-tiering.md index 5618b0751f..36ee0e5500 100644 --- a/docs/adr/0076-objectql-core-tiering.md +++ b/docs/adr/0076-objectql-core-tiering.md @@ -1,6 +1,6 @@ # ADR-0076: objectql is the data engine — relocate metadata management (protocol) out of it; enforce the boundary; defer the engine repo-split -**Status**: Proposed (2026-06-28, rev. 9) — D1–D12 below. D9 step-1 (interface segmentation) shipped in #2429; OQ#7 resolved (keep `metadata-protocol` name). rev.9 adds **D12 (honest capabilities** — discovery must not report stub/fallback services as real; the analytics fallback + dev stubs are marked honestly, not deleted) and corrects the D10 analytics note (deliberate fallback + `replaceService`, not a collision). — v12 assessment. Verified 2026-07-16: D1 (metadata-protocol extraction + back-compat re-export), D2 (core-boundary ratchet test), D9-step1 (segmented protocol interfaces) confirmed in code; D3 capability/profile contract unbuilt; D12 schema half landed but runtime enforcement missing (`http-dispatcher.ts` `svcAvailable` still hardcodes `status:'available'` for every service); D7/D10/D11-decomposition deferred as designed. +**Status**: Proposed (2026-06-28, rev. 9) — D1–D12 below. D9 step-1 (interface segmentation) shipped in #2429; OQ#7 resolved (keep `metadata-protocol` name). rev.9 adds **D12 (honest capabilities** — discovery must not report stub/fallback services as real; the analytics fallback + dev stubs are marked honestly, not deleted) and corrects the D10 analytics note (deliberate fallback + `replaceService`, not a collision). — v12 assessment. Verified 2026-07-16: D1 (metadata-protocol extraction + back-compat re-export), D2 (core-boundary ratchet test), D9-step1 (segmented protocol interfaces) confirmed in code; D3 capability/profile contract unbuilt; D12 framework side landed in #3028 (standardized `__serviceInfo` marker — OQ#11 — honored by BOTH discovery builders; `svcAvailable` no longer hardcodes `available`; analytics fallback reports `degraded`, `/realtime` no longer advertised) — the console-side consumer update ("trust only `handlerReady:true`") remains for the cross-repo window; D7/D10/D11-decomposition deferred as designed (OQ#9/OQ#10 resolved below, #3037). **Deciders**: ObjectStack Protocol Architects **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (sys_metadata overlay substrate), [ADR-0025](./0025-plugin-package-distribution.md) (plugin package distribution), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (open-core boundary), [ADR-0048](./0048-cross-package-metadata-collision.md) (package id is the addressing unit), [ADR-0066](./0066-unified-authorization-model.md) (secure-by-default, posture-gated bypass) **Consumers**: **new** `@objectstack/metadata-protocol` (receives `protocol` + `sys-metadata-repository` + `metadata-diagnostics`), `@objectstack/objectql` (loses protocol → becomes a lean data engine; keeps a back-compat re-export), `@objectstack/metadata-core` (gains the `SysMetadataEngine` interface), `@objectstack/plugin-security`, `@objectstack/plugin-sharing`, `@objectstack/spec`, and out-of-tree embedders — notably `../objectbase` (its `gateway`). @@ -204,6 +204,6 @@ import { SecurityPlugin } from '@objectstack/plugin-security'; 6. **Data-facade home** — does the `DataProtocol` impl live in the engine-adjacent transport layer / `rest`, or a small `@objectstack/protocol-data`? (It is thin and transport-shaped.) 7. **Metadata package name (post-segmentation)** — **Resolved: keep `@objectstack/metadata-protocol`** (already published; renaming has ~0 benefit and real churn). The `protocol` suffix is a low-cost naming exception — the contract is in `@objectstack/spec/api`; a README note in the package should clarify impl-vs-contract. 8. **Per-domain versioning** — once segmented, do capability protocols get independent version markers / a `getCapabilities()` discovery method? -9. **dispatcher vs rest-server overlap** — are `runtime/http-dispatcher` (~3.8k) and `rest/rest-server` (~5.1k) redundant central transport layers? Consolidate or delineate (D11). -10. **Validate multi-adapter** — write a second `IHttpServer` adapter (thin Workers/Express) to prove the port is free of Hono-isms before relying on it (D11). -11. **D12 stub marker** — standardise the self-identifying marker: reuse `_dev`, introduce `__stub: true`, or a richer per-service capability descriptor? (Decide when D12 is implemented.) +9. **dispatcher vs rest-server overlap** — **Resolved (audit, #2462): delineate, do not merge.** They partition the API surface by domain rather than duplicating it: in a standard boot REST owns data/meta/batch/import-export/search/email/forms/sharing/reports/approvals/security, the dispatcher bridge owns mcp/ai/graphql/keys/automation/actions/storage/i18n/analytics/health/ready. Only **discovery** and **packages** are genuinely double-mounted (first-registration-wins — the D11 smell to fix), and the dispatcher's `/data`/`/meta`/`/ui`/`/notifications`/`/share-links`/`/security` `dispatch()` branches are **not mounted** in a standard boot (they are live only when `HttpDispatcher` is used directly as a callable by out-of-tree adapters). D11 worklist: (1) kill or delegate the dead callable branches; (2) give packages + discovery a single owner; (3) extract per-domain handlers; (4) unify env-resolution on the injected `KernelResolver` seam. +10. **Validate multi-adapter** — **Resolved (validated, #2462): the port is free of hard Hono-isms.** A zero-dependency `node:http` reference adapter (`@objectstack/http-conformance`, private QA gate under `packages/qa/`) runs the dispatcher bridge + REST generator unchanged; a cross-adapter conformance suite (40 assertions incl. `/data` CRUD roundtrip, `:param` routing, 404/405 semantics, SSE streaming, discovery) passes identically on both adapters. Findings: the Hono adapter's Host-header backfill is adapter-local (a Fetch-API artifact, not a port leak); all remaining Hono coupling is confined to the `getRawApp()` escape hatch (metadata HMR, cloud-connection/marketplace routes, static/SPA + CORS + Server-Timing), whose consumers feature-detect and degrade. Follow-up for D11: codify the two soft extensions consumers already rely on (`res.write`/`res.end` for SSE, `getPort()`) and 404/405 semantics into the `IHttpServer` contract. +11. **D12 stub marker** — **Resolved (#2462): standard `__serviceInfo` self-descriptor** (`ServiceSelfInfoSchema` in `spec/api/discovery.zod.ts`, read via `readServiceSelfInfo()`), with plugin-dev's legacy `_dev: true` normalized to `{ status: 'stub', handlerReady: false }`. Both discovery builders honor it; the analytics fallback self-identifies as `degraded`. diff --git a/docs/adr/0085-object-semantic-roles-over-surface-hint-blocks.md b/docs/adr/0085-object-semantic-roles-over-surface-hint-blocks.md index 0526565e27..2da5b82711 100644 --- a/docs/adr/0085-object-semantic-roles-over-surface-hint-blocks.md +++ b/docs/adr/0085-object-semantic-roles-over-surface-hint-blocks.md @@ -113,5 +113,5 @@ Execution landed across both repos; differences from the proposal are noted inli - **§6 guardrails** — `@objectstack/lint` `validate-semantic-roles`: undeclared `Field.group` reference, declared-but-unreferenced group, dangling `stageField`/`highlightFields` pointers; plus `field-group-shadowed` (added by the #2548 follow-up): a group whose every visible member is hoisted into the detail highlight strip (or is the record title) renders on forms but never on detail pages. - **PR4 (legacy-path deletion)** — objectui#2546 removed the monolith `DetailView` branch in `RecordDetailView` together with the `detail.renderViaSchema` kill-switch and the `?renderViaSchema=0` debug param; schema-driven is the only path. - **Cross-surface consumption** — kanban default card fields ride `highlightFields` (objectui#2541), alongside grids/lists/detail strip. -- **Verification** — parse: `@objectstack/spec` suite; served pipeline: `packages/dogfood/test/semantic-roles.dogfood.test.ts`; real-backend browser pass over the four detail shapes (grouped / ungrouped / `stageField: false` / related-heavy): framework#3019, runbook + results in `docs/audits/2026-07-adr-0085-detail-shapes-browser-verify.md`, and a permanent Playwright spec in `examples/app-showcase/e2e/detail-shapes.spec.ts`. +- **Verification** — parse: `@objectstack/spec` suite; served pipeline: `packages/qa/dogfood/test/semantic-roles.dogfood.test.ts`; real-backend browser pass over the four detail shapes (grouped / ungrouped / `stageField: false` / related-heavy): framework#3019, runbook + results in `docs/audits/2026-07-adr-0085-detail-shapes-browser-verify.md`, and a permanent Playwright spec in `examples/app-showcase/e2e/detail-shapes.spec.ts`. - **Consequence surfaced by the browser pass** — because detail bodies hide strip fields, a fully-highlighted group silently disappears from detail pages. Judged working-as-intended (one curated list, every surface) but author-surprising — hence the `field-group-shadowed` warning and the semantic-zoo fixture keeping one non-highlighted member per group. diff --git a/docs/adr/0095-authz-kernel-tenant-layer-and-posture-ladder.md b/docs/adr/0095-authz-kernel-tenant-layer-and-posture-ladder.md index a9283cab67..9aff54b274 100644 --- a/docs/adr/0095-authz-kernel-tenant-layer-and-posture-ladder.md +++ b/docs/adr/0095-authz-kernel-tenant-layer-and-posture-ladder.md @@ -230,7 +230,7 @@ The chain is strictly serial — `D1 → D2 → D3` (#2920's B1 → B2 → B4) each step lands only behind the snapshot gate: 0. **Gate first.** The existing conformance matrix - (`packages/dogfood/test/authz-conformance.matrix.ts`) is integration-layer; + (`packages/qa/dogfood/test/authz-conformance.matrix.ts`) is integration-layer; before D1 begins, an equivalent `role × object × expected-visible-rows` snapshot must run at the **unit layer** (inside `plugin-security`'s test suite) so the loop is minutes, not a dogfood boot. @@ -315,7 +315,7 @@ each step lands only behind the snapshot gate: (`tenant_isolation` wildcard policy), `packages/core/src/security/resolve-authz-context.ts`, `packages/plugins/plugin-security/src/permission-evaluator.ts`, - `packages/dogfood/test/authz-conformance.matrix.ts`. + `packages/qa/dogfood/test/authz-conformance.matrix.ts`. - ADR-0002 (physical isolation), ADR-0066 (precedence rule this ADR makes true; superuser bypass gate), ADR-0073 (claim scoped by this ADR), ADR-0093 (`tenancy` service consumed by Layer 0). diff --git a/docs/adr/0096-execution-surface-identity-admission.md b/docs/adr/0096-execution-surface-identity-admission.md index 00f62f6ccc..52df303864 100644 --- a/docs/adr/0096-execution-surface-identity-admission.md +++ b/docs/adr/0096-execution-surface-identity-admission.md @@ -271,4 +271,4 @@ Everything else — the D3 signature migration, strict-mode-ON-in-CI per package - framework#2980 / #2981 / #2982 — the confirmed-exploitable instances the Evidence sweep surfaced (reports IDOR + scheduled-report RLS bypass; knowledge/RAG retrieval fall-open; bulk-write OWD gap) — fixed independently; this ADR's motivating evidence - `packages/plugins/plugin-security/src/security-plugin.ts` — the empty-principal seam (`:626` middleware, `:1689` getReadFilter) (D5's target) - `packages/runtime/src/http-dispatcher.ts` — `buildActionEngineFacade` (both facades), `invokeBusinessAction` -- `packages/dogfood/test/authz-conformance.matrix.ts` — the ADR-0056 D10 matrix D4 extends +- `packages/qa/dogfood/test/authz-conformance.matrix.ts` — the ADR-0056 D10 matrix D4 extends diff --git a/docs/audits/2026-07-adr-0085-detail-shapes-browser-verify.md b/docs/audits/2026-07-adr-0085-detail-shapes-browser-verify.md index 6c4bc4f74e..525636a192 100644 --- a/docs/audits/2026-07-adr-0085-detail-shapes-browser-verify.md +++ b/docs/audits/2026-07-adr-0085-detail-shapes-browser-verify.md @@ -15,7 +15,7 @@ objectui#2546 deleted the non-schema-driven `DetailView` monolith branch, so `RecordDetailView` now has exactly **one** path: an authored `PageSchema(pageType='record')` when assigned, else `buildDefaultPageSchema()` synthesis — both rendered through ``. Unit + DOM suites and the -served-metadata proof (`packages/dogfood/test/semantic-roles.dogfood.test.ts`) +served-metadata proof (`packages/qa/dogfood/test/semantic-roles.dogfood.test.ts`) cover parse and serialization, but neither renders a page in a browser against a live backend. This pass closes that gap for the four shapes the PR called out. diff --git a/eslint.config.mjs b/eslint.config.mjs index 4f3f2694e8..1f69115195 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -87,7 +87,7 @@ export default [ // backward-compat fixture (#2089) and are intentional. { files: ['examples/**/*.{ts,tsx,mts,cts}', 'packages/apps/**/*.{ts,tsx,mts,cts}'], - ignores: ['**/node_modules/**', '**/dist/**', 'packages/downstream-contract/**'], + ignores: ['**/node_modules/**', '**/dist/**', 'packages/qa/downstream-contract/**'], languageOptions: { parser: tsParser, parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, diff --git a/examples/app-showcase/src/data/objects/semantic-zoo.object.ts b/examples/app-showcase/src/data/objects/semantic-zoo.object.ts index a62b4aa838..902d941bbb 100644 --- a/examples/app-showcase/src/data/objects/semantic-zoo.object.ts +++ b/examples/app-showcase/src/data/objects/semantic-zoo.object.ts @@ -7,7 +7,7 @@ * *static*-checked by the spec suite; these two objects prove the SERVED * pipeline (defineStack → artifact → register → REST serialization) neither * strips nor mangles them. Guarded by - * `packages/dogfood/test/semantic-roles.dogfood.test.ts`. + * `packages/qa/dogfood/test/semantic-roles.dogfood.test.ts`. * * Two objects, two role postures: * - `SemanticZoo` authors the full canonical role set (highlightFields, diff --git a/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts b/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts index bf9afe0169..eabfbeddba 100644 --- a/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts +++ b/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts @@ -6,7 +6,7 @@ // expected-visible-rows` snapshot: every cell must match across the Layer 0 // extraction EXCEPT the four deltas the architect accepted (all W1-class, // all toward stronger/correcter isolation — see below). The existing -// conformance matrix (`packages/dogfood/test/authz-conformance.matrix.ts`) +// conformance matrix (`packages/qa/dogfood/test/authz-conformance.matrix.ts`) // proves this end-to-end through a real app boot (minutes). This file is the // UNIT-LAYER equivalent so the loop is seconds: it drives the real // SecurityPlugin CRUD middleware with the real seeded permission sets and diff --git a/packages/dogfood/CHANGELOG.md b/packages/qa/dogfood/CHANGELOG.md similarity index 100% rename from packages/dogfood/CHANGELOG.md rename to packages/qa/dogfood/CHANGELOG.md diff --git a/packages/dogfood/README.md b/packages/qa/dogfood/README.md similarity index 98% rename from packages/dogfood/README.md rename to packages/qa/dogfood/README.md index 7c2d30d547..db7acf1ba3 100644 --- a/packages/dogfood/README.md +++ b/packages/qa/dogfood/README.md @@ -126,7 +126,7 @@ keeps the green gate green): # 1. In plugin-security/src/security-plugin.ts, disable the pre-image check: # change `if (` to `if ( false &&` at the `(opCtx.operation === 'update' …` block. pnpm --filter @objectstack/plugin-security build # package resolves to dist -cd packages/dogfood && npx vitest run test/rls-fixture.dogfood.test.ts -t "owner-scoped" +cd packages/qa/dogfood && npx vitest run test/rls-fixture.dogfood.test.ts -t "owner-scoped" # → rls_note flips to [rls-hole]: "GET 404 yet MUTATED it by id (PATCH 200)". git checkout -- ../plugins/plugin-security/src/security-plugin.ts && pnpm --filter @objectstack/plugin-security build ``` diff --git a/packages/dogfood/package.json b/packages/qa/dogfood/package.json similarity index 100% rename from packages/dogfood/package.json rename to packages/qa/dogfood/package.json diff --git a/packages/dogfood/test/analytics-timezone.dogfood.test.ts b/packages/qa/dogfood/test/analytics-timezone.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/analytics-timezone.dogfood.test.ts rename to packages/qa/dogfood/test/analytics-timezone.dogfood.test.ts diff --git a/packages/dogfood/test/attachments-permission-matrix.dogfood.test.ts b/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/attachments-permission-matrix.dogfood.test.ts rename to packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts diff --git a/packages/dogfood/test/authz-conformance.matrix.ts b/packages/qa/dogfood/test/authz-conformance.matrix.ts similarity index 100% rename from packages/dogfood/test/authz-conformance.matrix.ts rename to packages/qa/dogfood/test/authz-conformance.matrix.ts diff --git a/packages/dogfood/test/authz-conformance.test.ts b/packages/qa/dogfood/test/authz-conformance.test.ts similarity index 99% rename from packages/dogfood/test/authz-conformance.test.ts rename to packages/qa/dogfood/test/authz-conformance.test.ts index 390835589d..a90a27ee3f 100644 --- a/packages/dogfood/test/authz-conformance.test.ts +++ b/packages/qa/dogfood/test/authz-conformance.test.ts @@ -20,8 +20,8 @@ import { checkLedger } from '@objectstack/verify'; import { AUTHZ_CONFORMANCE, type AuthzPrimitive } from './authz-conformance.matrix.js'; const HERE = dirname(fileURLToPath(import.meta.url)); -// packages/dogfood/test → repo root. -const REPO_ROOT = join(HERE, '../../..'); +// packages/qa/dogfood/test → repo root. +const REPO_ROOT = join(HERE, '../../../..'); // ── #2567 ratchet — static enumeration of anonymous-deny HTTP entry points ── // diff --git a/packages/dogfood/test/conformance-helper.test.ts b/packages/qa/dogfood/test/conformance-helper.test.ts similarity index 100% rename from packages/dogfood/test/conformance-helper.test.ts rename to packages/qa/dogfood/test/conformance-helper.test.ts diff --git a/packages/dogfood/test/controlled-by-parent.dogfood.test.ts b/packages/qa/dogfood/test/controlled-by-parent.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/controlled-by-parent.dogfood.test.ts rename to packages/qa/dogfood/test/controlled-by-parent.dogfood.test.ts diff --git a/packages/dogfood/test/dashboard-designer-roundtrip.dogfood.test.ts b/packages/qa/dogfood/test/dashboard-designer-roundtrip.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/dashboard-designer-roundtrip.dogfood.test.ts rename to packages/qa/dogfood/test/dashboard-designer-roundtrip.dogfood.test.ts diff --git a/packages/dogfood/test/delegation-of-duty.dogfood.test.ts b/packages/qa/dogfood/test/delegation-of-duty.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/delegation-of-duty.dogfood.test.ts rename to packages/qa/dogfood/test/delegation-of-duty.dogfood.test.ts diff --git a/packages/dogfood/test/derive-topology.test.ts b/packages/qa/dogfood/test/derive-topology.test.ts similarity index 100% rename from packages/dogfood/test/derive-topology.test.ts rename to packages/qa/dogfood/test/derive-topology.test.ts diff --git a/packages/dogfood/test/expression-conformance.ledger.ts b/packages/qa/dogfood/test/expression-conformance.ledger.ts similarity index 99% rename from packages/dogfood/test/expression-conformance.ledger.ts rename to packages/qa/dogfood/test/expression-conformance.ledger.ts index 0fc9357a8c..6142774bfe 100644 --- a/packages/dogfood/test/expression-conformance.ledger.ts +++ b/packages/qa/dogfood/test/expression-conformance.ledger.ts @@ -44,7 +44,7 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [ dialect: 'cel', mode: 'compile', state: 'enforced', failPolicy: 'fail-closed', enforcement: 'plugin-security/rls-compiler.ts → @objectstack/formula compileCelToFilter (legacy SQL bridged); AND-injected by security-plugin computeRlsFilter + service-analytics read-scope-sql', covers: ['security/rls.zod.ts:using'], - proof: 'packages/dogfood/test/rls-fixture.dogfood.test.ts', + proof: 'packages/qa/dogfood/test/rls-fixture.dogfood.test.ts', }, { id: 'rls-check', diff --git a/packages/dogfood/test/expression-conformance.test.ts b/packages/qa/dogfood/test/expression-conformance.test.ts similarity index 98% rename from packages/dogfood/test/expression-conformance.test.ts rename to packages/qa/dogfood/test/expression-conformance.test.ts index 99861778cb..c59a1f914e 100644 --- a/packages/dogfood/test/expression-conformance.test.ts +++ b/packages/qa/dogfood/test/expression-conformance.test.ts @@ -15,7 +15,7 @@ import { checkLedger } from '@objectstack/verify'; import { EXPRESSION_SURFACE } from './expression-conformance.ledger.js'; const HERE = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = join(HERE, '../../..'); +const REPO_ROOT = join(HERE, '../../../..'); const SPEC_SRC = join(REPO_ROOT, 'packages/spec/src'); const MODES = new Set(['compile', 'interpret']); diff --git a/packages/dogfood/test/field-zoo-roundtrip.dogfood.test.ts b/packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/field-zoo-roundtrip.dogfood.test.ts rename to packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts diff --git a/packages/dogfood/test/fixtures/attachments-fixture.ts b/packages/qa/dogfood/test/fixtures/attachments-fixture.ts similarity index 100% rename from packages/dogfood/test/fixtures/attachments-fixture.ts rename to packages/qa/dogfood/test/fixtures/attachments-fixture.ts diff --git a/packages/dogfood/test/fixtures/cbp-fixture.ts b/packages/qa/dogfood/test/fixtures/cbp-fixture.ts similarity index 100% rename from packages/dogfood/test/fixtures/cbp-fixture.ts rename to packages/qa/dogfood/test/fixtures/cbp-fixture.ts diff --git a/packages/dogfood/test/fixtures/flow-runas-fixture.ts b/packages/qa/dogfood/test/fixtures/flow-runas-fixture.ts similarity index 100% rename from packages/dogfood/test/fixtures/flow-runas-fixture.ts rename to packages/qa/dogfood/test/fixtures/flow-runas-fixture.ts diff --git a/packages/dogfood/test/fixtures/flow-touch-fixture.ts b/packages/qa/dogfood/test/fixtures/flow-touch-fixture.ts similarity index 100% rename from packages/dogfood/test/fixtures/flow-touch-fixture.ts rename to packages/qa/dogfood/test/fixtures/flow-touch-fixture.ts diff --git a/packages/dogfood/test/fixtures/rls-owner-fixture.ts b/packages/qa/dogfood/test/fixtures/rls-owner-fixture.ts similarity index 100% rename from packages/dogfood/test/fixtures/rls-owner-fixture.ts rename to packages/qa/dogfood/test/fixtures/rls-owner-fixture.ts diff --git a/packages/dogfood/test/flow-node.dogfood.test.ts b/packages/qa/dogfood/test/flow-node.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/flow-node.dogfood.test.ts rename to packages/qa/dogfood/test/flow-node.dogfood.test.ts diff --git a/packages/dogfood/test/flow-runas-schedule.dogfood.test.ts b/packages/qa/dogfood/test/flow-runas-schedule.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/flow-runas-schedule.dogfood.test.ts rename to packages/qa/dogfood/test/flow-runas-schedule.dogfood.test.ts diff --git a/packages/dogfood/test/flow-runas.dogfood.test.ts b/packages/qa/dogfood/test/flow-runas.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/flow-runas.dogfood.test.ts rename to packages/qa/dogfood/test/flow-runas.dogfood.test.ts diff --git a/packages/dogfood/test/flow-trigger-conformance.ledger.ts b/packages/qa/dogfood/test/flow-trigger-conformance.ledger.ts similarity index 100% rename from packages/dogfood/test/flow-trigger-conformance.ledger.ts rename to packages/qa/dogfood/test/flow-trigger-conformance.ledger.ts diff --git a/packages/dogfood/test/flow-trigger-conformance.test.ts b/packages/qa/dogfood/test/flow-trigger-conformance.test.ts similarity index 97% rename from packages/dogfood/test/flow-trigger-conformance.test.ts rename to packages/qa/dogfood/test/flow-trigger-conformance.test.ts index 325676b2c3..4354b62c6e 100644 --- a/packages/dogfood/test/flow-trigger-conformance.test.ts +++ b/packages/qa/dogfood/test/flow-trigger-conformance.test.ts @@ -15,7 +15,7 @@ import { checkLedger } from '@objectstack/verify'; import { FLOW_TRIGGER_SURFACE } from './flow-trigger-conformance.ledger.js'; const HERE = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = join(HERE, '../../..'); +const REPO_ROOT = join(HERE, '../../../..'); const FLOW_ZOD = join(REPO_ROOT, 'packages/spec/src/automation/flow.zod.ts'); /** Re-discover every `Flow.type` enum value (the one tagged `.describe('Flow type')`). */ diff --git a/packages/dogfood/test/form-self-auth.dogfood.test.ts b/packages/qa/dogfood/test/form-self-auth.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/form-self-auth.dogfood.test.ts rename to packages/qa/dogfood/test/form-self-auth.dogfood.test.ts diff --git a/packages/dogfood/test/hook-error-format.dogfood.test.ts b/packages/qa/dogfood/test/hook-error-format.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/hook-error-format.dogfood.test.ts rename to packages/qa/dogfood/test/hook-error-format.dogfood.test.ts diff --git a/packages/dogfood/test/me-apps-and-everyone-baseline.dogfood.test.ts b/packages/qa/dogfood/test/me-apps-and-everyone-baseline.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/me-apps-and-everyone-baseline.dogfood.test.ts rename to packages/qa/dogfood/test/me-apps-and-everyone-baseline.dogfood.test.ts diff --git a/packages/dogfood/test/membership-reconciler.dogfood.test.ts b/packages/qa/dogfood/test/membership-reconciler.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/membership-reconciler.dogfood.test.ts rename to packages/qa/dogfood/test/membership-reconciler.dogfood.test.ts diff --git a/packages/dogfood/test/meta-types-create-seed.dogfood.test.ts b/packages/qa/dogfood/test/meta-types-create-seed.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/meta-types-create-seed.dogfood.test.ts rename to packages/qa/dogfood/test/meta-types-create-seed.dogfood.test.ts diff --git a/packages/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts b/packages/qa/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts rename to packages/qa/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts diff --git a/packages/dogfood/test/package-first-authoring.dogfood.test.ts b/packages/qa/dogfood/test/package-first-authoring.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/package-first-authoring.dogfood.test.ts rename to packages/qa/dogfood/test/package-first-authoring.dogfood.test.ts diff --git a/packages/dogfood/test/primary-bu-projection.dogfood.test.ts b/packages/qa/dogfood/test/primary-bu-projection.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/primary-bu-projection.dogfood.test.ts rename to packages/qa/dogfood/test/primary-bu-projection.dogfood.test.ts diff --git a/packages/dogfood/test/read-coercion-conformance.test.ts b/packages/qa/dogfood/test/read-coercion-conformance.test.ts similarity index 100% rename from packages/dogfood/test/read-coercion-conformance.test.ts rename to packages/qa/dogfood/test/read-coercion-conformance.test.ts diff --git a/packages/dogfood/test/rls-fixture.dogfood.test.ts b/packages/qa/dogfood/test/rls-fixture.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/rls-fixture.dogfood.test.ts rename to packages/qa/dogfood/test/rls-fixture.dogfood.test.ts diff --git a/packages/dogfood/test/rls-multitenant.dogfood.test.ts b/packages/qa/dogfood/test/rls-multitenant.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/rls-multitenant.dogfood.test.ts rename to packages/qa/dogfood/test/rls-multitenant.dogfood.test.ts diff --git a/packages/dogfood/test/rls-runner.test.ts b/packages/qa/dogfood/test/rls-runner.test.ts similarity index 100% rename from packages/dogfood/test/rls-runner.test.ts rename to packages/qa/dogfood/test/rls-runner.test.ts diff --git a/packages/dogfood/test/semantic-roles.dogfood.test.ts b/packages/qa/dogfood/test/semantic-roles.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/semantic-roles.dogfood.test.ts rename to packages/qa/dogfood/test/semantic-roles.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-agent-intersection.dogfood.test.ts b/packages/qa/dogfood/test/showcase-agent-intersection.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-agent-intersection.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-agent-intersection.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-agent-scope-ceiling.dogfood.test.ts b/packages/qa/dogfood/test/showcase-agent-scope-ceiling.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-agent-scope-ceiling.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-agent-scope-ceiling.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts b/packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-anonymous-deny.dogfood.test.ts b/packages/qa/dogfood/test/showcase-anonymous-deny.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-anonymous-deny.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-anonymous-deny.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.ts b/packages/qa/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-d3-d4-capabilities.dogfood.test.ts b/packages/qa/dogfood/test/showcase-d3-d4-capabilities.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-d3-d4-capabilities.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-d3-d4-capabilities.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-d7-default-profile.dogfood.test.ts b/packages/qa/dogfood/test/showcase-d7-default-profile.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-d7-default-profile.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-d7-default-profile.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-declarative-mcp.dogfood.test.ts b/packages/qa/dogfood/test/showcase-declarative-mcp.dogfood.test.ts similarity index 97% rename from packages/dogfood/test/showcase-declarative-mcp.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-declarative-mcp.dogfood.test.ts index b7e6edc632..2d654c6a1c 100644 --- a/packages/dogfood/test/showcase-declarative-mcp.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-declarative-mcp.dogfood.test.ts @@ -23,7 +23,7 @@ import { ConnectorMcpPlugin } from '@objectstack/connector-mcp'; import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi'; import { ConnectorRestPlugin } from '@objectstack/connector-rest'; -const SHOWCASE_DIR = fileURLToPath(new URL('../../../examples/app-showcase/', import.meta.url)); +const SHOWCASE_DIR = fileURLToPath(new URL('../../../../examples/app-showcase/', import.meta.url)); interface ConnectorDescriptor { name: string; diff --git a/packages/dogfood/test/showcase-declarative-rbac-seeding.dogfood.test.ts b/packages/qa/dogfood/test/showcase-declarative-rbac-seeding.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-declarative-rbac-seeding.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-declarative-rbac-seeding.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-default-profile.dogfood.test.ts b/packages/qa/dogfood/test/showcase-default-profile.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-default-profile.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-default-profile.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-external-autoconnect.dogfood.test.ts b/packages/qa/dogfood/test/showcase-external-autoconnect.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-external-autoconnect.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-external-autoconnect.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-invoice-cbp.dogfood.test.ts b/packages/qa/dogfood/test/showcase-invoice-cbp.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-invoice-cbp.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-invoice-cbp.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-invoice-seed-isolation.dogfood.test.ts b/packages/qa/dogfood/test/showcase-invoice-seed-isolation.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-invoice-seed-isolation.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-invoice-seed-isolation.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-permission-projection.dogfood.test.ts b/packages/qa/dogfood/test/showcase-permission-projection.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-permission-projection.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-permission-projection.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-permission-seeding.dogfood.test.ts b/packages/qa/dogfood/test/showcase-permission-seeding.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-permission-seeding.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-permission-seeding.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-permission-zoo.dogfood.test.ts b/packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-permission-zoo.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-private-owd.dogfood.test.ts b/packages/qa/dogfood/test/showcase-private-owd.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-private-owd.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-private-owd.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-public-form.dogfood.test.ts b/packages/qa/dogfood/test/showcase-public-form.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-public-form.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-public-form.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-public-read-owd.dogfood.test.ts b/packages/qa/dogfood/test/showcase-public-read-owd.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-public-read-owd.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-public-read-owd.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts b/packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-scope-depth.dogfood.test.ts b/packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-scope-depth.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts diff --git a/packages/dogfood/test/showcase-static-readonly.dogfood.test.ts b/packages/qa/dogfood/test/showcase-static-readonly.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/showcase-static-readonly.dogfood.test.ts rename to packages/qa/dogfood/test/showcase-static-readonly.dogfood.test.ts diff --git a/packages/dogfood/test/single-tenant-identity-create.dogfood.test.ts b/packages/qa/dogfood/test/single-tenant-identity-create.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/single-tenant-identity-create.dogfood.test.ts rename to packages/qa/dogfood/test/single-tenant-identity-create.dogfood.test.ts diff --git a/packages/dogfood/test/storage-growth.dogfood.test.ts b/packages/qa/dogfood/test/storage-growth.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/storage-growth.dogfood.test.ts rename to packages/qa/dogfood/test/storage-growth.dogfood.test.ts diff --git a/packages/dogfood/test/two-doors-permission.dogfood.test.ts b/packages/qa/dogfood/test/two-doors-permission.dogfood.test.ts similarity index 100% rename from packages/dogfood/test/two-doors-permission.dogfood.test.ts rename to packages/qa/dogfood/test/two-doors-permission.dogfood.test.ts diff --git a/packages/dogfood/test/validation-conformance.ledger.ts b/packages/qa/dogfood/test/validation-conformance.ledger.ts similarity index 100% rename from packages/dogfood/test/validation-conformance.ledger.ts rename to packages/qa/dogfood/test/validation-conformance.ledger.ts diff --git a/packages/dogfood/test/validation-conformance.test.ts b/packages/qa/dogfood/test/validation-conformance.test.ts similarity index 97% rename from packages/dogfood/test/validation-conformance.test.ts rename to packages/qa/dogfood/test/validation-conformance.test.ts index ecf4e42eee..2fd0ff18a0 100644 --- a/packages/dogfood/test/validation-conformance.test.ts +++ b/packages/qa/dogfood/test/validation-conformance.test.ts @@ -16,7 +16,7 @@ import { checkLedger } from '@objectstack/verify'; import { VALIDATION_SURFACE } from './validation-conformance.ledger.js'; const HERE = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = join(HERE, '../../..'); +const REPO_ROOT = join(HERE, '../../../..'); const VALIDATION_ZOD = join(REPO_ROOT, 'packages/spec/src/data/validation.zod.ts'); /** Re-discover every `validations` union rule type from its discriminator. */ diff --git a/packages/dogfood/tsconfig.json b/packages/qa/dogfood/tsconfig.json similarity index 100% rename from packages/dogfood/tsconfig.json rename to packages/qa/dogfood/tsconfig.json diff --git a/packages/downstream-contract/CHANGELOG.md b/packages/qa/downstream-contract/CHANGELOG.md similarity index 100% rename from packages/downstream-contract/CHANGELOG.md rename to packages/qa/downstream-contract/CHANGELOG.md diff --git a/packages/downstream-contract/README.md b/packages/qa/downstream-contract/README.md similarity index 100% rename from packages/downstream-contract/README.md rename to packages/qa/downstream-contract/README.md diff --git a/packages/downstream-contract/package.json b/packages/qa/downstream-contract/package.json similarity index 100% rename from packages/downstream-contract/package.json rename to packages/qa/downstream-contract/package.json diff --git a/packages/downstream-contract/src/account.object.ts b/packages/qa/downstream-contract/src/account.object.ts similarity index 100% rename from packages/downstream-contract/src/account.object.ts rename to packages/qa/downstream-contract/src/account.object.ts diff --git a/packages/downstream-contract/src/account.view.ts b/packages/qa/downstream-contract/src/account.view.ts similarity index 100% rename from packages/downstream-contract/src/account.view.ts rename to packages/qa/downstream-contract/src/account.view.ts diff --git a/packages/downstream-contract/src/additional-domains.fixtures.ts b/packages/qa/downstream-contract/src/additional-domains.fixtures.ts similarity index 100% rename from packages/downstream-contract/src/additional-domains.fixtures.ts rename to packages/qa/downstream-contract/src/additional-domains.fixtures.ts diff --git a/packages/downstream-contract/src/log-call.action.ts b/packages/qa/downstream-contract/src/log-call.action.ts similarity index 100% rename from packages/downstream-contract/src/log-call.action.ts rename to packages/qa/downstream-contract/src/log-call.action.ts diff --git a/packages/downstream-contract/src/modern.action.ts b/packages/qa/downstream-contract/src/modern.action.ts similarity index 100% rename from packages/downstream-contract/src/modern.action.ts rename to packages/qa/downstream-contract/src/modern.action.ts diff --git a/packages/downstream-contract/src/pipeline.report.ts b/packages/qa/downstream-contract/src/pipeline.report.ts similarity index 100% rename from packages/downstream-contract/src/pipeline.report.ts rename to packages/qa/downstream-contract/src/pipeline.report.ts diff --git a/packages/downstream-contract/src/stack.ts b/packages/qa/downstream-contract/src/stack.ts similarity index 100% rename from packages/downstream-contract/src/stack.ts rename to packages/qa/downstream-contract/src/stack.ts diff --git a/packages/downstream-contract/src/welcome.page.ts b/packages/qa/downstream-contract/src/welcome.page.ts similarity index 100% rename from packages/downstream-contract/src/welcome.page.ts rename to packages/qa/downstream-contract/src/welcome.page.ts diff --git a/packages/downstream-contract/test/contract.test.ts b/packages/qa/downstream-contract/test/contract.test.ts similarity index 100% rename from packages/downstream-contract/test/contract.test.ts rename to packages/qa/downstream-contract/test/contract.test.ts diff --git a/packages/downstream-contract/tsconfig.json b/packages/qa/downstream-contract/tsconfig.json similarity index 100% rename from packages/downstream-contract/tsconfig.json rename to packages/qa/downstream-contract/tsconfig.json diff --git a/packages/qa/http-conformance/LICENSE b/packages/qa/http-conformance/LICENSE new file mode 100644 index 0000000000..16bc23f404 --- /dev/null +++ b/packages/qa/http-conformance/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute + must include a readable copy of the attribution notices + contained within such NOTICE file, excluding those notices + that do not pertain to any part of the Derivative Works, + in at least one of the following places: within a NOTICE + text file distributed as part of the Derivative Works; within + the Source form or documentation, if provided along with + the Derivative Works; or, within a display generated by the + Derivative Works, if and wherever such third-party notices + normally appear. The contents of the NOTICE file are for + informational purposes only and do not modify the License. + You may add Your own attribution notices within Derivative + Works that You distribute, alongside or as an addendum to + the NOTICE text from the Work, provided that such additional + attribution notices cannot be construed as modifying the + License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 ObjectStack + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/qa/http-conformance/README.md b/packages/qa/http-conformance/README.md new file mode 100644 index 0000000000..c624663205 --- /dev/null +++ b/packages/qa/http-conformance/README.md @@ -0,0 +1,52 @@ +# @objectstack/http-conformance + +**HTTP transport-port conformance gate** — private, never published. +Part of the `packages/qa/` family of verification gates (alongside the +dogfood regression gate and the downstream-contract compatibility gate). + +## What this pins + +The transport layer is ports-and-adapters (ADR-0076 D11): route consumers — +the dispatcher bridge (`createDispatcherPlugin`) and the REST route +generator (`createRestApiPlugin`) — register against the `IHttpServer` port +(`@objectstack/spec/contracts`), and concrete servers implement it. But only +the Hono adapter ever existed, so "the port is framework-agnostic" was an +unproven claim (ADR-0076 OQ#10, issue #2462). + +This package is the proof, kept as a permanent CI gate: + +- `src/adapter.ts` — `NodeHttpServer`, a **reference implementation** of the + port on raw `node:http` with zero dependencies beyond `@objectstack/core`. + No framework means nothing papers over a port gap: if a consumer needs + anything Hono-specific, this implementation cannot provide it and the + suite breaks. +- `src/conformance.integration.test.ts` — boots the real framework stacks + (dispatcher bridge + REST generator + ObjectQL + memory driver) on **both** + this adapter and `plugin-hono-server`, over real sockets, and asserts + identical observable behavior: full `/data` CRUD roundtrip, `/meta` reads, + `:param` routing, 404/405-with-`Allow` semantics, SSE streaming, discovery, + plus a probe-for-probe response-shape parity matrix. + +If a framework-ism ever leaks into a route consumer, the node half of this +suite is what breaks. + +## The port contract, as exercised here + +`IHttpServer` proper, plus the two soft extensions consumers feature-detect +(candidates for formalizing into the contract at the D11 window): + +- **SSE streaming** — `res.write` / `res.end`, used by AI routes. +- **`getPort()`** — port discovery after `listen(0)`. + +Deliberately absent (each a known escape hatch whose consumers +feature-detect and degrade): `getRawApp()` (Hono-specific — metadata HMR, +cloud-connection routes, static/SPA + CORS + Server-Timing), `mount()`, +multipart parsing (binary bodies stay raw behind the lazy `req.rawBody()`). + +## Not a product server + +`NodeHttpServer` is a validation instrument. Deployments use +`@objectstack/plugin-hono-server`; a user-facing second adapter +(Express/Workers) is a separate product decision for the ADR-0076 +cross-repo window — when it lands, plug it into this suite's `ADAPTERS` +array and the same assertions validate it for free. diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json new file mode 100644 index 0000000000..89779e46b3 --- /dev/null +++ b/packages/qa/http-conformance/package.json @@ -0,0 +1,38 @@ +{ + "name": "@objectstack/http-conformance", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "description": "HTTP transport-port conformance gate (ADR-0076 D11/OQ#10, #2462) — a zero-dependency node:http reference implementation of IHttpServer plus a cross-adapter suite that boots the dispatcher bridge and REST generator on it AND on plugin-hono-server, pinning that the port stays free of framework-isms. Not published; validation instrument, not a product server.", + "type": "module", + "scripts": { + "test": "vitest run" + }, + "dependencies": { + "@objectstack/core": "workspace:*" + }, + "devDependencies": { + "@objectstack/driver-memory": "workspace:*", + "@objectstack/objectql": "workspace:*", + "@objectstack/plugin-hono-server": "workspace:*", + "@objectstack/runtime": "workspace:*", + "@types/node": "^26.1.1", + "typescript": "^6.0.3", + "vitest": "^4.1.10" + }, + "keywords": [ + "objectstack", + "qa", + "conformance", + "http-server", + "adapter" + ], + "author": "ObjectStack", + "repository": { + "type": "git", + "url": "https://github.com/objectstack-ai/framework.git", + "directory": "packages/qa/http-conformance" + }, + "homepage": "https://objectstack.ai/docs", + "bugs": "https://github.com/objectstack-ai/framework/issues" +} diff --git a/packages/qa/http-conformance/src/adapter.test.ts b/packages/qa/http-conformance/src/adapter.test.ts new file mode 100644 index 0000000000..19e3ee49f3 --- /dev/null +++ b/packages/qa/http-conformance/src/adapter.test.ts @@ -0,0 +1,157 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { NodeHttpServer, compileRoute } from './adapter.js'; + +describe('compileRoute', () => { + it('matches literal paths exactly', () => { + const { regex } = compileRoute('/api/v1/health'); + expect(regex.test('/api/v1/health')).toBe(true); + expect(regex.test('/api/v1/health/x')).toBe(false); + expect(regex.test('/api/v1')).toBe(false); + }); + + it('captures :param segments', () => { + const { regex, keys } = compileRoute('/api/v1/i18n/labels/:object/:locale'); + expect(keys).toEqual(['object', 'locale']); + const m = regex.exec('/api/v1/i18n/labels/task/zh-CN'); + expect(m?.[1]).toBe('task'); + expect(m?.[2]).toBe('zh-CN'); + expect(regex.test('/api/v1/i18n/labels/task')).toBe(false); + }); + + it('supports the trailing * wildcard across slashes', () => { + const { regex } = compileRoute('/api/v1/ai/*'); + expect(regex.test('/api/v1/ai/chat')).toBe(true); + expect(regex.test('/api/v1/ai/agents/a1/run')).toBe(true); + }); + + it('escapes regex metacharacters in literals', () => { + const { regex } = compileRoute('/.well-known/objectstack'); + expect(regex.test('/.well-known/objectstack')).toBe(true); + expect(regex.test('/xwell-known/objectstack')).toBe(false); + }); +}); + +describe('NodeHttpServer (live socket)', () => { + let server: NodeHttpServer; + let base: string; + + beforeAll(async () => { + server = new NodeHttpServer(0); + + server.get('/echo/:id', (req, res) => { + res.json({ id: req.params.id, q: req.query, path: req.path }); + }); + server.post('/json', (req, res) => { res.status(201); res.json({ got: req.body }); }); + server.post('/raw', async (req, res) => { + const buf = await req.rawBody!(); + res.json({ bytes: buf.length, firstByte: buf[0] ?? null }); + }); + server.get('/no-content', (req, res) => { res.status(204); (res as any).end(); }); + server.get('/boom', () => { throw Object.assign(new Error('kaboom'), { statusCode: 418 }); }); + server.get('/silent', () => { /* resolves without responding */ }); + server.get('/sse', (req, res) => { + const r = res as any; + res.status(200); + res.header('Content-Type', 'text/event-stream'); + r.write('data: one\n\n'); + r.write('data: two\n\n'); + r.end(); + }); + + await server.listen(0); + base = `http://127.0.0.1:${server.getPort()}`; + }); + + afterAll(async () => { await server.close(); }); + + it('routes :param and multi-value query', async () => { + const res = await fetch(`${base}/echo/42?a=1&b=x&b=y`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.id).toBe('42'); + expect(body.q).toEqual({ a: '1', b: ['x', 'y'] }); + expect(body.path).toBe('/echo/42'); + }); + + it('parses JSON bodies and honors res.status()', async () => { + const res = await fetch(`${base}/json`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hello: 'world' }), + }); + expect(res.status).toBe(201); + expect((await res.json()).got).toEqual({ hello: 'world' }); + }); + + it('leaves binary bodies unparsed but readable via rawBody()', async () => { + const res = await fetch(`${base}/raw`, { + method: 'POST', + headers: { 'Content-Type': 'application/octet-stream' }, + body: new Uint8Array([7, 8, 9]), + }); + expect(await res.json()).toEqual({ bytes: 3, firstByte: 7 }); + }); + + it('supports body-less 204 via res.end()', async () => { + const res = await fetch(`${base}/no-content`); + expect(res.status).toBe(204); + expect(await res.text()).toBe(''); + }); + + it('maps thrown errors with statusCode', async () => { + const res = await fetch(`${base}/boom`); + expect(res.status).toBe(418); + expect((await res.json()).error).toBe('kaboom'); + }); + + it('500s when a handler resolves without responding', async () => { + const res = await fetch(`${base}/silent`); + expect(res.status).toBe(500); + expect((await res.json()).error).toBe('No response from handler'); + }); + + it('streams SSE via the res.write/res.end extension', async () => { + const res = await fetch(`${base}/sse`); + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toContain('text/event-stream'); + expect(await res.text()).toBe('data: one\n\ndata: two\n\n'); + }); + + it('404s unknown paths with the shared not-found body', async () => { + const res = await fetch(`${base}/nope`); + expect(res.status).toBe(404); + expect(await res.json()).toEqual({ error: 'Not found' }); + }); + + it('405s a method mismatch with an accurate Allow header', async () => { + const res = await fetch(`${base}/json`, { method: 'GET' }); + expect(res.status).toBe(405); + expect(res.headers.get('allow')).toBe('POST'); + const body = await res.json(); + expect(body.code).toBe('METHOD_NOT_ALLOWED'); + expect(body.allowed).toEqual(['POST']); + }); + + it('answers HEAD from GET routes without a body', async () => { + const res = await fetch(`${base}/echo/1`, { method: 'HEAD' }); + expect(res.status).toBe(200); + expect(await res.text()).toBe(''); + }); + + it('drops prototype-polluting query keys (CodeQL: remote property injection)', async () => { + const res = await fetch(`${base}/echo/1?__proto__=polluted&constructor=x&a=ok`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.q).toEqual({ a: 'ok' }); + // The global Object prototype must be untouched. + expect(({} as any).polluted).toBeUndefined(); + }); + + it('exposes the Host header natively (no backfill needed)', async () => { + server.get('/host', (req, res) => { res.json({ host: req.headers.host }); }); + const res = await fetch(`${base}/host`); + expect((await res.json()).host).toBe(`127.0.0.1:${server.getPort()}`); + }); +}); diff --git a/packages/qa/http-conformance/src/adapter.ts b/packages/qa/http-conformance/src/adapter.ts new file mode 100644 index 0000000000..1120051726 --- /dev/null +++ b/packages/qa/http-conformance/src/adapter.ts @@ -0,0 +1,371 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { createServer, Server, IncomingMessage, ServerResponse } from 'node:http'; +import { + IHttpServer, + IHttpRequest, + IHttpResponse, + RouteHandler, + Middleware, +} from '@objectstack/core'; + +/** + * NodeHttpServer — a thin `IHttpServer` implementation on raw `node:http`, + * with **zero** framework dependencies. + * + * This is the second adapter behind the transport port (ADR-0076 D11 / + * OQ#10, #2462): its purpose is to *prove* that everything registered + * through `IHttpServer` (the dispatcher bridge, the REST route generator, + * package routes, …) runs unchanged on a non-Hono server. Anything the + * framework needs beyond the `IHttpServer` contract is, by definition, a + * port leak — this adapter deliberately implements the contract and the two + * documented soft extensions only: + * + * - **Streaming** (`res.write` / `res.end`): consumers feature-detect these + * for SSE (AI routes); on node:http they map directly onto the native + * response stream. + * - **`getPort()`**: used by boot code/tests to discover the OS-assigned + * port after `listen(0)`. + * + * Deliberately NOT implemented (each one is a known escape hatch whose + * consumers feature-detect and degrade): + * - `getRawApp()` — Hono-specific; metadata HMR, cloud-connection routes and + * the hono-plugin's own static/SPA mounts use it and will log-and-skip. + * - `mount()` — Hono sub-app composition. + * + * Route patterns support the same subset the framework registers (see + * plugin-hono-server/src/route-pattern.ts): `:param` segments and a trailing + * `*` wildcard. Matching is registration-order first-match-wins, mirroring + * the documented behavior of the primary adapter (rest-server.ts relies on + * exactly this). + */ + +interface CompiledRoute { + method: string; + pattern: string; + regex: RegExp; + /** `:param` names, in order of appearance. */ + keys: string[]; + handler: RouteHandler; +} + +/** Strip a single trailing slash so `/a/b` and `/a/b/` match the same pattern. */ +function normalize(path: string): string { + if (path.length > 1 && path.endsWith('/')) return path.slice(0, -1); + return path; +} + +const ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g; + +/** + * Compile a route pattern (`:param` + trailing `*` subset) into an anchored + * regex with capture groups for named params. + */ +export function compileRoute(pattern: string): { regex: RegExp; keys: string[] } { + const keys: string[] = []; + const body = normalize(pattern) + .split('/') + .map((segment) => { + if (segment.startsWith(':') && segment.length > 1) { + keys.push(segment.slice(1)); + return '([^/]+)'; + } + if (segment === '*') { + // Trailing wildcard — matches the rest of the path, including '/'. + return '.*'; + } + return segment.replace(ESCAPE_REGEX, '\\$&'); + }) + .join('/'); + return { regex: new RegExp(`^${body}$`), keys }; +} + +/** Body-parsing content types we consume eagerly; everything else stays raw. */ +function isEagerlyParsed(contentType: string): boolean { + return ( + contentType.includes('application/json') || + contentType.includes('application/x-www-form-urlencoded') || + contentType.startsWith('text/') + ); +} + +function readBody(req: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + req.on('data', (c) => chunks.push(c)); + req.on('end', () => resolve(Buffer.concat(chunks))); + req.on('error', reject); + }); +} + +export class NodeHttpServer implements IHttpServer { + private routes: CompiledRoute[] = []; + private middlewares: Middleware[] = []; + private server: Server | undefined; + private listeningPort: number | undefined; + + constructor( + private port: number = 3000, + /** + * Max time (ms) to let in-flight requests drain on `close()` before + * force-closing the remainder. Mirrors the Hono adapter's drain + * window; kept well under the kernel's 60s shutdownTimeout. + */ + private drainTimeoutMs: number = 10_000, + ) {} + + private register(method: string, pattern: string, handler: RouteHandler) { + const { regex, keys } = compileRoute(pattern); + this.routes.push({ method, pattern, regex, keys, handler }); + } + + get(path: string, handler: RouteHandler) { this.register('GET', path, handler); } + post(path: string, handler: RouteHandler) { this.register('POST', path, handler); } + put(path: string, handler: RouteHandler) { this.register('PUT', path, handler); } + delete(path: string, handler: RouteHandler) { this.register('DELETE', path, handler); } + patch(path: string, handler: RouteHandler) { this.register('PATCH', path, handler); } + + use(pathOrHandler: string | Middleware, handler?: Middleware) { + // Same degenerate semantics as the Hono adapter's wrapper: middleware + // is invoked as a sequential hook and the chain always continues, + // whether or not it called next(). The path argument is ignored there + // too (the wrapper passes empty req/res), so we keep behavior aligned. + const mw = typeof pathOrHandler === 'function' ? pathOrHandler : handler; + if (mw) this.middlewares.push(mw); + } + + /** + * The HTTP methods registered for a concrete request path, ignoring the + * request's own method — used to answer `405 Method Not Allowed` with an + * accurate `Allow` header instead of an opaque 404. `HEAD` is implied by + * `GET`, matching the primary adapter. + */ + allowedMethodsForPath(path: string): string[] { + const normalized = normalize(path); + const methods = new Set(); + for (const route of this.routes) { + if (route.regex.test(normalized)) methods.add(route.method); + } + if (methods.has('GET')) methods.add('HEAD'); + return Array.from(methods).sort(); + } + + private match(method: string, path: string): { route: CompiledRoute; params: Record } | undefined { + const normalized = normalize(path); + // HEAD is answered by GET handlers (body suppressed by node core for + // HEAD automatically when we end without a body; JSON bodies are + // dropped in the response wrapper below). + const effective = method === 'HEAD' ? 'GET' : method; + for (const route of this.routes) { + if (route.method !== effective) continue; + const m = route.regex.exec(normalized); + if (!m) continue; + // Own-data-property construction for symmetry with `query` — keys + // come from the registered pattern (developer-controlled), values + // from the URL. + const params = Object.fromEntries( + route.keys.map((k, i) => [k, decodeURIComponent(m[i + 1])]), + ) as Record; + return { route, params }; + } + return undefined; + } + + private async handleRequest(nodeReq: IncomingMessage, nodeRes: ServerResponse) { + const method = (nodeReq.method || 'GET').toUpperCase(); + const url = new URL(nodeReq.url || '/', 'http://internal'); + const path = url.pathname; + + const matched = this.match(method, path); + if (!matched) { + // Distinguish "path exists under another verb" (405 + Allow) from a + // genuine 404 — same semantics as the primary adapter's notFound. + const allowed = this.allowedMethodsForPath(path); + if (allowed.length > 0 && !allowed.includes(method)) { + nodeRes.statusCode = 405; + nodeRes.setHeader('Allow', allowed.join(', ')); + nodeRes.setHeader('Content-Type', 'application/json; charset=utf-8'); + nodeRes.end(JSON.stringify({ + error: 'Method Not Allowed', + code: 'METHOD_NOT_ALLOWED', + message: `${method} is not supported for ${path}. Allowed: ${allowed.join(', ')}.`, + method, + path, + allowed, + })); + return; + } + nodeRes.statusCode = 404; + nodeRes.setHeader('Content-Type', 'application/json; charset=utf-8'); + nodeRes.end(JSON.stringify({ error: 'Not found' })); + return; + } + + // ── Query params (multi-value aware) ──────────────────────────────── + // The property names come straight from the request URL, so this map + // must never be built via dynamic property writes (`obj[key] = …`) — + // `?__proto__=…` would walk the prototype chain (CodeQL: remote + // property injection). `Object.fromEntries` creates own data + // properties only; the dangerous keys are dropped outright as + // defense in depth. + const seen = new Set(); + const queryEntries: Array<[string, string | string[]]> = []; + for (const key of url.searchParams.keys()) { + if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue; + if (seen.has(key)) continue; + seen.add(key); + const all = url.searchParams.getAll(key); + queryEntries.push([key, all.length > 1 ? all : all[0]]); + } + const query = Object.fromEntries(queryEntries) as Record; + + // ── Body ──────────────────────────────────────────────────────────── + // JSON / urlencoded / text bodies are consumed eagerly (like the + // primary adapter); anything else (octet-stream, multipart) stays on + // the socket for the lazy `rawBody()` accessor so binary uploads pay + // no parsing cost. + const contentType = String(nodeReq.headers['content-type'] || ''); + let body: any = {}; + let bufferedBody: Buffer | undefined; + if (method !== 'GET' && method !== 'HEAD' && isEagerlyParsed(contentType)) { + bufferedBody = await readBody(nodeReq); + const text = bufferedBody.toString('utf8'); + if (contentType.includes('application/json')) { + try { body = text ? JSON.parse(text) : {}; } catch { body = {}; } + } else if (contentType.includes('application/x-www-form-urlencoded')) { + body = Object.fromEntries(new URLSearchParams(text)); + } else { + body = text; + } + } + + // node:http exposes headers as a lowercased record already, Host + // included — no backfill needed (the Fetch-API Host backfill in the + // Hono adapter is adapter-local, not a port requirement). + const req: IHttpRequest = { + params: matched.params, + query, + body, + headers: nodeReq.headers as Record, + method, + path, + rawBody: async () => bufferedBody ?? (bufferedBody = await readBody(nodeReq)), + }; + + // ── Response wrapper (contract + streaming extension) ─────────────── + const isHead = method === 'HEAD'; + let streaming = false; + const res: IHttpResponse & { write: (chunk: string | Uint8Array) => void; end: () => void } = { + status: (code: number) => { nodeRes.statusCode = code; return res; }, + header: (name: string, value: string | string[]) => { nodeRes.setHeader(name, value); return res; }, + json: (data: any) => { + if (nodeRes.writableEnded) return; + if (!nodeRes.headersSent) nodeRes.setHeader('Content-Type', 'application/json; charset=utf-8'); + nodeRes.end(isHead ? undefined : JSON.stringify(data)); + }, + send: (data: string | Uint8Array | ArrayBuffer) => { + if (nodeRes.writableEnded) return; + if (typeof data === 'string') { + if (!nodeRes.headersSent) nodeRes.setHeader('Content-Type', 'text/html; charset=utf-8'); + nodeRes.end(isHead ? undefined : data); + } else { + if (!nodeRes.headersSent) nodeRes.setHeader('Content-Type', 'application/octet-stream'); + const buf = data instanceof ArrayBuffer ? Buffer.from(data) : Buffer.from(data.buffer, data.byteOffset, data.byteLength); + if (isHead) nodeRes.end(); + else nodeRes.end(buf); + } + }, + // Streaming extension (SSE): map straight onto the native stream. + write: (chunk: string | Uint8Array) => { + streaming = true; + if (!nodeRes.writableEnded) nodeRes.write(chunk); + }, + end: () => { + if (!nodeRes.writableEnded) nodeRes.end(); + }, + }; + + try { + for (const mw of this.middlewares) { + let advanced = false; + await mw(req, res as IHttpResponse, () => { advanced = true; }); + void advanced; // chain always continues — parity with the primary adapter + } + await matched.route.handler(req, res as IHttpResponse); + // Handler resolved without producing a response and without + // starting a stream (SSE handlers legitimately resolve while the + // stream is still open — dispatcher drains it in the background). + if (!nodeRes.writableEnded && !streaming) { + nodeRes.statusCode = 500; + nodeRes.setHeader('Content-Type', 'application/json; charset=utf-8'); + nodeRes.end(JSON.stringify({ error: 'No response from handler' })); + } + } catch (err: any) { + if (!nodeRes.writableEnded) { + if (!nodeRes.headersSent) { + nodeRes.statusCode = typeof err?.statusCode === 'number' ? err.statusCode : 500; + nodeRes.setHeader('Content-Type', 'application/json; charset=utf-8'); + } + nodeRes.end(JSON.stringify({ error: err?.message || 'Internal Server Error' })); + } + } + } + + async listen(port: number) { + const targetPort = port ?? this.port; + // Port 0 = OS-assigned; retry only makes sense for concrete ports. + const maxRetries = targetPort === 0 ? 1 : 20; + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + await this.tryListen(targetPort === 0 ? 0 : targetPort + attempt); + return; + } catch (err: any) { + if (err?.code === 'EADDRINUSE' && attempt < maxRetries - 1) continue; + throw err; + } + } + } + + private tryListen(port: number): Promise { + return new Promise((resolve, reject) => { + const server = createServer((req, res) => { + void this.handleRequest(req, res).catch(() => { + if (!res.writableEnded) { + res.statusCode = 500; + res.end(JSON.stringify({ error: 'Internal Server Error' })); + } + }); + }); + const onError = (err: any) => { server.close(); reject(err); }; + server.once('error', onError); + server.listen(port, () => { + server.removeListener('error', onError); + const addr = server.address(); + this.listeningPort = typeof addr === 'object' && addr ? addr.port : port; + this.server = server; + resolve(); + }); + }); + } + + getPort(): number { + return this.listeningPort ?? this.port; + } + + async close() { + if (!this.server) return; + const server = this.server; + this.server = undefined; + await new Promise((resolve) => { + let settled = false; + const finish = () => { if (!settled) { settled = true; resolve(); } }; + server.close(() => finish()); + if (typeof server.closeIdleConnections === 'function') server.closeIdleConnections(); + const timer = setTimeout(() => { + if (typeof server.closeAllConnections === 'function') server.closeAllConnections(); + finish(); + }, this.drainTimeoutMs); + if (typeof timer.unref === 'function') timer.unref(); + }); + } +} diff --git a/packages/qa/http-conformance/src/conformance.integration.test.ts b/packages/qa/http-conformance/src/conformance.integration.test.ts new file mode 100644 index 0000000000..c611d0b365 --- /dev/null +++ b/packages/qa/http-conformance/src/conformance.integration.test.ts @@ -0,0 +1,216 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * ADR-0076 D11 / OQ#10 (#2462) — multi-adapter conformance suite. + * + * The transport port (`IHttpServer`) was designed for multiple adapters but + * only Hono ever existed, so "the port is framework-agnostic" was an unproven + * claim. This suite is the proof: it boots the SAME framework stacks — + * the dispatcher bridge (control plane) and the REST route generator (data + * plane) — once on `plugin-hono-server` and once on the zero-dependency + * `NodeHttpServer`, and asserts identical observable behavior over real + * sockets. + * + * If a Hono-ism ever leaks into a route consumer (dispatcher-plugin, + * rest-server, package-routes), the node half of this suite is what breaks. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { LiteKernel } from '@objectstack/core'; +import { ObjectQL, ObjectQLPlugin } from '@objectstack/objectql'; +import { InMemoryDriver } from '@objectstack/driver-memory'; +import { HonoServerPlugin } from '@objectstack/plugin-hono-server'; +import { createDispatcherPlugin, createRestApiPlugin } from '@objectstack/runtime'; +import { NodeServerPlugin } from './node-plugin.js'; + +type AdapterCase = { + label: 'node' | 'hono'; + makePlugin: () => any; +}; + +const ADAPTERS: AdapterCase[] = [ + { label: 'node', makePlugin: () => new NodeServerPlugin({ port: 0 }) }, + { label: 'hono', makePlugin: () => new HonoServerPlugin({ port: 0, registerStandardEndpoints: false }) }, +]; + +/** + * Boot the full HTTP stack (ObjectQL engine + REST generator + dispatcher + * bridge) on the given adapter and return its base URL + kernel. + */ +async function bootStack(makePlugin: () => any) { + const kernel = new LiteKernel(); + kernel.use(new ObjectQLPlugin()); + kernel.use(makePlugin()); + kernel.use(createRestApiPlugin({ + api: { + api: { + // Conformance focuses on transport parity; the anonymous-deny + // posture is exercised separately with requireAuth on. + requireAuth: false, + } as any, + }, + })); + kernel.use(createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false, requireAuth: false })); + + await kernel.bootstrap(); + + const ql = kernel.getService('objectql'); + ql.registerDriver(new InMemoryDriver(), true); + ql.registerObject({ + name: 'task', + label: 'Task', + fields: { + title: { type: 'text', label: 'Title' }, + }, + }); + + const httpServer = kernel.getService('http.server'); + return { kernel, base: `http://127.0.0.1:${httpServer.getPort()}` }; +} + +describe.each(ADAPTERS)('IHttpServer conformance on $label adapter', ({ makePlugin }) => { + let kernel: LiteKernel; + let base: string; + + beforeAll(async () => { + ({ kernel, base } = await bootStack(makePlugin)); + }, 30_000); + + afterAll(async () => { + if (kernel) { + await Promise.race([ + kernel.shutdown(), + new Promise((resolve) => setTimeout(resolve, 10_000)), + ]); + } + }, 30_000); + + // ── Dispatcher bridge (control plane) ──────────────────────────────── + + it('serves GET /api/v1/ready (dispatcher bridge)', async () => { + const res = await fetch(`${base}/api/v1/ready`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.success).toBe(true); + expect(body.data.status).toBe('ready'); + }); + + it('serves GET /api/v1/health (dispatcher bridge)', async () => { + const res = await fetch(`${base}/api/v1/health`); + expect(res.status).toBe(200); + expect((await res.json()).data.status).toBe('ok'); + }); + + it('serves /.well-known/objectstack discovery', async () => { + const res = await fetch(`${base}/.well-known/objectstack`); + expect(res.status).toBe(200); + const body = await res.json(); + const routes = body.data?.routes ?? body.routes; + expect(routes.data).toBe('/api/v1/data'); + // D12 (#2462): no HTTP realtime surface exists — must not be advertised. + expect(routes.realtime).toBeUndefined(); + }); + + it('routes :param segments through the bridge (i18n 501 without service)', async () => { + // No i18n service registered — the handler answers 501. Reaching the + // handler at all proves param-routing works on this adapter. + const res = await fetch(`${base}/api/v1/i18n/translations/zh-CN`); + expect(res.status).toBe(501); + }); + + // ── REST route generator (data plane) ──────────────────────────────── + + it('runs a full /data CRUD roundtrip through the REST generator', async () => { + const created = await fetch(`${base}/api/v1/data/task`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'adapter parity' }), + }); + expect(created.status).toBe(201); + const createdBody = await created.json(); + const id = createdBody?.record?.id ?? createdBody?.data?.record?.id ?? createdBody?.id; + expect(id).toBeTruthy(); + + const list = await fetch(`${base}/api/v1/data/task`); + expect(list.status).toBe(200); + const listBody = await list.json(); + const records = listBody?.records ?? listBody?.data?.records ?? []; + expect(records.some((r: any) => r.title === 'adapter parity')).toBe(true); + + const one = await fetch(`${base}/api/v1/data/task/${id}`); + expect(one.status).toBe(200); + + const del = await fetch(`${base}/api/v1/data/task/${id}`, { method: 'DELETE' }); + expect(del.status).toBe(200); + }); + + it('serves /api/v1/meta metadata reads', async () => { + const res = await fetch(`${base}/api/v1/meta/objects/task`); + expect(res.status).toBe(200); + }); + + // ── Error semantics ─────────────────────────────────────────────────── + + it('404s unknown paths with the shared not-found body', async () => { + const res = await fetch(`${base}/api/v1/this-route-does-not-exist`); + expect(res.status).toBe(404); + expect(await res.json()).toEqual({ error: 'Not found' }); + }); + + it('405s a method mismatch with an Allow header', async () => { + // /api/v1/analytics/query is registered POST-only by the bridge. + const res = await fetch(`${base}/api/v1/analytics/query`, { method: 'PUT' }); + expect(res.status).toBe(405); + expect(res.headers.get('allow')).toContain('POST'); + expect((await res.json()).code).toBe('METHOD_NOT_ALLOWED'); + }); +}); + +/** + * Cross-adapter parity: the SAME requests against both adapters must produce + * the same status codes and (for JSON control-plane responses) the same body + * shape. Catches divergence that per-adapter assertions can miss. + */ +describe('node ↔ hono response parity', () => { + let node: { kernel: LiteKernel; base: string }; + let hono: { kernel: LiteKernel; base: string }; + + beforeAll(async () => { + node = await bootStack(ADAPTERS[0].makePlugin); + hono = await bootStack(ADAPTERS[1].makePlugin); + }, 60_000); + + afterAll(async () => { + for (const stack of [node, hono]) { + if (stack?.kernel) { + await Promise.race([ + stack.kernel.shutdown(), + new Promise((resolve) => setTimeout(resolve, 10_000)), + ]); + } + } + }, 30_000); + + const PROBES: Array<{ path: string; method?: string }> = [ + { path: '/api/v1/ready' }, + { path: '/api/v1/health' }, + { path: '/.well-known/objectstack' }, + { path: '/api/v1/discovery' }, + { path: '/api/v1/i18n/locales' }, + { path: '/api/v1/meta/objects/task' }, + { path: '/api/v1/data/task' }, + { path: '/api/v1/no-such-route' }, + { path: '/api/v1/analytics/query', method: 'PUT' }, + ]; + + it.each(PROBES)('parity on $method $path', async ({ path, method = 'GET' }) => { + const [a, b] = await Promise.all([ + fetch(`${node.base}${path}`, { method }), + fetch(`${hono.base}${path}`, { method }), + ]); + expect(a.status).toBe(b.status); + const [aBody, bBody] = await Promise.all([a.json(), b.json()]); + // Compare shapes, not values (ids/timestamps differ): same top-level keys. + expect(Object.keys(aBody ?? {}).sort()).toEqual(Object.keys(bBody ?? {}).sort()); + }); +}); diff --git a/packages/qa/http-conformance/src/index.ts b/packages/qa/http-conformance/src/index.ts new file mode 100644 index 0000000000..de75e79418 --- /dev/null +++ b/packages/qa/http-conformance/src/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +export { NodeHttpServer, compileRoute } from './adapter.js'; +export { NodeServerPlugin } from './node-plugin.js'; +export type { NodeServerPluginOptions } from './node-plugin.js'; diff --git a/packages/qa/http-conformance/src/node-plugin.ts b/packages/qa/http-conformance/src/node-plugin.ts new file mode 100644 index 0000000000..dd8db94a84 --- /dev/null +++ b/packages/qa/http-conformance/src/node-plugin.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Plugin, PluginContext } from '@objectstack/core'; +import { NodeHttpServer } from './adapter.js'; + +export interface NodeServerPluginOptions { + /** Port to listen on. `0` requests an OS-assigned free port. @default 3000 */ + port?: number; + /** Drain window (ms) for graceful close. @default 10000 */ + drainTimeoutMs?: number; +} + +/** + * NodeServerPlugin — registers a {@link NodeHttpServer} as the kernel's + * `http.server` service, exactly like `HonoServerPlugin` does for Hono. + * + * Deliberately thin (ADR-0076 D11 / OQ#10, #2462): no CORS, no static/SPA + * mounts, no standard-endpoint fallbacks, no Server-Timing. Its job is to be + * the *second* adapter behind the transport port so the framework's route + * consumers (dispatcher bridge, REST generator) are validated against a + * non-Hono `IHttpServer`. Production deployments should keep using + * `plugin-hono-server`; this adapter targets embedding scenarios and the + * multi-adapter conformance suite. + */ +export class NodeServerPlugin implements Plugin { + name = 'com.objectstack.server.node'; + type = 'server'; + version = '0.1.0'; + + private server: NodeHttpServer; + + constructor(private options: NodeServerPluginOptions = {}) { + this.server = new NodeHttpServer(options.port ?? 3000, options.drainTimeoutMs); + } + + init = async (ctx: PluginContext) => { + // Same service names as the primary adapter, so every consumer that + // resolves 'http.server' / 'http-server' works unchanged. + ctx.registerService('http.server', this.server); + ctx.registerService('http-server', this.server); + ctx.logger.debug('Node HTTP server service registered', { serviceName: 'http.server' }); + }; + + start = async (ctx: PluginContext) => { + await this.server.listen(this.options.port ?? 3000); + ctx.logger.info('Node HTTP server started', { + port: this.server.getPort(), + url: `http://localhost:${this.server.getPort()}`, + }); + }; + + async destroy() { + await this.server.close(); + } +} diff --git a/packages/qa/http-conformance/tsconfig.json b/packages/qa/http-conformance/tsconfig.json new file mode 100644 index 0000000000..e1db4ba390 --- /dev/null +++ b/packages/qa/http-conformance/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "ignoreDeprecations": "6.0", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "outDir": "./dist", + "types": [ + "node" + ], + "rootDir": "./src" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts" + ] +} diff --git a/packages/qa/http-conformance/vitest.config.ts b/packages/qa/http-conformance/vitest.config.ts new file mode 100644 index 0000000000..201ee43e71 --- /dev/null +++ b/packages/qa/http-conformance/vitest.config.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + // The conformance suite boots real kernels + sockets. + testTimeout: 30_000, + hookTimeout: 30_000, + }, +}); diff --git a/packages/services/service-realtime/README.md b/packages/services/service-realtime/README.md index 5c612b45c8..c543791e81 100644 --- a/packages/services/service-realtime/README.md +++ b/packages/services/service-realtime/README.md @@ -83,7 +83,7 @@ real client transport), the delivery path MUST gain one of: 2. **id-only payloads** — the client re-fetches the record under its own authority. This posture is registered in the authz conformance matrix -(`packages/dogfood/test/authz-conformance.matrix.ts`, row `realtime-delivery-authz`), +(`packages/qa/dogfood/test/authz-conformance.matrix.ts`, row `realtime-delivery-authz`), and transport **tripwire probes** in `authz-conformance.test.ts` fail CI if a transport is wired without upgrading that row with a real enforcement site. diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index ad492f1eea..9197cfee07 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -50,7 +50,7 @@ outcome. "type": { "status": "live", "evidence": "packages/objectql/src/engine.ts", - "proof": "packages/dogfood/test/field-zoo-roundtrip.dogfood.test.ts#field-type-roundtrip" + "proof": "packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts#field-type-roundtrip" } ``` @@ -64,7 +64,7 @@ self-declares the id with a greppable tag near its top: The gate validates **statically** (it never runs the test — that's the dogfood gate's job, keeping this gate seconds-cheap): the file must exist **and** declare the `@proof: ` tag. A bound entry must point at *its own class's* proof. The reverse -is also checked: a `@proof:` tag under `packages/dogfood/test/**` that isn't +is also checked: a `@proof:` tag under `packages/qa/dogfood/test/**` that isn't registered in `../scripts/liveness/proof-registry.mts` is flagged (warning) so a new proof gets wired in. @@ -84,7 +84,7 @@ binding lands one class at a time (ADR-0054 §3), never as a big-bang backfill. To bind a pending class: add its dogfood proof + `@proof:` tag, set `bound: true` and its `ledgerBindings` in `proof-registry.mts`, add the `proof` to the ledger entry, and -confirm the gate is green. Because the gate also triggers on `packages/dogfood/**`, +confirm the gate is green. Because the gate also triggers on `packages/qa/dogfood/**`, deleting or renaming a proof re-runs this check and the dangling reference is caught. ## Author warnings — closing the loop (`authorWarn`) diff --git a/packages/spec/liveness/dataset.json b/packages/spec/liveness/dataset.json index 89effa7992..c5a210d0d3 100644 --- a/packages/spec/liveness/dataset.json +++ b/packages/spec/liveness/dataset.json @@ -56,7 +56,7 @@ "dateGranularity": { "status": "live", "evidence": "packages/services/service-analytics/src/dataset-compiler.ts:146", - "proof": "packages/dogfood/test/analytics-timezone.dogfood.test.ts#analytics-tz-bucketing", + "proof": "packages/qa/dogfood/test/analytics-timezone.dogfood.test.ts#analytics-tz-bucketing", "note": "ADR-0054 high-risk class (analytics): a time dimension's single granularity auto-buckets at query time (dataset-executor.ts:272-287); the proof asserts the day bucket SHIFTS with the org timezone (2024-03-01T03:00Z buckets to 2024-02-29 in America/Los_Angeles) — guarding the analytics-strategy ↔ in-memory count ↔ REST exec-context integration that #2018 fixed." } } diff --git a/packages/spec/liveness/field.json b/packages/spec/liveness/field.json index a7454d941d..1bf7dd678f 100644 --- a/packages/spec/liveness/field.json +++ b/packages/spec/liveness/field.json @@ -13,7 +13,7 @@ "type": { "status": "live", "evidence": "packages/objectql/src/engine.ts", - "proof": "packages/dogfood/test/field-zoo-roundtrip.dogfood.test.ts#field-type-roundtrip", + "proof": "packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts#field-type-roundtrip", "note": "ADR-0054 high-risk class (field types): the proof writes one record of (almost) every field type over the real HTTP API and asserts each reads back with type fidelity (rating/slider→number, toggle→boolean) — guarding the persistence + read-coercion integration that #2025 fixed." }, "description": { @@ -88,7 +88,7 @@ "readonly": { "status": "live", "evidence": "packages/objectql/src/validation/rule-validator.ts", - "proof": "packages/dogfood/test/showcase-static-readonly.dogfood.test.ts#readonly-static-write", + "proof": "packages/qa/dogfood/test/showcase-static-readonly.dogfood.test.ts#readonly-static-write", "note": "renderer + server write path: stripReadonlyFields drops non-system UPDATE writes to the field (#2948/#3003 — was renderer-only, i.e. false compliance for approval/status columns); INSERT exempt, symmetric with readonlyWhen." }, "hidden": { diff --git a/packages/spec/liveness/flow.json b/packages/spec/liveness/flow.json index 63095b1153..16507d4c78 100644 --- a/packages/spec/liveness/flow.json +++ b/packages/spec/liveness/flow.json @@ -38,7 +38,7 @@ }, "runAs": { "status": "live", - "proof": "packages/dogfood/test/flow-runas.dogfood.test.ts#flow-runas-identity", + "proof": "packages/qa/dogfood/test/flow-runas.dogfood.test.ts#flow-runas-identity", "evidence": "packages/services/service-automation/src/builtin/crud-nodes.ts: CRUD nodes pass the run identity to ObjectQL per flow.runAs; the engine sets context.runAs at run setup; resolveRunDataContext maps system->isSystem / user->trigger user (#1888)", "note": "ENFORCED 2026-06-24 (#1888, ADR-0049): was marker-driven experimental; the engine now switches the data-layer execution identity and restores the caller context. Proof-backed by the dogfood gate." }, @@ -72,7 +72,7 @@ "type": { "status": "live", "evidence": "packages/services/service-automation/src/engine.ts", - "proof": "packages/dogfood/test/flow-node.dogfood.test.ts#flow-node-execution", + "proof": "packages/qa/dogfood/test/flow-node.dogfood.test.ts#flow-node-execution", "note": "open string vs live executor registry (ADR-0018); FlowNodeAction enum is out of sync. ADR-0054 high-risk class (flow nodes): the proof authors a flow, triggers it over HTTP, and asserts the update_record node executed AND the input variable wired into its filter (only the targeted record changed) — guarding node execution + variable wiring, not just that the engine reads the node." }, "label": { diff --git a/packages/spec/liveness/object.json b/packages/spec/liveness/object.json index 7cf4c284de..1eff94918f 100644 --- a/packages/spec/liveness/object.json +++ b/packages/spec/liveness/object.json @@ -64,7 +64,7 @@ "lifecycle": { "status": "live", "evidence": "packages/objectql/src/lifecycle/lifecycle-service.ts", - "proof": "packages/dogfood/test/storage-growth.dogfood.test.ts#adr0057-lifecycle-bounded-growth", + "proof": "packages/qa/dogfood/test/storage-growth.dogfood.test.ts#adr0057-lifecycle-bounded-growth", "note": "ADR-0057 data lifecycle contract. The platform LifecycleService (registered by ObjectQLPlugin, default-on) reaps rows past retention.maxAge / ttl.expireAfter under a system context, bounds rotation-declared telemetry by shards×unit, never hot-deletes archive-declared audit ledgers unarchived, and reclaims driver space (SQLite incremental_vacuum) after each sweep. Non-record classes without a bounding policy are rejected at parse time (LifecycleSchema superRefine)." }, "external": { @@ -119,7 +119,7 @@ "sharingModel": { "status": "live", "evidence": "packages/plugins/plugin-sharing/src/sharing-service.ts:54", - "proof": "packages/dogfood/test/controlled-by-parent.dogfood.test.ts#cbp-controlled-by-parent", + "proof": "packages/qa/dogfood/test/controlled-by-parent.dogfood.test.ts#cbp-controlled-by-parent", "note": "ADR-0055 high-risk class (sharing): the `controlled_by_parent` value derives a detail object's access from its master — the security layer injects `masterFK IN (accessible master ids)` on reads and requires master edit-access on by-id writes. The proof asserts a member who cannot read the master can neither read nor by-id-write the detail, and is not over-blocked on a master they own." }, "publicSharing": { diff --git a/packages/spec/liveness/permission.json b/packages/spec/liveness/permission.json index 2a827f8d2f..1eff20d8b7 100644 --- a/packages/spec/liveness/permission.json +++ b/packages/spec/liveness/permission.json @@ -14,7 +14,7 @@ "packageId": { "status": "live", "evidence": "packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts (author-declared fallback for the registry `_packageId` provenance; persisted to sys_permission_set.package_id)", - "note": "ADR-0086 D3 — owning package for a package-shipped set; proven by packages/dogfood/test/showcase-permission-seeding.dogfood.test.ts." + "note": "ADR-0086 D3 — owning package for a package-shipped set; proven by packages/qa/dogfood/test/showcase-permission-seeding.dogfood.test.ts." }, "managedBy": { "status": "live", @@ -29,7 +29,7 @@ "isDefault": { "status": "live", "evidence": "plugin-security: packages/plugins/plugin-security/src/security-plugin.ts (constructor) — when no explicit fallbackPermissionSet is configured, the SecurityPlugin resolves the fallback from the permission set marked isDefault:true (ADR-0056 D7).", - "note": "LIVE — app-declared default profile; proven by packages/dogfood/test/showcase-default-profile.dogfood.test.ts." + "note": "LIVE — app-declared default profile; proven by packages/qa/dogfood/test/showcase-default-profile.dogfood.test.ts." }, "objects": { "children": { @@ -75,7 +75,7 @@ "readScope": { "status": "live", "evidence": "packages/plugins/plugin-security/src/permission-evaluator.ts (getEffectiveScope) + packages/plugins/plugin-sharing/src/sharing-service.ts (owner-match widened by readScope; hierarchy values delegated to IHierarchyScopeResolver)", - "note": "ADR-0057 D1 — read access DEPTH (own/own_and_reports/unit/unit_and_below/org). own/org enforced in open edition; hierarchy values via the enterprise hierarchy-scope-resolver (fail-closed to owner-only when absent; defineStack requires 'hierarchy-security'). Proven: packages/dogfood/test/showcase-scope-depth.dogfood.test.ts." + "note": "ADR-0057 D1 — read access DEPTH (own/own_and_reports/unit/unit_and_below/org). own/org enforced in open edition; hierarchy values via the enterprise hierarchy-scope-resolver (fail-closed to owner-only when absent; defineStack requires 'hierarchy-security'). Proven: packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts." }, "writeScope": { "status": "live", @@ -132,7 +132,7 @@ "using": { "status": "live", "evidence": "packages/plugins/plugin-security/src/rls-compiler.ts", - "proof": "packages/dogfood/test/rls-fixture.dogfood.test.ts#rls-by-id-write", + "proof": "packages/qa/dogfood/test/rls-fixture.dogfood.test.ts#rls-by-id-write", "note": "compiled into find + analytics SQL. ADR-0054 high-risk class (RLS): the proof boots an owner-isolated fixture so a fresh member cannot read an admin-created row, then asserts the runner's verdict in both directions — `rls-consistent` when the owner predicate also gates the by-id write (#1994 pre-image check) and `rls-hole` when it doesn't. Guards read AND by-id-write enforcement, not just the read predicate." }, "check": { diff --git a/packages/spec/liveness/position.json b/packages/spec/liveness/position.json index 3e38576f7e..ed3757928f 100644 --- a/packages/spec/liveness/position.json +++ b/packages/spec/liveness/position.json @@ -18,7 +18,7 @@ "delegatable": { "status": "live", "evidence": "packages/plugins/plugin-security/src/delegated-admin-gate.ts:positionIsDelegatable", - "proof": "packages/dogfood/test/delegation-of-duty.dogfood.test.ts#delegation-of-duty", + "proof": "packages/qa/dogfood/test/delegation-of-duty.dogfood.test.ts#delegation-of-duty", "note": "ADR-0091 D3: gates the self-service delegation branch — a non-admin may delegate this position only when delegatable=true, time-boxed and reasoned (enforced fail-closed in the D12 gate). ADR-0054 high-risk class (delegation): the proof drives a gated delegation write over HTTP and asserts the delegate's grant resolves in-window and dies at valid_until." } } diff --git a/packages/spec/scripts/liveness/check-liveness.mts b/packages/spec/scripts/liveness/check-liveness.mts index 11fd5a8006..291649d13a 100644 --- a/packages/spec/scripts/liveness/check-liveness.mts +++ b/packages/spec/scripts/liveness/check-liveness.mts @@ -190,7 +190,7 @@ function classify(type: string, path: string, status: string, led: any, cat: any // must be registered in proof-registry.mts. An orphan tag means a proof was // written but never wired into the high-risk-class list — flag it (warning). function scanOrphanProofs() { - const proofDir = join(repoRoot, 'packages/dogfood/test'); + const proofDir = join(repoRoot, 'packages/qa/dogfood/test'); if (!existsSync(proofDir)) return; // spec may be consumed standalone (published) const walk = (dir: string): string[] => { const out: string[] = []; diff --git a/packages/spec/scripts/liveness/proof-registry.mts b/packages/spec/scripts/liveness/proof-registry.mts index 2c28ab87dc..577e022d13 100644 --- a/packages/spec/scripts/liveness/proof-registry.mts +++ b/packages/spec/scripts/liveness/proof-registry.mts @@ -60,7 +60,7 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ summary: 'persistence + read-coercion fidelity across the field-type matrix (write 4 → read 4, not "4").', proofId: 'field-type-roundtrip', - proofRef: 'packages/dogfood/test/field-zoo-roundtrip.dogfood.test.ts#field-type-roundtrip', + proofRef: 'packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts#field-type-roundtrip', bound: true, ledgerBindings: [{ type: 'field', path: 'type' }], }, @@ -70,7 +70,7 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ summary: "row-level read AND by-id-write enforcement (the #1994 can't-write-what-you-can't-read invariant).", proofId: 'rls-by-id-write', - proofRef: 'packages/dogfood/test/rls-fixture.dogfood.test.ts#rls-by-id-write', + proofRef: 'packages/qa/dogfood/test/rls-fixture.dogfood.test.ts#rls-by-id-write', bound: true, // `using` is the read predicate the #1994 fix re-applies as a by-id-write // pre-image check — the property whose end-to-end correctness the proof guards. @@ -81,7 +81,7 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ label: 'Master-detail controlled-by-parent', summary: "a detail record's read/write access derived from its master record (ADR-0055).", proofId: 'cbp-controlled-by-parent', - proofRef: 'packages/dogfood/test/controlled-by-parent.dogfood.test.ts#cbp-controlled-by-parent', + proofRef: 'packages/qa/dogfood/test/controlled-by-parent.dogfood.test.ts#cbp-controlled-by-parent', bound: true, ledgerBindings: [{ type: 'object', path: 'sharingModel' }], }, @@ -90,7 +90,7 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ label: 'Analytics dimensions / measures', summary: 'date-dimension bucketing / aggregation under the org timezone (#1982/#2018).', proofId: 'analytics-tz-bucketing', - proofRef: 'packages/dogfood/test/analytics-timezone.dogfood.test.ts#analytics-tz-bucketing', + proofRef: 'packages/qa/dogfood/test/analytics-timezone.dogfood.test.ts#analytics-tz-bucketing', bound: true, // The org-timezone shift acts on a time dimension's bucketing granularity — // the property whose end-to-end correctness the tz proof guards. @@ -101,7 +101,7 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ label: 'Flow nodes', summary: 'node execution + variable wiring through the automation engine.', proofId: 'flow-node-execution', - proofRef: 'packages/dogfood/test/flow-node.dogfood.test.ts#flow-node-execution', + proofRef: 'packages/qa/dogfood/test/flow-node.dogfood.test.ts#flow-node-execution', bound: true, // `nodes.type` selects which executor runs — the property whose end-to-end // execution + variable wiring the proof guards. @@ -112,7 +112,7 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ label: 'Flow runAs identity', summary: 'flow.runAs switches the data-layer execution identity — system elevates (RLS-bypass), user de-elevates to the triggering user (#1888).', proofId: 'flow-runas-identity', - proofRef: 'packages/dogfood/test/flow-runas.dogfood.test.ts#flow-runas-identity', + proofRef: 'packages/qa/dogfood/test/flow-runas.dogfood.test.ts#flow-runas-identity', bound: true, // `runAs` decides whether CRUD nodes run as an elevated system principal or // the triggering user — the security property the proof guards in both directions. @@ -124,7 +124,7 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ summary: 'a non-admin holder of a delegatable position may self-service a time-boxed grant that dies at valid_until (ADR-0091 D3) — the write is gated and the resolution expires, both proven end-to-end.', proofId: 'delegation-of-duty', - proofRef: 'packages/dogfood/test/delegation-of-duty.dogfood.test.ts#delegation-of-duty', + proofRef: 'packages/qa/dogfood/test/delegation-of-duty.dogfood.test.ts#delegation-of-duty', bound: true, // `delegatable` opens the D12 self-service branch — an authority-widening // flag whose end-to-end enforcement (gated write + expiring resolution) a @@ -137,7 +137,7 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ summary: 'declared lifecycle policies actually bound growth at runtime — the Reaper deletes past-window rows, record-class/business data stays untouched, archive-declared audit ledgers are never hot-deleted unarchived.', proofId: 'adr0057-lifecycle-bounded-growth', - proofRef: 'packages/dogfood/test/storage-growth.dogfood.test.ts#adr0057-lifecycle-bounded-growth', + proofRef: 'packages/qa/dogfood/test/storage-growth.dogfood.test.ts#adr0057-lifecycle-bounded-growth', bound: true, // `lifecycle` declares the retention contract the LifecycleService enforces — // a declaration whose sweeper stopped running is dead surface (ADR-0049). @@ -149,7 +149,7 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ summary: 'a statically `readonly: true` field cannot be forged by a non-system UPDATE — the write is stripped server-side, not merely hidden by the form (#2948/#3003: approval/status/amount columns "protected" only by readonly were one direct PATCH away from self-approval).', proofId: 'readonly-static-write', - proofRef: 'packages/dogfood/test/showcase-static-readonly.dogfood.test.ts#readonly-static-write', + proofRef: 'packages/qa/dogfood/test/showcase-static-readonly.dogfood.test.ts#readonly-static-write', bound: true, // `readonly` was renderer-only until #2948 — declared ≠ enforced is exactly // the false-compliance class ADR-0049 closes. The proof pins the server-side @@ -176,7 +176,7 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [ + 'projection). A record that could drift from — or independently authorize against — its ' + 'metadata is the two-store split-brain ADR-0094 retires.', proofId: 'permission-set-projection', - proofRef: 'packages/dogfood/test/showcase-permission-projection.dogfood.test.ts#permission-set-projection', + proofRef: 'packages/qa/dogfood/test/showcase-permission-projection.dogfood.test.ts#permission-set-projection', // Unbound: this is a STORAGE/architecture invariant, not an authorable // `type.path` property the ledger ratchet can key on — there is no // permission-set field whose `live` status this proof gates. The proof is diff --git a/packages/spec/scripts/liveness/proof-registry.test.ts b/packages/spec/scripts/liveness/proof-registry.test.ts index 2162afd463..1ad3c13bb7 100644 --- a/packages/spec/scripts/liveness/proof-registry.test.ts +++ b/packages/spec/scripts/liveness/proof-registry.test.ts @@ -20,8 +20,8 @@ const repoRoot = resolve(here, '../../../..'); // packages/spec/scripts/liveness describe('parseProofRef', () => { it('splits a well-formed ref into file + id', () => { - expect(parseProofRef('packages/dogfood/test/x.test.ts#my-proof')).toEqual({ - file: 'packages/dogfood/test/x.test.ts', + expect(parseProofRef('packages/qa/dogfood/test/x.test.ts#my-proof')).toEqual({ + file: 'packages/qa/dogfood/test/x.test.ts', id: 'my-proof', }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93a8b31490..4c2e7a9a25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -767,77 +767,6 @@ importers: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3))(vite@8.0.16(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) - packages/dogfood: - dependencies: - '@objectstack/connector-mcp': - specifier: workspace:* - version: link:../connectors/connector-mcp - '@objectstack/connector-openapi': - specifier: workspace:* - version: link:../connectors/connector-openapi - '@objectstack/connector-rest': - specifier: workspace:* - version: link:../connectors/connector-rest - '@objectstack/example-crm': - specifier: workspace:* - version: link:../../examples/app-crm - '@objectstack/example-showcase': - specifier: workspace:* - version: link:../../examples/app-showcase - '@objectstack/objectql': - specifier: workspace:* - version: link:../objectql - '@objectstack/plugin-audit': - specifier: workspace:* - version: link:../plugins/plugin-audit - '@objectstack/plugin-auth': - specifier: workspace:* - version: link:../plugins/plugin-auth - '@objectstack/plugin-security': - specifier: workspace:* - version: link:../plugins/plugin-security - '@objectstack/service-storage': - specifier: workspace:* - version: link:../services/service-storage - '@objectstack/spec': - specifier: workspace:* - version: link:../spec - '@objectstack/verify': - specifier: workspace:* - version: link:../verify - devDependencies: - '@objectstack/core': - specifier: workspace:* - version: link:../core - '@objectstack/driver-memory': - specifier: workspace:* - version: link:../plugins/driver-memory - '@objectstack/driver-sql': - specifier: workspace:* - version: link:../plugins/driver-sql - '@types/node': - specifier: ^26.1.1 - version: 26.1.1 - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vitest: - specifier: ^4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3))(vite@8.0.16(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) - - packages/downstream-contract: - dependencies: - '@objectstack/spec': - specifier: workspace:* - version: link:../spec - devDependencies: - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vitest: - specifier: ^4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3))(vite@8.0.16(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) - packages/formula: dependencies: '@marcbachmann/cel-js': @@ -1629,6 +1558,105 @@ importers: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3))(vite@8.0.16(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + packages/qa/dogfood: + dependencies: + '@objectstack/connector-mcp': + specifier: workspace:* + version: link:../../connectors/connector-mcp + '@objectstack/connector-openapi': + specifier: workspace:* + version: link:../../connectors/connector-openapi + '@objectstack/connector-rest': + specifier: workspace:* + version: link:../../connectors/connector-rest + '@objectstack/example-crm': + specifier: workspace:* + version: link:../../../examples/app-crm + '@objectstack/example-showcase': + specifier: workspace:* + version: link:../../../examples/app-showcase + '@objectstack/objectql': + specifier: workspace:* + version: link:../../objectql + '@objectstack/plugin-audit': + specifier: workspace:* + version: link:../../plugins/plugin-audit + '@objectstack/plugin-auth': + specifier: workspace:* + version: link:../../plugins/plugin-auth + '@objectstack/plugin-security': + specifier: workspace:* + version: link:../../plugins/plugin-security + '@objectstack/service-storage': + specifier: workspace:* + version: link:../../services/service-storage + '@objectstack/spec': + specifier: workspace:* + version: link:../../spec + '@objectstack/verify': + specifier: workspace:* + version: link:../../verify + devDependencies: + '@objectstack/core': + specifier: workspace:* + version: link:../../core + '@objectstack/driver-memory': + specifier: workspace:* + version: link:../../plugins/driver-memory + '@objectstack/driver-sql': + specifier: workspace:* + version: link:../../plugins/driver-sql + '@types/node': + specifier: ^26.1.1 + version: 26.1.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3))(vite@8.0.16(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + + packages/qa/downstream-contract: + dependencies: + '@objectstack/spec': + specifier: workspace:* + version: link:../../spec + devDependencies: + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3))(vite@8.0.16(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + + packages/qa/http-conformance: + dependencies: + '@objectstack/core': + specifier: workspace:* + version: link:../../core + devDependencies: + '@objectstack/driver-memory': + specifier: workspace:* + version: link:../../plugins/driver-memory + '@objectstack/objectql': + specifier: workspace:* + version: link:../../objectql + '@objectstack/plugin-hono-server': + specifier: workspace:* + version: link:../../plugins/plugin-hono-server + '@objectstack/runtime': + specifier: workspace:* + version: link:../../runtime + '@types/node': + specifier: ^26.1.1 + version: 26.1.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3))(vite@8.0.16(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + packages/rest: dependencies: '@objectstack/core': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 317ff8001c..4175c78e36 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - packages/* - packages/apps/* - packages/plugins/* + - packages/qa/* - packages/triggers/* - packages/services/* - packages/adapters/*