Skip to content

Commit 23d4084

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/d1-d2-strict-default-flip-vbly5z
# Conflicts: # packages/spec/src/system/migration.zod.ts
2 parents 4817336 + d6938bf commit 23d4084

102 files changed

Lines changed: 3443 additions & 1060 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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
docs(adr-0104): record the fresh-datastore attestation seam as implemented — driver-reported created-vs-found table counts, not "sys_migration is missing" (which is the 16→17 trap) — and note that born-attested dogfood now exercises the #3459 collection path on every boot. Releases nothing.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/platform-objects": minor
4+
"@objectstack/objectql": minor
5+
"@objectstack/driver-memory": minor
6+
"@objectstack/driver-sql": minor
7+
"@objectstack/service-storage": minor
8+
---
9+
10+
feat(migrate): a datastore created from empty attests its data migrations at creation (#3438, ADR-0104 2026-07-30 addendum)
11+
12+
Deployment-level migration flags could only be recorded by running
13+
`os migrate`. That left a hole at the other end of a deployment's life: a
14+
database created on a version that already ships the migrations started **lax**
15+
and stayed lax until someone thought to run a command that, for them, converts
16+
nothing and finds nothing. Every new deployment re-entered the warn regime, so
17+
the warn regime would never die out — and, since #3459, every new deployment
18+
also kept every released file forever.
19+
20+
A store the platform **creates from empty** now records
21+
`adr-0104-file-references` and `adr-0104-value-shapes` at that moment. Nothing
22+
to run; enforcement and collection are live from the first boot.
23+
24+
**This is not version-gating in disguise.** The fact recorded — no legacy value
25+
is stored here — is *observed*: the store had no history at all. The platform
26+
attests only what it watched itself create, and the test is deliberately
27+
strict: every table made by this boot and **none found already present**. One
28+
pre-existing table anywhere, one datasource that was already there, one driver
29+
that cannot account for its schema sync — any of those and the deployment
30+
attests nothing and produces its evidence by scan, exactly as before. "Found
31+
empty" and "created empty" are not the same claim, and only the second is an
32+
observation.
33+
34+
**New surfaces.** `IDataDriver.getSchemaSyncStats?()` (optional, purely
35+
observational: tables created vs found since connect — implemented by the SQL
36+
and in-memory drivers), `engine.wasDatastoreCreatedFromEmpty()`,
37+
`attestFreshDatastore()` in `@objectstack/platform-objects/system`, and
38+
`VALUE_SHAPES_MIGRATION_ID` / `CREATION_ATTESTED_MIGRATION_IDS` in
39+
`@objectstack/spec/system`. Attestation never overwrites an existing flag row
40+
and never throws into a boot: a failure leaves the deployment lax, which a
41+
migration run can still fix.
42+
43+
**Upgrading changes nothing for an existing database.** It is non-empty when
44+
the platform reaches it, so it is never attested — run
45+
`os migrate files-to-references --apply` as before. Importing legacy values
46+
into an attested deployment is rejected loudly at the write path;
47+
`OS_ALLOW_LAX_MEDIA_VALUES=1` re-opens leniency while you diagnose.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
---
5+
6+
Reconcile the remaining flat builtins' declared config against what their
7+
executors read (#4045 — the CRUD / screen / map step, after notify / http /
8+
connector in #4210).
9+
10+
**Six executor-derived Zod contracts.** `GetRecordConfigSchema`,
11+
`CreateRecordConfigSchema`, `UpdateRecordConfigSchema`,
12+
`DeleteRecordConfigSchema`, `ScreenConfigSchema` (+ `ScreenFieldConfigSchema`)
13+
and `MapConfigSchema` in `automation/builtin-node-config.zod.ts`, each written
14+
by reading the executor rather than transcribing the descriptor literal, so the
15+
new bidirectional ledger test is evidence rather than a tautology. Contract
16+
exports only — nothing parses with them yet (#4045 step 3b, gated on the #4059
17+
warning data).
18+
19+
**Seven capabilities the executors honour are now authorable.** Each was read
20+
by the executor and offered by no form — online or offline — so it was reachable
21+
only by hand-written metadata:
22+
23+
- `get_record.fields` — the query projection, passed straight into
24+
`find`/`findOne`;
25+
- `screen.recordId` — the record `mode: 'edit'` opens; the form declared the
26+
edit mode while offering no way to name its target;
27+
- `screen.fields[].options` / `defaultValue` / `placeholder` — all three
28+
forwarded into the ScreenSpec the client renders, so a select field's choices
29+
could not be authored in Studio at all. Same nested repeater position as the
30+
`visibleWhen` gap #3528 was filed for;
31+
- `map.indexVariable` and `map.input` — the index binding and the per-item
32+
subflow params.
33+
34+
**`map`'s undeclared `flow` alias graduates to the conversion layer.** The
35+
executor carried `cfg.flowName ?? cfg.flow` for a spelling no schema ever
36+
described — the `notify.source` shape (Prime Directive #12). The bare fallback
37+
is deleted and `flow-node-map-flow-alias` (protocol 17, retires at 18) renames
38+
it at load, including the `AutomationEngine.registerFlow` rehydration seam.
39+
40+
**`assignment` is pinned as deliberately un-reconcilable**, with the reason on
41+
record: with no `assignments` wrapper its top-level config keys ARE the author's
42+
variable names, so no fixed key set can describe it and a catchall Zod would
43+
reconcile vacuously. What the ledger pins instead is that the form offers
44+
exactly the canonical `assignments` map and that the map stays open.
45+
46+
With this, every builtin that publishes a `configSchema` is reconciled against
47+
its executor, and the ones that publish none each have a recorded reason.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/runtime': major
3+
---
4+
5+
A dispatcher domain whose route is mounted but whose implementation is absent answers **501**, not a 404 that blames the route (#4093 follow-up).
6+
7+
Two different facts were being answered with whatever each domain happened to reach for, and only `mcp` told them apart:
8+
9+
- **The route is not there.** `/mcp` when the server is disabled for the environment; `/analytics` when the service is unserveable, because `dispatcher-plugin` gates the *mount* and never registers those paths (#4000). A path the server does not expose is a **404** from the host's own router. **Unchanged** — that half was already right.
10+
- **The route is there; the implementation is not.** Every unconditionally-mounted domain. The request reached a handler that had nothing to delegate to. That is **501**, and it is what changes here.
11+
12+
`/automation` and `/notifications` returned `{ handled: false }`, which looks neutral but lands on the dispatcher plugin's single exit: `404 ROUTE_NOT_FOUND` with the hint *"No handler matched this request. Check the API discovery endpoint for available routes."* Both halves were false — a handler did match, and discovery correctly does not list the route, so the hint pointed at a page that would never mention it. An operator reads that as a routing bug and goes looking for one that does not exist.
13+
14+
`/ui` answered **503**, which claims the condition is temporary. An uninstalled MetadataPlugin does not become installed by retrying.
15+
16+
`/ai` answered **404** for the same mounted-but-unimplemented case.
17+
18+
The refusal now carries the same remedy sentence discovery reports for that slot (`serviceUnavailableMessage`, shared via `@objectstack/spec/system`), so the wall and the discovery entry cannot drift into naming different fixes — `POST /api/v1/automation` on a stack without the service answers `501 Install @objectstack/service-automation to enable`. `/ai` keeps a local message: its real provider ships outside this workspace as a Cloud/EE package, so the shared table — verified against workspace packages — records no entry and would otherwise describe it as "nothing ships".
19+
20+
Deliberately unchanged: `analytics`'s route-mount gate (a genuinely absent path); `mcp`'s 404-vs-501 pair, which is the model; the `handled: false` at the END of each domain, which means "no sub-route matched" and is a true 404; `GET /ai/agents`'s empty-list 200, a deliberate courtesy for the console's per-navigation poll; and `/ai`'s `503 routes not yet initialized`, which is a different condition (service present, internal state unready) and may genuinely be transient.
21+
22+
FROM → TO: requests to `/automation`, `/notifications`, `/ui/*` and `/ai/*` on a deployment lacking the backing service now get `501` (with the package to install) instead of `404`/`503`. Anything branching on the old status should branch on `status >= 400` or read the error code; the capability was equally unavailable before, so no working flow changes. Discovery is unaffected — it already reported these slots `unavailable` and advertised no route for them.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
fix(rest): `GET /data/:object/export` honours a `search` term
6+
7+
The streaming export route accepted `filter` and `orderby` but had no way to
8+
carry the term a user had typed into the list's search box. So exporting after
9+
a search downloaded the **unsearched superset** — more rows than the screen
10+
showed, in a file that looks authoritative, with nothing indicating the
11+
difference. The route's own comment claimed the opposite: that it "mirrors the
12+
active view's filter + sort so the exported file matches what the user sees".
13+
14+
Same family as a dropped filter (objectstack#3948, objectstack#4181): a
15+
plausible answer that is quietly broader than the one asked for.
16+
17+
Two new query params, both matching the list endpoint's semantics:
18+
19+
- `search=<term>` — folded into `findData` as `$search`, so it **composes**
20+
with `filter` (`{ $and: [filter, search] }`) rather than replacing it. Empty
21+
or whitespace-only terms are ignored rather than applied as a blank predicate.
22+
- `searchFields=a,b` — the ADR-0061 override for which fields the term scans.
23+
Only meaningful alongside `search`, and intersected with the object's allowed
24+
searchable set by the engine, exactly as on the list endpoint.
25+
26+
Unknown query params on this route were already ignored, so a client that sends
27+
`search` to an older server gets today's behaviour rather than an error.
28+
29+
Covered by `export-integration.test.ts` against the real engine + protocol: the
30+
composition case is built so each half alone returns a different non-empty
31+
result and only "both applied" returns none. Reverting the route change fails 4
32+
of the tests. The file's in-memory driver also learned `$or` / `$contains`
33+
without them a search predicate is a silent no-op and an "it filtered"
34+
assertion would pass for the wrong reason.

.changeset/nav-item-input-type.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
fix(spec): `defineApp` type-checked its navigation again — the #4171 fix only covered half the annotation (#3786)
6+
7+
`NavigationItemSchema` is recursive, so it cannot infer its own type and carries a
8+
hand-written annotation instead. #4171 fixed that annotation's **Output** half: it
9+
had been `z.ZodType<any>`, which made the exported `NavigationItem` `any` for every
10+
consumer — a type that constrains nothing, which reads exactly like a type that
11+
works.
12+
13+
`z.ZodType` takes two parameters, `<Output, Input>`, and **`Input` defaults to
14+
`unknown`**. Naming only the first left the input half at that default, so
15+
`z.input<typeof AppSchema>` resolved `navigation` to `unknown` — and `unknown`
16+
accepts everything. `defineApp(config: z.input<typeof AppSchema>)` is the documented
17+
authoring entry point, and it took
18+
19+
```ts
20+
defineApp({
21+
name: 'my_app',
22+
label: 'My App',
23+
navigation: [{ totally: 'made up' }, 42, 'nonsense'], // compiled clean
24+
});
25+
```
26+
27+
with no complaint. Every authoring path through the app schema — `AppInput`,
28+
`NavigationAreaSchema`, `NavigationContributionSchema` — was unchecked the same way.
29+
Parsing was never affected: the schema rejected all of the above at runtime. It was
30+
only the compile-time contract that lied, which is why nothing in the test suite
31+
noticed. #4171's fix was verified through `z.infer`; nobody re-measured `z.input`,
32+
and half a fix looks identical to a whole one.
33+
34+
**The fix.** A new exported `NavigationItemInput` describes the authoring side, and
35+
the annotation now names both parameters. The two unions genuinely differ and one
36+
cannot serve both: `GroupNavItemSchema.expanded` and `UrlNavItemSchema.target` carry
37+
`.default()`, so those keys are **required** in the parsed output and **omissible**
38+
when authoring. Reusing `NavigationItem` as the input type would force authors to
39+
write values the schema exists to supply.
40+
41+
**What this changes for you.** Nothing at runtime, and nothing for code that reads a
42+
parsed app. Code that *authors* navigation through `defineApp`, `AppInput`, or
43+
`z.input` of any schema embedding `NavigationItemSchema` is now type-checked where it
44+
previously was not, so genuinely malformed navigation that used to compile will now
45+
surface as a compile error — the errors are pre-existing bugs becoming visible, not
46+
new restrictions. Two notes on what the checked type says:
47+
48+
- Authoring types (`AppInput`, `NavigationItemInput`) let you omit `expanded` and
49+
`target`; the parsed types (`App`, `NavigationItem`) still guarantee both are
50+
present.
51+
- If you annotate a hand-written literal with the parsed type (`const APP: App = {…}`)
52+
rather than the authoring type, you must spell out every defaulted key. That was
53+
already true and is unchanged — this release just makes the authoring alternative
54+
actually check its contents.
55+
56+
**The mechanism, not a comment.** `src/ui/app.nav-type-assertions.ts` is a non-test
57+
`src` module (the package's `tsc --noEmit` CI gate excludes test files) holding
58+
compile-level probes for both unions and, critically, for the **wiring** between them
59+
and the schema. A correct `NavigationItemInput` that no schema references would leave
60+
every authoring path back at `unknown` — precisely the state being fixed — so the
61+
load-bearing assertions go through `AppInput` and fail the moment the annotation loses
62+
its second parameter. Each probe was mutation-tested: dropping the `Input` parameter,
63+
removing the `expanded` default, and leaking `children` onto a flat nav branch each
64+
turn the corresponding assertion red.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
refactor(spec)!: retire the plugin lifecycle-hook family the kernel never implemented (#4212)
6+
7+
`PluginLifecycleSchema` declared `onInstall` / `onEnable` / `onDisable` /
8+
`onUninstall` / `onUpgrade`, each with confident TSDoc ("Called when plugin is
9+
installed", …). **The kernel calls none of them, and never has.** Its plugin
10+
contract is `init` / `start` / `destroy` (`packages/core/src/types.ts`):
11+
`kernel.use()` validates and stores, `bootstrap()` runs `init` then `start`,
12+
shutdown runs `destroy` in reverse order. There is no install phase and no
13+
first-time-only path. Repo-wide, the schema's only importer was its own test —
14+
and the docs built on it sent authors at a dead seam (`registerMetadataTypeSchema`
15+
told plugins to register custom metadata types "from their `onInstall` hook";
16+
a plugin that obeyed registered nothing, with no error saying so).
17+
18+
Removed, with zero consumers verified across `objectstack`, `objectui` and
19+
`cloud`:
20+
21+
- `PluginLifecycleSchema` + `PluginLifecycleHooks`, and the five hook members
22+
on `PluginSchema` (now a plain descriptor object).
23+
- `UpgradeContextSchema` + `UpgradeContext` — existed solely to serve
24+
`onUpgrade`.
25+
- The `@objectstack/spec/system` `./types` module: the `ObjectStackPlugin`
26+
interface (the same three hooks as a TS contract, referenced by no file in
27+
any repo) and its companions `PluginContext` (interface duplicate),
28+
`PluginLogger`, `ObjectQLClient`, `IKernel`, `ObjectOSKernel` — seeded by
29+
the aspirational spec in issue #2 and never consumed since.
30+
31+
FROM → TO:
32+
33+
- `onInstall` → there is no install-time code hook; installation is a
34+
package-registry state transition (`registry.installPackage()`
35+
`sys_packages`). Install-shaped setup (registering services, schemas,
36+
metadata types) belongs in the plugin's **`init(ctx)`**.
37+
- `onEnable` (kernel plugin) → `init(ctx)` / `start(ctx)`. The *app-bundle*
38+
`onEnable` module export is a different, real contract and is unchanged —
39+
`AppPlugin` invokes it at boot (`STACK_RUNTIME_MEMBERS`).
40+
- `onDisable` / `onUninstall``destroy()` for runtime teardown; package
41+
uninstall is a registry transition.
42+
- `onUpgrade` / `UpgradeContext` → package upgrades apply metadata migrations
43+
(ADR-0087); no plugin code runs.
44+
45+
One-line fix: replace the hook object with a class implementing
46+
`Plugin``name` + `async init(ctx)` (+ optional `start`/`destroy`).
47+
48+
Plain deletion rather than `retiredKey()` tombstones because nothing parses
49+
plugin objects through these schemas (`stack.zod` carries `plugins` as
50+
`z.array(z.unknown())`) — a prescription nobody can receive is noise (the
51+
`plugin-runtime.zod.ts` precedent). Per that precedent, the key-vanish guard's
52+
baseline entries (`kernel/UpgradeContext:*` in `authorable-surface.json`) are
53+
dropped deliberately in this PR. A pleasant side effect: with the function
54+
members gone, `PluginSchema` became JSON-representable and now publishes a
55+
`kernel/Plugin` JSON schema for the first time. No ADR-0087 conversion: these are function
56+
members on runtime objects, not authorable stack metadata; there is no source
57+
file for `os migrate meta` to rewrite. The kernel docs
58+
(`protocol/kernel/{index,lifecycle,plugin-spec}.mdx`) that documented the
59+
fictional lifecycle — including a manifest `lifecycle:` file-map ManifestSchema
60+
never declared — now document the real contract.

0 commit comments

Comments
 (0)