Skip to content

Commit 1b00a88

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-2f5vpq
2 parents 50dae82 + be78977 commit 1b00a88

45 files changed

Lines changed: 2068 additions & 660 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/service-storage': patch
3+
---
4+
5+
fix(storage)!: an anonymous write carries no file
6+
7+
Ruling C ("an anonymous submission carries no file") now holds at every anonymous write door of ADR-0138 D2, not only at the public form door. A record write whose caller has no user, such as the guest an `authRequired: false` endpoint executes as under the grants a deployment binds to the guest, may not put a value in a file field (`file`, `image`, `avatar`, `video`, `audio`). It is refused before anything is written, with the form door's envelope: `400 VALIDATION_FAILED`, one `fields[]` entry per refused field carrying the ADR-0114 field code `invalid_value` and the field's label. An empty or absent file field is written as before. Signed-in callers, system writes and context-less programmatic writes are unchanged. No error code is added.
8+
9+
Clause-②: no (narrowing) — an anonymous write that carried a value in a file field is refused. Nothing that was refused becomes accepted.
10+
11+
Metadata-protocol: none (a write path's file rule; nothing a metadata author writes changes)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition, stored row or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is which record writes the file-reference path accepts (an anonymous caller's write carrying a file value is refused). The remedy is a change of who provides the file (a signed-in caller), never a rewrite of a package source or a stored row. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
14+
15+
**BREAKING** for an anonymous write that carried a file value, shipped as `patch` because the metadata protocol does not move.
16+
17+
- **Before.** Only the public form door refused a file value from an anonymous caller.
18+
- **Now.** Every write whose caller has no user is refused with `400 VALIDATION_FAILED` when a file field carries a value.
19+
- **Fix.** Leave file fields empty in an anonymous write. A file is provided by a signed-in caller, through the authenticated upload doors.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/core': patch
3+
'@objectstack/objectql': patch
4+
'@objectstack/service-package': patch
5+
'@objectstack/cloud-connection': patch
6+
'@objectstack/metadata-core': patch
7+
---
8+
9+
feat(install): a package that declares an installed app in `manifest.dependencies` may share that app's namespace, and a boot hydrates installed packages in dependency order (ADR-0141 D5)
10+
11+
Clause-②: yes (narrowing)
12+
Metadata-protocol: none
13+
14+
<!-- adr-0087: not-required (no-migration-prescription) No key of any metadata schema is added, removed, renamed or re-shaped: `manifest.dependencies` keeps its record shape and meaning. The one narrowing is an install-local acceptance detail of the not-yet-released ADR-0141 D2 check (an artifact wrapper's own id no longer counts as a delivered package), plus the matching removal of the unreleased `DeclaredDependencyCheck.deliveredIds` option; there is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->
15+
16+
**Namespace co-ownership by declaration.** The install gate (`SchemaRegistry.installPackage`) used to admit packages into one namespace only when one release artifact delivered them together. A module shipped as its own artifact that uses its app's namespace was refused `422 NAMESPACE_CONFLICT` beside the installed app, whatever it declared. Now an installed package P co-owns namespace N with:
17+
18+
- an installed X that P declares in `manifest.dependencies`, when both use N;
19+
- a sitting owner of N that declares P, which is the same relation read from the other end, so an upgrade or reinstall of the app beside its modules is admitted;
20+
- a sibling S, when S and P each declare one installed package that holds N (two modules of one app, neither declaring the other).
21+
22+
Only direct declarations count. The shared parent must be installed and must hold N itself.
23+
24+
The object-name check reads the same co-owner set. A co-owner by declaration that defines an object name another co-owner already owns is refused `422 DUPLICATE_ARTIFACT_OBJECT_NAME` before any DDL. Examples: a module redefining its app's object, a sibling redefining the other sibling's object, or an app upgrade newly defining a name one of its modules owns. The message is worded per source (same artifact, declared dependency, sibling). `NAMESPACE_CONFLICT` now names the declaration as the fix for a package that extends the app holding the namespace.
25+
26+
**Hydrate order.** The `sys_packages` hydrate and the install-local `kernel:ready` rehydrate used to register in source-listing order. Now they sort their batch by `manifest.dependencies`, with declared ids as optional edges, through `resolvePluginOrder`. Before this, a module that hydrated before the app whose namespace it shares could be refused and stay absent for the process lifetime. In the install-local rehydrate, each ledger entry is one node, so a declared id resolves to the entry that carries that package. A dependency cycle never stops a hydrate: its members keep source order and an error names them.
27+
28+
**BREAKING** (an install-acceptance narrowing of the unreleased ADR-0141 D2 check) at `POST /api/v1/marketplace/install-local`. A multi-package artifact's own wrapper id no longer counts as delivered unless one of its packages carries it. A module declaring the wrapper id is judged like any other entry, because it would be `absent` on its first standalone install. `DeclaredDependencyCheck.deliveredIds` is removed with it. Also, an undecided range union whose installed version is readable is now reported `found: 'unreadable-range'` instead of `'unreadable-version'`; for example, `<5 || latest` against `7.0.0`.
29+
30+
**What changes for you.**
31+
32+
- A module that extends an app and uses its namespace declares the app in `manifest.dependencies`, and then installs beside it as its own artifact. Two modules of one app need not declare each other.
33+
- Every package of the app's own artifact that uses the namespace should declare the app too.
34+
- New exports: `@objectstack/core` exports `orderHydrationBatch` and `declaredDependencyIds`.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
fix(service-automation): a `notify` node naming `email` and another channel mails an address no user holds and no longer writes an inbox row addressed to it
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
An email address written out in `recipients` is the author's constant: on a node whose `channels` name `email` it is mailed as written. The inbox delivers to a user, keying its row by the recipient as a user id, and so does every channel but `email`. So on a node whose `channels` name `email` and another channel, such as `['inbox', 'email']`, an address no user holds now goes to the email channel alone.
11+
12+
- **Fixed.** Before, the address went to every channel as it stood: the email channel mailed it, and the inbox also wrote a `sys_inbox_message` row whose `user_id` was the address, which no inbox reads. Now the node emits that address apart, on the `email` channel only, and the inbox writes no row for it. `user:ADDRESS` is handled the same way, whoever holds the address, because `user:` hands the address on as the user id with no lookup. An address whose holder cannot be looked up (no data engine, a lookup that fails) is mailed alone too.
13+
- **Unchanged.** The node succeeds as before. An address a user holds, written bare, still reaches that user's inbox and is mailed. A value expression that evaluates to an address on a node naming `email` still resolves to the user who holds it, or fails the node pointing to `{ emailField }`. A node that does not name `email` still fails on an address no user holds.
14+
- **What a run records.** When a node's recipients mix users and such addresses, the node writes two `sys_notification` events: one for the users on the node's `channels`, which the node's `notificationId` output names, and one for the addresses on `email` alone. The `delivered`, `enqueued` and `failed` outputs add up both.
15+
- **Who moves.** Nobody needs to change a flow. A report or audit over `sys_inbox_message` no longer sees rows whose `user_id` is an address.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): the flow inspector's list field shows a stored string as one entry instead of an empty list
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A notify node's `channels` may be a single string (`channels: 'email'`) or a list; the spec accepts both and the runtime reads the string as a one-item list. Studio's flow inspector read any non-list value as empty, so the field showed no channels and adding one saved a list without `email`. A stored non-blank string now reads as a one-entry list (trimmed, as the runtime does). An edit saves the list form (`['email', 'sms']`); a field left untouched keeps the stored string.

‎README.md‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -248,17 +248,18 @@ git clone https://github.com/objectstack-ai/objectstack.git
248248
cd objectstack
249249
pnpm install # Node 22+, pnpm 10 (corepack enable)
250250
pnpm build # build all packages
251-
pnpm objectui:build # build the Console SPA (not part of pnpm build)
251+
pnpm console:build # put the Console SPA in packages/console/dist
252252
pnpm dev # showcase example: REST + Console on :3000
253253
```
254254

255-
`pnpm objectui:build` builds [objectui](https://github.com/objectstack-ai/objectui)
256-
at the commit pinned in `.objectui-sha` into `packages/console/dist`
257-
(gitignored). It builds from a `../objectui` checkout if one sits next to this
258-
repo, otherwise from a shallow clone of objectui into `.cache/`; either way it
259-
installs objectui's dependencies, so it needs network. Skip it and `pnpm dev`
260-
still serves the REST API, without the Console. Rerun it when `.objectui-sha`
261-
moves.
255+
`pnpm console:build` builds the Console from its source in this workspace
256+
(`ui/apps/console`, against this tree's own spec and client) and copies the
257+
result into `packages/console/dist` (gitignored), which is where `pnpm dev`
258+
serves it from. Nothing is cloned or pinned. `pnpm build` builds the Console
259+
too but does not copy it, so after `pnpm build` this step is a copy. Skip it
260+
and `pnpm dev` still serves the REST API, without the Console. Rerun it after
261+
pulling changes under `ui/`: `packages/console/dist` is never refreshed on its
262+
own.
262263

263264
A first run is slow, not stuck. Measured once on a fresh clone of `main`
264265
(4-vCPU container, Node 22.22, pnpm 10.31):
@@ -267,7 +268,7 @@ A first run is slow, not stuck. Measured once on a fresh clone of `main`
267268
|:---|---:|:---|
268269
| `pnpm install` | 16 s | |
269270
| `pnpm build` | 6 m 54 s | 72 turbo tasks |
270-
| `pnpm objectui:build` | 10 m 36 s | clones objectui and builds the Console |
271+
| `pnpm console:build` | 12 m 16 s | 72 turbo tasks: the Console and what it depends on, run before any `pnpm build` (`--concurrency=2`); a cache hit plus a copy, about 1 s, once `pnpm build` has run |
271272
| `pnpm test` | 54 m 03 s | the full suite: 135 turbo tasks |
272273

273274
For your own change, test what it reaches instead of the full suite:

‎content/docs/deployment/app-dependencies.mdx‎

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@ The declaration does two things:
4040
reference, not the data: users still see the other app's records under that
4141
app's own permission sets and sharing rules.
4242
- **It is checked before install** — below.
43+
- **It lets your package share the depended-on app's namespace** — below.
4344

4445
Every entry is required; there is no optional arm. Declare only the packages
45-
your app actually references.
46+
your app actually references. Two modules of one app need not declare each
47+
other: each declares the app, and that is enough for both to install, in either
48+
order, and to share the app's namespace.
4649

4750
## The version-range grammar
4851

@@ -125,7 +128,11 @@ The check runs at those install acts only. It does not run:
125128

126129
- **at boot.** A restart re-registers what an install already admitted, while
127130
the registry is still filling; it never refuses a package for a dependency it
128-
has not seen yet.
131+
has not seen yet. It does register them in dependency order: the stored
132+
packages, and the apps installed through `os package install`, are each sorted
133+
so a package registers after the packages it declares. Two packages that
134+
declare each other are a cycle: they register in stored order for that boot,
135+
and the server logs an error naming them.
129136
- **at `PATCH /api/v1/packages/:id`.** Editing an authored package's
130137
`dependencies` after install is applied as given; an entry added there is
131138
judged when the package is next installed.
@@ -134,12 +141,35 @@ The check runs at those install acts only. It does not run:
134141

135142
## Sharing a namespace with the app you depend on
136143

137-
Each package's namespace must still be its own across artifacts: a package that
138-
uses the namespace of the app it depends on is refused at install with
139-
`NAMESPACE_CONFLICT`, exactly as before. Inside one artifact, packages may share
140-
a namespace ([ADR-0130](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0130-release-artifact-as-co-ownership-boundary.md)).
141-
Dependency-declared namespace co-ownership across artifacts (ADR-0141 D5) is
142-
not delivered yet.
144+
A namespace is otherwise one package's: a package using a namespace another
145+
installed package holds is refused at install with `NAMESPACE_CONFLICT`. Two
146+
relations make packages co-owners of one namespace instead:
147+
148+
- **the same artifact** delivers them ([ADR-0130](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0130-release-artifact-as-co-ownership-boundary.md)); or
149+
- **a declared dependency**: your package declares an installed app that uses the
150+
same namespace — or another installed package that declares that same app
151+
uses it too (two modules of one app).
152+
153+
So a module that ships as its own artifact, declares `app.objectstack.hotcrm`
154+
and uses `crm` installs beside HotCRM, and a second such module installs beside
155+
both. The relation is one-way — the app declares nothing — and it is read from
156+
either side, so upgrading or reinstalling the app beside its modules is
157+
admitted too. A package that shares the namespace without declaring the app is
158+
still refused, and the fix the error names is the declaration.
159+
160+
Sharing a namespace never means sharing an object name. A package defining an
161+
object another co-owner already defines — your module redefining
162+
`crm_account`, two modules both defining `crm_invoice`, or an app upgrade newly
163+
defining a name one of its modules owns — is refused at install, before any
164+
table is touched, with `422 DUPLICATE_ARTIFACT_OBJECT_NAME` naming both
165+
packages and the object. Rename the object, or `extend` the other package's
166+
object instead of owning a second one.
167+
168+
Every package of the depended-on app's own artifact that uses the namespace
169+
should declare the app too (inside the artifact it costs nothing: it is judged
170+
for presence only). The platform does not record which artifact an installed
171+
package came from, so an undeclared companion is a sitting owner your module
172+
cannot co-own with.
143173

144174
## Build-time validation cannot see a dependency's objects
145175

‎content/docs/getting-started/examples.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ Each script starts one example's dev server. `pnpm dev` is an alias for `pnpm de
6464
`pnpm install` alone leaves the workspace unbuilt. This is the same sequence
6565
`README.md` documents.
6666

67-
`pnpm build` still does not produce the Console SPA at `/_console/` — that is built
68-
separately by `pnpm objectui:build`.
67+
`pnpm build` still does not put the Console SPA served at `/_console/` in place —
68+
`pnpm console:build` does, copying it into `packages/console/dist`.
6969
</Callout>
7070

7171
---

‎content/docs/protocol/kernel/plugin-spec.mdx‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -469,12 +469,16 @@ Use for large dependencies (React, Vue) that should be shared across plugins.
469469

470470
<Callout type="warn">
471471
**The kernel does not check these range strings at plugin load — installing a package
472-
does.** `manifest.dependencies` has three consumers. Two read `Object.keys(...)` only — the
473-
ids, never the ranges: `resolveArtifactPackageOrder` (`packages/core/src/artifact-packages.ts`)
474-
hands the ids to `resolvePluginOrder` as `optionalDependencies`, so an id naming a sibling
475-
package in the same artifact orders ahead of it and an id naming anything else is skipped;
476-
and `resolveWritePackageScope` (`packages/metadata-protocol/src/protocol.ts`) walks the ids
477-
transitively for the write-scope gate. The third is the pre-install dependency check every
472+
does.** `manifest.dependencies` has five consumers. Four read the ids only, never the ranges:
473+
`resolveArtifactPackageOrder` (`packages/core/src/artifact-packages.ts`) hands the ids to
474+
`resolvePluginOrder` as `optionalDependencies`, so an id naming a sibling package in the same
475+
artifact orders ahead of it and an id naming anything else is skipped;
476+
`resolveWritePackageScope` (`packages/metadata-protocol/src/protocol.ts`) walks the ids
477+
transitively for the write-scope gate; the install gate (`SchemaRegistry.installPackage`,
478+
ADR-0141 D5) admits a package that declares an installed app into that app's namespace, and
479+
two packages declaring one such app into it beside each other; and both boot hydrates sort
480+
their batch with the same optional edges (`orderHydrationBatch`,
481+
`packages/core/src/hydration-order.ts`). The fifth is the pre-install dependency check every
478482
install door runs (ADR-0141 D2): each entry must name a package installed inside its range,
479483
or one the same artifact delivers, or the install is refused `422 DEPENDENCY_MISSING`. That
480484
check reads ranges with `rangeAdmitsVersion` (`packages/metadata-core/src/protocol-handshake.ts`),

0 commit comments

Comments
 (0)