Skip to content

Commit b35b33a

Browse files
authored
Merge branch 'main' into claude/issue-12844-app-plugin-artifact-conversion
2 parents 4f3746a + 2b4178a commit b35b33a

109 files changed

Lines changed: 10275 additions & 1501 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
"@objectstack/metadata-core": minor
3+
"@objectstack/objectql": patch
4+
"@objectstack/service-automation": patch
5+
"@objectstack/service-datasource": patch
6+
---
7+
8+
feat(devx,datasource,automation): published `src/**` may only import workspace packages it declares (#10062)
9+
10+
A package's non-test `src/**` was free to import any workspace package,
11+
declared or not, and nothing checked it. The class was filed with one member
12+
and a mitigation — the import was type-only, so nothing reached the emitted
13+
JavaScript and rollup-plugin-dts inlined the declaration rather than naming an
14+
unresolvable module. It grew to four members with no signal, and one of them
15+
killed the mitigation: `service-automation/src/flow-precedence.ts` **value**
16+
imports from `@objectstack/objectql`, which it does not declare, and because
17+
the shared tsup config externalises only `dependencies`/`peerDependencies`, the
18+
bundler answered by inlining objectql's implementation into
19+
`service-automation/dist/index.js` — a second copy of another package's code,
20+
kept correct by build configuration alone.
21+
22+
`pnpm check:undeclared-dep-imports` is the gate, and the per-member fixes here
23+
are decided one at a time rather than by a uniform policy — declaring makes a
24+
coupling real and installable, routing it away removes it, and the two are not
25+
interchangeable:
26+
27+
* **`@objectstack/service-datasource`** now declares `@objectstack/driver-sql`
28+
and `@objectstack/driver-memory` as **dependencies**. Both are loaded through
29+
an *unguarded* `await import(...)` on the postgres, mysql, sqlite and memory
30+
arms, so a consumer reaching one of those paths needed a package it was never
31+
told to install, and would have met `ERR_MODULE_NOT_FOUND` rather than a
32+
diagnosis. The three *guarded* driver arms — `@objectstack/driver-sqlite-wasm`,
33+
`@objectstack/driver-mongodb`, `@objectstack/driver-turso` — are deliberately
34+
left undeclared: each load sits in a `try`/`catch` that answers an absent
35+
package with the fault, the consequence and the install command, and each
36+
rides as an optional install. Declaring them would install them (turso drags
37+
`@libsql/client`'s native bindings) and, measured on this branch, takes a live
38+
assertion out of the tree: `default-datasource-driver-factory.test.ts` reaches
39+
the missing-package arm with no stub precisely because the package does not
40+
resolve from here.
41+
* **`@objectstack/metadata-core`** now owns the ADR-0029 D9.6 provenance pair,
42+
`isCodeArtifactBody` and `isTenantAuthored`, sunk out of
43+
`@objectstack/objectql`'s registry by the same criterion as the write-verb
44+
dispatch predicates and the audit governance table beside them: a second layer
45+
needs the answer and the reverse import would either close a cycle or make the
46+
consumer depend on the whole data engine for one predicate. `objectql`
47+
re-exports `isCodeArtifactBody` from its original path, so its public API is
48+
unchanged; `service-automation` imports it from `metadata-core`, which it
49+
already declared, and its bundle no longer carries a copy of objectql's code.
50+
51+
Two members stay recorded rather than remediated, because the tree already
52+
carries the decision not to declare them together with its reason
53+
(`@objectstack/runtime` → `@objectstack/driver-turso`, whose bare `import()` is
54+
a host-replaceable default thunk under #6268; `@objectstack/rest` →
55+
`@objectstack/objectql`, whose absence must degrade to `501 NOT_IMPLEMENTED`
56+
rather than fail module load). Their ledger rows carry mechanical evidence and
57+
go red the moment that evidence stops holding — in particular, a `type-only`
58+
row reds on the day its import becomes a value import, which is exactly the
59+
transition nothing caught the first time.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/core": patch
3+
"@objectstack/service-cluster": patch
4+
---
5+
6+
docs(core,service-cluster): retire the two docblocks left stale by `IPubSub`'s corrected delivery guarantee (#12836)
7+
8+
#12651 corrected `IPubSub`'s contract docblock: delivery is whatever the
9+
configured driver declares, no shipped driver exceeds at-most-once, a missed
10+
message is EXPECTED, and handlers must be idempotent **and** tolerate loss.
11+
Two docblocks elsewhere still described the world before that correction.
12+
13+
**`@objectstack/core` — `security/authz-invalidation-channel.ts`.** It carried a
14+
paragraph asserting, in the present tense, that the interface docblock "still
15+
says" *At-least-once delivery*, and that repairing it was a `packages/spec`
16+
change filed separately. That filing was #12651 and it has landed, so the
17+
paragraph is now false rather than merely stale — it sends the next reader
18+
looking for a live disagreement between the interface and the drivers that no
19+
longer exists. Replaced with a plain pointer to the interface docblock.
20+
Everything else in that docblock is unchanged: the at-most-once reasoning, the
21+
TTL-is-the-bound rule, and the best-effort-at-the-publish-site note all still
22+
hold.
23+
24+
**`@objectstack/service-cluster` — `memory/pubsub.ts`.** The line "At-least-once
25+
semantics held vacuously (a single in-process delivery)" was wrong on its own
26+
terms even before #12651: the same docblock states that handler errors are
27+
swallowed and logged via `onError`, so a handler that throws loses the message
28+
with no retry and no persistence. That is not at-least-once in any sense, and
29+
"vacuously" does not save it. Replaced with the honest statement — one
30+
synchronous in-process delivery attempt per subscriber, no persistence, no
31+
retry, no replay.
32+
33+
Prose only. No behaviour change, and no test changed.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): retire preview mode — the `'preview'` RuntimeMode value and the whole `KernelContext.previewMode` / `PreviewModeConfig` block (#11846, ADR-0049)
6+
7+
<!-- adr-0087: registered kernel-context-preview-mode-retired -->
8+
9+
**BREAKING** accept-set narrowing and export removal, landing after the
10+
v17.0.0 cut (the lockstep launch-window convention ships it as `minor`; the
11+
prescription is registered under protocol major 18 —
12+
`RETIRED_KEYS_BY_MAJOR[18]` for both walked-shape keys,
13+
`RETIRED_DEFS_BY_MAJOR[18]` for the def, plus the D3 semantic entry
14+
`kernel-context-preview-mode-retired` — where `os migrate meta` users will
15+
look).
16+
17+
The declaration was the sharpest declared-≠-enforced shape on a SECURITY
18+
surface: the schema promised "bypass auth, simulate admin identity" and named
19+
a production guard "the runtime must enforce", and NO code path implemented
20+
either half. Measured zero consumers in objectstack, objectui and cloud
21+
(cloud#1651, closed 2026-08-26 with positive controls: `RuntimeMode` has zero
22+
hits repo-wide there, and `ArtifactKernelFactory` — where preview auto-login
23+
would live if anywhere — has 20+ hits and never touches `previewMode`;
24+
re-verified in objectstack at dispatch, 2026-08-27). An author — very often
25+
an AI — could write the six-key block per the reference docs, parse cleanly,
26+
and get no behaviour and no diagnostic.
27+
28+
FROM → TO:
29+
30+
- `mode: 'preview'` → *(removed value)* — `mode` defaults to `'production'`;
31+
use `'development'` for local demo work. The rejection carries the
32+
prescription via the enum's own error map (the `HookBodyCapability`
33+
precedent); every other mode keeps zod's own message.
34+
- `previewMode: { … }` on `KernelContext` / `TenantRuntimeContext` →
35+
*(removed key)* — tombstoned with `retiredKey()` (the schemas are not
36+
`.strict()`, so a bare deletion would be a silent strip): authoring it is
37+
now a `tsc` error and a parse error carrying the prescription.
38+
- `PreviewModeConfigSchema` / `PreviewModeConfig` / `PreviewModeConfigParsed`
39+
→ *(removed — no replacement)*. The def described behaviour no layer
40+
implemented; an exported value schema with no consumer reads as a
41+
capability (#3950).
42+
43+
One-line fix: delete the key and the value — neither ever changed runtime
44+
behaviour, so removing them changes nothing observable. Preview deployment
45+
ROUTING is untouched: `OS_PREVIEW_MODE` / `OS_PREVIEW_BASE_DOMAINS` keep
46+
working exactly as documented (deployment routing, never identity). If a
47+
preview experience becomes a product capability it re-declares fresh, with
48+
the production-posture hard-refusal as the first-landed half (#11846 ruling
49+
record, maintainer 2026-08-27).
50+
51+
The retirement kit:
52+
53+
- tombstones at both declarations (`kernel/KernelContext:previewMode` and the
54+
`.extend()` copy `kernel/TenantRuntimeContext:previewMode`, both in
55+
`RETIRED_KEYS_BY_MAJOR[18]`); the enum value's prescription on
56+
`RuntimeMode`'s error map (enum-VALUE retirements register nothing in
57+
RETIRED_KEYS_BY_MAJOR and leave the surface ratchets byte-identical)
58+
- whole-def deletion `kernel/PreviewModeConfig` in `RETIRED_DEFS_BY_MAJOR[18]`
59+
(manifest key deliberately removed; the #4725 gate adjudicated it)
60+
- deliberately NO D2 conversion: a kernel context is constructed by host code
61+
at boot — not a stack collection member, never a `sys_metadata` row — so
62+
the conversion chain has no seam that would ever see one (the
63+
`kernel/Manifest:loading` disposition); the D3 semantic entry carries the
64+
prescription
65+
- pin tests (`kernel/preview-mode-retirement.test.ts`): both rejection sites
66+
flip from silent parse to the prescription; zero holders for all 3 retired
67+
export names on every public entry; the carrier schemas survive
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): a failed `sys_organization` tenant scan no longer decides a retention window (#12853)
6+
7+
`LifecycleService.loadGovernance()` filled `snapshot.tenantOverrides` — the
8+
ADR-0057 §3.2 per-tenant retention/expiry window set — behind a bare `catch {}`
9+
whose comment named ONE benign cause ("No sys_organization (single-tenant
10+
kernel)") while the `catch` swallowed every cause. On a connection drop, a
11+
timeout, a permission refusal or a driver fault the map came back EMPTY, and an
12+
empty map is not a neutral value: `reap()` and `archiveObject()` read it as
13+
"this deployment has tuned no tenant" and fall every tenant back to the global
14+
window. That window is wrong in both directions, and the expensive direction is
15+
a tenant configured to retain LONGER having its rows expired early. Nothing
16+
reported it: `GovernanceSnapshot` carries no field saying the tenant pass did not
17+
complete, and the catch logged nothing — so the platform deleted on knowingly
18+
incomplete evidence, without knowing the evidence was incomplete.
19+
20+
The scan now discriminates by error TYPE through the shared
21+
`isMissingTableError` predicate. An unprovisioned `sys_organization` really does
22+
mean "no tenant overrides", so a single-tenant kernel is unchanged. Every other
23+
cause aborts the sweep **before any policy is applied** — for a deletion action,
24+
"do not act on incomplete evidence" is the correct failure direction, and a log
25+
cannot bring back a reaped row. The rows a deferred sweep leaves are still there
26+
for the next one.
27+
28+
Operational posture change, deliberate and worth stating: a transient
29+
`sys_organization` outage now costs a sweep. The abort is REPORTED, not thrown —
30+
one `report.errors` entry per declared object plus a `warn` — because `sweep()`'s
31+
declared contract is that it never throws and the scheduler enters it as
32+
`void this.sweep()`, where a rejection would be unhandled. That is the same
33+
objection #8906 recorded when it declined to rethrow from `checkGovernance` one
34+
method below.
35+
36+
Bump argued, not defaulted: `patch`. No exported signature, type, option or
37+
report field moves — the failure surfaces through `LifecycleSweepReport.errors`,
38+
which already exists for exactly this. The tension is honest and does not change
39+
the answer: what a deployment observes on a failure path DOES change (a sweep
40+
that used to complete silently now aborts and says so), but that is the
41+
correction of a defect, not a new capability, and the sibling repairs in this
42+
family (#8896, #8906, #9817) all shipped as `patch`.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/plugin-security": minor
3+
---
4+
5+
feat(security): the referential FK-clear write is exempt from the object-level CRUD check (#12597)
6+
7+
**This changes which deletes succeed** — an observable behavioural contract
8+
change on the delete path, which is why it ships `minor` rather than as a
9+
patch-grade defect repair.
10+
11+
Deleting a record makes the engine clear every optional lookup that points at it
12+
(`deleteBehavior: 'set_null'`). That cleanup `UPDATE` is engine-owned referential
13+
integrity, and it has carried the server-derived `__referentialFieldClear` marker
14+
since #3023 — but the marker reached only the ownership-anchor guard, so the
15+
write still had to pass the **object-level CRUD check** on the referencing
16+
object. Consequence, measured on a real deployment across 17 role×object pairs: a
17+
role with full delete rights on A and no grant at all on B could delete an A only
18+
while B was **empty**. The moment a real row referenced it, the delete failed with
19+
one generic "you do not have permission", and nothing on any permission screen
20+
showed that deleting A also required write authority on B.
21+
22+
**What is exempt: the object-level CRUD grant check, and nothing else.** A marked
23+
`update` skips that one gate (both the caller's grant and the ADR-0090 D10
24+
delegator half of the same question). Everything else in the security middleware
25+
runs unchanged and is pinned test-by-test:
26+
27+
- field-level security on the FK column still refuses;
28+
- the RLS `using` row scope on the referencing object still refuses;
29+
- the RLS post-image `check` still refuses — so a deployment declaring
30+
`product != null` keeps getting a truthful refusal instead of a silent clear;
31+
- declared validation rules keep firing (they were never in this path);
32+
- a caller without delete rights on the target is still refused;
33+
- an ordinary, unmarked update on the referencing object is untouched.
34+
35+
⛔ Deliberately **not** `isSystem`: that bypass is total (see
36+
`content/docs/permissions/system-context.mdx` — "Elevation is total, and it is not
37+
granular"), and it would have switched off all three guards above. ⛔ The
38+
`cascade` arm — deleting whole referencing rows — is **unchanged** and still
39+
requires the caller's own delete authority on those rows.
40+
41+
The write is not elevated at all, so audit attribution is unchanged: the cleanup
42+
`UPDATE` still runs under the operator's identity and lands in the ledger as that
43+
operator (`user_id` / `actor`, and the `updated_by` stamp).
44+
45+
No authorable surface changes, and no metadata needs migrating: a deployment that
46+
was working around this by granting write access on referencing tables can narrow
47+
those grants, but nothing forces it to.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/core": minor
3+
---
4+
5+
feat(core): retire the inert `PluginMetadata` surfaces — `configSchema` with `PluginConfigValidator`, and `hotReloadable` (#11982, #12587, ADR-0049)
6+
7+
<!-- adr-0087: not-required (runtime-interface-only packages/core/src/plugin-loader.ts#PluginMetadata) PluginMetadata is a runtime TS interface in packages/core with no Zod schema, no spec declaration and no stored representation; no metadata surface references it (the PluginMetadata in packages/spec/src/kernel/plugin-validator.zod.ts is an unrelated locally-declared homonym). The deleted PluginConfigValidator / createPluginConfigValidator were runtime classes in the same non-metadata module family, so `objectstack migrate meta` has nothing to rewrite; the compiler is the notification channel — TS2353 on the removed fields, TS2305 on the removed exports. -->
8+
9+
**BREAKING**: removes a published-but-inert capability from the `.` entry of
10+
`@objectstack/core`. Shipped as `minor` under the lockstep launch-window
11+
convention (a `major` bump is refused repo-wide by `check:changeset-no-major`).
12+
13+
Removed, each measured at zero live consumers with positive controls (the
14+
sibling `startupTimeout` is read live by the kernel's startup timeout guard);
15+
maintainer ruled retire under ADR-0049 enforce-or-remove, 2026-08-27,
16+
decision-inbox batch 5; recorded in ADR-0025 §3.7:
17+
18+
- `PluginMetadata.configSchema` — declared "Configuration schema for
19+
validation", but the mechanism could never run: the loader's only call
20+
passed no config, and no caller could — plugin factories close over their
21+
config, so the kernel never receives it. Every one of ~40 production
22+
`kernel.use()` compositions already passes config as constructor arguments
23+
and works.
24+
- `PluginConfigValidator` / `createPluginConfigValidator` — the validator
25+
behind that field: real code with zero reachable invocations, deleted along
26+
with its unit test and its export from the security barrel.
27+
- `PluginMetadata.hotReloadable` — declared "Whether plugin supports hot
28+
reload" with zero reads and zero declarations: `HotReloadManager.reloadPlugin`
29+
gates only on its own registered reload configs, so `hotReloadable: false`
30+
was hot-reloaded identically to `true`.
31+
- The `packages/core/ADVANCED_FEATURES.md` example whose inline comment
32+
promised "Config is validated before init is called" — false on the
33+
retired ref, and the retired surface's only in-repo declaration site.
34+
35+
One-line fixes, per symbol. If you declared `configSchema` on a plugin:
36+
delete the field and parse your config at the plugin's own seam —
37+
`MyConfigSchema.parse(options)` in the plugin factory or constructor, the
38+
pattern `packages/rest` uses. If you imported `PluginConfigValidator` or
39+
`createPluginConfigValidator`: delete the import and hold your own
40+
`schema.parse` call; the compiler (TS2305) locates every such site. If you
41+
declared `hotReloadable`: delete the field — it never gated anything, and
42+
hot-reload participation remains governed solely by
43+
`HotReloadManager.registerReloadConfig`.
44+
45+
Re-declaring a kernel-owned config-validation surface is a fresh decision for
46+
the day ADR-0025's plugin distribution layer lands, with #11982's zero-caller
47+
measurement as its starting evidence.

0 commit comments

Comments
 (0)