Skip to content

Commit 2a96299

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14957-platform-object-tenancy-census
2 parents a4c43b0 + a06faeb commit 2a96299

22 files changed

Lines changed: 1783 additions & 104 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
`chart-config-missing` no longer fires on a widget whose binding the renderer derives
6+
7+
The rule warned on every chart-family widget that declared no `chartConfig`, on the
8+
stated grounds that "the renderer cannot determine which measure to plot, so the series
9+
renders empty". Measured against the `@object-ui` revision this repo pins
10+
(`.objectui-sha`), that consequence is false: `DatasetWidget` derives the x-axis key and
11+
one series per measure from the widget's own `dimensions` / `values` via
12+
`buildChartSeries`, and refuses an authored `ChartAxis.field` / `ChartSeries.name`
13+
outright — `chartConfig` carries presentation only. The renderer pins this by name:
14+
"ignores an authored axis `field` and keeps the derived axis binding", "ignores an
15+
authored series and keeps one derived series per measure", "emits none of the
16+
presentation keys when no chartConfig is declared".
17+
18+
The false finding was landing on this platform's own shipped metadata — the
19+
`system_overview` dashboard's pie and bar tiles, on the Setup board every customer opens
20+
first — which is the ADR-0072 D1 cost the rule family exists to avoid.
21+
22+
The rule id is unchanged and keeps one true arm: a `combo` widget with no `chartConfig`,
23+
whose per-series mark is authored as `chartConfig.series[].type` and has no other
24+
channel, so every measure draws with the same default mark and the chart is not a
25+
combination at all. Its message now names that consequence instead of the binding.
26+
An existing `suppressWarnings: ['chart-config-missing']` entry stays valid.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
"@objectstack/driver-mongodb": patch
3+
---
4+
5+
fix(driver-mongodb): put the test layer in front of tsc, so the package's own typecheck reports a PASS and not a NUMBER (#14917)
6+
7+
`packages/drivers/driver-mongodb`'s `tsconfig.json` excluded `**/*.test.ts`, and
8+
its `typecheck` script is `tsc --noEmit` against that very config. Measured at
9+
`6ed4b811af` with the dependency closure built: that program admits **0** of the
10+
package's 30 `src/**/*.test.ts` files while all **10** of its non-test `src/**`
11+
files ARE there, so `pnpm --filter @objectstack/driver-mongodb typecheck`
12+
exiting 0 was a true sentence carrying no information about any test file.
13+
14+
The filing's headline — that a compile-time `Equals` / `IsAny` pin here is
15+
"checked by nothing" — is **false**, and the correction on the card is right: a
16+
second program does compile these files. `check-type-check-coverage.mjs`'s
17+
`remeasureProject` drops only the test glob and compares the result against its
18+
`TEST_DEBT` ledger. Confirmed here by ablation rather than argued: a
19+
deliberately false `Equals` pin added to `mongodb-driver.test.ts` takes that
20+
program from 10 errors to 11, above the ledger's recorded 10, which reddens it.
21+
The pins were never phantoms. What was true is narrower, and is what this change
22+
closes: the only program reading this layer was a **debt ratchet** — an
23+
instrument that reports a number and fails when the number moves, not a gate
24+
that reports a pass.
25+
26+
Gives the package the #5286 sibling shape (`packages/rest`, `runtime`,
27+
`objectql`, `core`): a `tsconfig.test.json` with module semantics only —
28+
`esnext` / `bundler` / `lib: ES2022`, matching how vitest actually executes
29+
these files — strictness inherited and untouched, named by the `typecheck`
30+
script via `check:test-typecheck`.
31+
32+
Measured: **10** errors under the ratchet's shape (matching its recorded number,
33+
and its recorded composition `TS1309 x7, TS2550 x3`, class for class), and **0**
34+
under the split. All 10 were config-tier in full — 7 `TS1309` (`await` at module
35+
scope in a program NodeNext compiles as CJS, because this package has no `"type":
36+
"module"`) and 3 `TS2550` (`Array.prototype.at` against a `lib` older than
37+
es2022). Neither class says anything about a test, and nothing was exposed
38+
behind them: there was no unresolved-import cascade here to collapse, so there
39+
is no `+n` term. `noUnusedLocals` / `noUnusedParameters` are live for this
40+
package (unlike `driver-turso`, which switches both off) and neither fires.
41+
42+
The `TEST_DEBT` entry (10 errors) is **deleted**, not lowered — the graduation
43+
this ratchet's invariant requires. No `test-typecheck-debt.json` is added:
44+
residue is 0, so none is owed (#5286, maintainer-only to open). That leaves all
45+
30 files unledgered, so any error any one of them gains is red on arrival.
46+
47+
`check:type-source-resolution` went red from onboarding the new program (the
48+
documented onboarding-limb case, #11490): a registry entry is added rather than
49+
`paths`, with its numbers stated in place — 123 tsc programs / 309 pairs before,
50+
124 / 310 after. The single new pair is `@objectstack/objectql`, a devDependency
51+
that no non-test file in `src/` imports.
52+
53+
No runtime code changes: not one test file and not one source file is edited, so
54+
no shipped behaviour moves — the suite reports the same 552 passed / 147 skipped
55+
across 30 files as before. The `patch` level reflects the published
56+
`package.json` gaining `typecheck` / `check:test-typecheck` scripts and a `tsx`
57+
devDependency.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
"@objectstack/runtime": minor
3+
---
4+
5+
feat(runtime): the platform action route executes the declarative row-level `operation: 'update'` action (#14092)
6+
7+
The spec half (#15077) made `operation: 'update'` + `patch` parse; nothing performed the
8+
write, so an authored update action reached the action route with no handler and collected
9+
the registry's loud not-registered answer. It now performs the write.
10+
11+
`POST /api/v1/actions/<object>/<action>/<recordId>` — and the MCP `run_action` bridge, through
12+
the same shared executor — performs exactly ONE data-plane update of the current record:
13+
14+
- **As the caller.** The write carries the caller's own `ExecutionContext`, never the
15+
`isSystem`-elevated context a `type: 'script'` BODY runs under. There is no author body here
16+
to trust, so the data plane's own gate is the only gate — the object's permissions, its hooks
17+
and its validations fire exactly as for a user edit, and their refusals reach the caller with
18+
their own `code` and `status`. This consumes the `runAs: 'user'` direction ruled on #14010; no
19+
`runAs` key is added.
20+
- **A caller who cannot read the row is refused before anything is written** (404
21+
`RECORD_NOT_FOUND`, the platform's one existence-non-disclosing envelope), by consuming the
22+
caller-scope load's verdict rather than re-deriving it from the stamped `record.id` — the
23+
#14143 class: a swallowed load must never become an implicit grant.
24+
- **The write is `{ ...patch, ...collectedParams }`** — static values under the dialog's, so a
25+
param of the same name wins. Nothing else from the action is merged, and the ADR-0104 D2 param
26+
contract still bounds what the wire can add.
27+
- **No current record ⇒ a located refusal**, never a silent no-op: no `recordId` on the route or
28+
in the body, an action addressed at the object-less key, or an empty write bag each answer 400
29+
naming the action and the fix.
30+
- **`undoable: true`** returns `undo: { type, objectName, recordId, undoData, redoData }` — the
31+
prior values of exactly the fields written, `null` for a field the row did not carry, so the
32+
existing Undo readers can restore. The three remaining `UndoableOperation` keys (`id`,
33+
`timestamp`, `description`) stay the client's.
34+
- `visible` is deliberately unread here: it is a per-record renderer predicate, and the
35+
authorization is the point above.
36+
37+
`operation` is read BEFORE `type` at every reader, so the HTTP door and the MCP bridge agree:
38+
`isHeadlessInvokableAction` now accepts a declarative update (it has neither `target` nor `body`
39+
by construction), `headlessActionTypeError` hands it no client-side-type prescription, and
40+
`summarizeAction` reports `operation` and `requiresRecord: true`.
41+
42+
Unchanged: a handler-less `type: 'script'` action WITHOUT `operation` still gets today's
43+
not-registered 404 — the script path is not widened.

content/docs/permissions/system-context.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ not on any flag.
6565

6666
`isSystem` is **server-constructed and never client-supplied**. Inbound HTTP
6767
cannot set it (`packages/rest/src/rest-server.ts:1548`, `:1577`), and neither
68-
can an action body (`packages/runtime/src/domains/actions.ts:404`). It is
68+
can an action body (`packages/runtime/src/domains/actions.ts:414`). It is
6969
written by internal callers only, as an option on the engine call:
7070

7171
```ts
@@ -160,7 +160,7 @@ The largest single consumer — **17 of the 106 sites**.
160160
| 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:401` |
161161
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:4967`, `:6381`, `:6629`, `:7060`, `:7253` |
162162
| 51 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | `meta-write-capability.ts:134` |
163-
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:411`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:422`, `external-datasource-routes.ts:302`, `package-routes.ts:97` |
163+
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:422`, `external-datasource-routes.ts:302`, `package-routes.ts:97` |
164164
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` |
165165
| 54 | Package REST route capability gate bypassed | rest | Get: package read/write over REST without `manage_metadata` / `studio.access` / `setup.access` | `package-routes.ts:102` |
166166
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `domains/packages.ts:241`, `:274` |
@@ -199,7 +199,7 @@ assuming `isSystem` covers it is a documented source of bugs.
199199
| "It preserves a supplied `updated_at` / `updated_by`" | **No.** That is `preserveAudit`, a separate opt-in — and an UPDATE-path exemption only | `field.zod.ts:1580` (#3493 / #6640) |
200200
| "It stamps `created_by`" | **No.** Audit stamping reads `userId` from the context. A user-less system write stamps nothing — that is today's behaviour, not an error | `runtime-identity.ts:280``281` |
201201
| "It bypasses every guard" | **No.** The last-admin guard applies to **every** context, `isSystem` included — the deprovision path that actually locks an org out is the system one | `last-admin-guard.ts:299` |
202-
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1548`, `:1577`; `domains/actions.ts:404` |
202+
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1548`, `:1577`; `domains/actions.ts:414` |
203203

204204
---
205205

packages/drivers/driver-mongodb/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
1717
"dev": "tsc -w",
1818
"test": "vitest run",
19-
"typecheck": "tsc --noEmit"
19+
"typecheck": "tsc --noEmit && pnpm check:test-typecheck",
20+
"check:test-typecheck": "tsx ../../../scripts/check-test-typecheck.mts --self-test && tsx ../../../scripts/check-test-typecheck.mts --package packages/drivers/driver-mongodb --project tsconfig.test.json"
2021
},
2122
"dependencies": {
2223
"@objectstack/core": "workspace:*",
@@ -29,6 +30,7 @@
2930
"@objectstack/objectql": "workspace:*",
3031
"@types/node": "^26.2.0",
3132
"mongodb-memory-server": "^11.2.0",
33+
"tsx": "^4.23.12",
3234
"typescript": "^6.0.3",
3335
"vitest": "^4.1.10"
3436
},
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// The TEST-layer type-check program (#14917), adopting the mechanism #5286 set
2+
// for `packages/spec` and #5449 generalised — the route `packages/objectql`
3+
// (#13676), `packages/runtime` (#14504) and `packages/core` (#14613) already
4+
// run. `tsconfig.json` beside this file stays exactly as it is: it is the BUILD
5+
// config, and its `**/*.test.ts` exclusion has a reason. This sibling puts the
6+
// excluded layer back in front of tsc, and `package.json`'s `typecheck` script
7+
// NAMES it (via `check:test-typecheck --project`), because a config no script
8+
// invokes is exactly the phantom this whole mechanism is about.
9+
//
10+
// BEFORE THIS FILE, NO tsc PROGRAM COMPILED A SINGLE TEST FILE HERE, and that
11+
// is measured rather than read off the config. At 6ed4b811af with the
12+
// dependency closure built first, `tsc --noEmit --listFiles -p tsconfig.json`
13+
// puts **0** of this package's 30 `src/**/*.test.ts` files in the program —
14+
// while all **10** of its non-test `src/**` files ARE there, so the zero is the
15+
// `exclude` line and not a probe that sees nothing. Under this file the count is
16+
// **30 of 30**, with the same 10 non-test files beside them.
17+
// `pnpm --filter @objectstack/driver-mongodb typecheck` exiting 0 was a true
18+
// sentence carrying no information about any test file in this package.
19+
//
20+
// ⚠️ WHAT THE CARD SAID, AND WHAT WAS ACTUALLY WRONG. The filing's headline —
21+
// that a compile-time `Equals` / `IsAny` pin here is "checked by nothing" — is
22+
// FALSE, and the correction on the card is right: a second program does compile
23+
// these files. `scripts/check-type-check-coverage.mjs`'s `remeasureProject`
24+
// extends this package's tsconfig, drops only the test glob, and compares the
25+
// result against its `TEST_DEBT` ledger. That is how CI caught PR #14914's
26+
// three TS18047 errors, which this package's own `typecheck` could not see. So
27+
// the pins were not phantoms. What was true is narrower and is what this file
28+
// closes: the only program reading this layer was a DEBT RATCHET — an
29+
// instrument that reports a NUMBER and fails when the number MOVES, not a gate
30+
// that reports a pass.
31+
//
32+
// What differs from the build config, and what deliberately does NOT:
33+
// - module semantics ONLY, plus `lib`. The tests are written and executed as
34+
// ESM by vitest (esbuild/vite), while this package has no `"type":
35+
// "module"`, so the build config's NodeNext compiles them as CJS. Measured
36+
// cost of that mismatch here: ALL 10 of the raw diagnostics — TS1309 x7
37+
// ("cannot use `await` at the top level" in a CJS program, one per suite
38+
// that awaits `startMongod()` at module scope) and TS2550 x3 (all three the
39+
// same `Array.prototype.at` message in `mongodb-findone-options.test.ts`,
40+
// against a `lib` older than es2022). Those 10 are about the CHECK, never
41+
// about the code. Matching vitest is fidelity, not laxity. No `DOM` in
42+
// `lib`: nothing in this layer touches a browser global.
43+
// - ⛔ STRICTNESS IS UNTOUCHED. `strict`, `noUnusedLocals`,
44+
// `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`
45+
// are inherited from the root config, and `types: ["node"]` restates
46+
// `tsconfig.json`'s — load-bearing for the same reason its comment gives,
47+
// since the `.test.ts` files call the `setTimeout` / `console` users in
48+
// `src/test-mongod.ts` and use those globals themselves. Nothing here may
49+
// loosen a type rule; if a test does not compile, that is the finding.
50+
// ⛔ Not one `any` and not one `@ts-expect-error` was added to any test file
51+
// to open this gate — that shape is what turns a real gate into a phantom.
52+
// - ⚠️ `noUnusedLocals` / `noUnusedParameters` are worth naming explicitly:
53+
// they are `true` at the root and `false` in `driver-turso`'s own overrides
54+
// but NOT in this package's, so this layer meets STRICTER settings than the
55+
// sibling driver whose clean state might otherwise be read as a prediction.
56+
// Measured: neither fires here, in either direction. Zero of the 10.
57+
// - `rootDir` is INHERITED as `./src` and deliberately not widened. Like
58+
// `packages/runtime` and unlike `objectql` / `spec`, this layer produces no
59+
// TS6059: measured, every file this program admits is already under `src`
60+
// (all 30 test files sit beside the sources they exercise). Widening it "to
61+
// be safe" would admit files the build config does not and change what the
62+
// gate judges, so it stays as the build config has it.
63+
//
64+
// ⭐ MEASURED at 6ed4b811af, dependency closure built first (an error count
65+
// taken against an unbuilt closure is not a reading — unresolved-import
66+
// cascades inflate it): this program reports **0 errors across 30 files**, from
67+
// a raw 10 under the inherited NodeNext semantics. Every one of the 10 was
68+
// config-tier and none survives; the residue is EMPTY. So unlike `objectql`,
69+
// `runtime`, `spec` and `core`, this package needs **no**
70+
// `test-typecheck-debt.json` at all, and carries none — which is the strongest
71+
// form of this gate: all 30 files are unledgered, so any error any one of them
72+
// ever gains is red on arrival, starting today.
73+
//
74+
// That empty residue is also why the `TEST_DEBT` entry in
75+
// `scripts/check-type-check-coverage.mjs` GRADUATES in the same change rather
76+
// than being paid down. Its recorded 10 and its recorded composition
77+
// ("TS1309 x7, TS2550 x3") match this file's raw reading exactly, which is the
78+
// cleanest possible confirmation that the ledger was measuring the CHECK's
79+
// misconfiguration and never a defect in the tests.
80+
{
81+
"extends": "./tsconfig.json",
82+
"compilerOptions": {
83+
"noEmit": true,
84+
"module": "esnext",
85+
"moduleResolution": "bundler",
86+
"lib": ["ES2022"],
87+
"types": ["node"]
88+
},
89+
"include": ["src/**/*"],
90+
"exclude": ["node_modules", "dist"]
91+
}

0 commit comments

Comments
 (0)