Skip to content

Commit f2af5d3

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/rest-list-field-validation-yyx9tk
# Conflicts: # packages/objectql/src/engine.ts
2 parents dfab708 + 55bbefc commit f2af5d3

123 files changed

Lines changed: 4508 additions & 2332 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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/objectql": minor
4+
"@objectstack/cli": minor
5+
---
6+
7+
feat(migrate): `os migrate value-shapes` — the per-deployment gate for reference and structured-JSON value shapes (#3438)
8+
9+
The second of ADR-0104 D1's two evidence gates. Media value shapes already
10+
enforce once a deployment has verified its file migration (#3681); the
11+
reference (`lookup` / `master_detail` / `user` / `tree`) and structured-JSON
12+
(`location` / `address` / `composite` / `repeater` / `record` / `vector`)
13+
classes now get a gate of their own.
14+
15+
```bash
16+
os migrate value-shapes # scan: reports, writes nothing
17+
os migrate value-shapes --apply # scan + record the deployment flag when clean
18+
```
19+
20+
The run walks every stored value of those classes against
21+
`valueSchemaFor(field, 'stored')` — the same predicate the write path enforces,
22+
imported rather than re-derived — and, at zero violations, records
23+
`sys_migration { id: 'adr-0104-value-shapes', verified_at, blocking: 0 }`.
24+
Strict enforcement of these classes reads **that row**, never the platform
25+
version, so upgrading changes nothing until a deployment produces its own
26+
evidence.
27+
28+
**There is no backfill, deliberately.** The file migration converts legacy
29+
values because the platform narrowed that storage form and owes the conversion.
30+
A malformed `location` is application data whose correct value only its author
31+
knows, so this run reports and prescribes — naming the object, field, type,
32+
count, offending record ids and the parse issue — and the operator fixes and
33+
re-runs. With nothing to convert, `--apply`'s only write is the flag row, which
34+
keeps the #3617 invariant trivially: a dry run changes nothing, and whether a
35+
run changed this deployment's posture never depends on what it found.
36+
37+
**A separate flag from the file migration**, because it attests a separate
38+
fact. That flag says file values were migrated and their ownership reconciled;
39+
it says nothing about whether a `lookup` id or a `location` payload is well
40+
formed. Gating these classes on it would be borrowing evidence for a fact it
41+
does not cover.
42+
43+
- New escape hatch **`OS_ALLOW_LAX_VALUE_SHAPES=1`** returns a verified
44+
deployment to warnings, with the same precedence as its media sibling: the
45+
opt-out beats `OS_DATA_VALUE_SHAPE_STRICT_ENABLED`, which beats the flag.
46+
Wrongly staying lenient costs a warning; wrongly enforcing stops a working
47+
app from writing.
48+
- `@objectstack/spec/system` exports `VALUE_SHAPES_MIGRATION_ID`.
49+
- `@objectstack/objectql` exports `scanValueShapes`, `valueShapeScanPassed`
50+
and `formatValueShapeScanReport`. The scanner is read-only and does **not**
51+
record the flag: readers of a migration flag use the spec contract, only
52+
writers depend on `@objectstack/platform-objects`, so the composition lives
53+
with the CLI command rather than inverting the engine's dependencies.
54+
- `validateRecord` gains `valueShapeStrict`, the sibling of
55+
`mediaValueShapeStrict`. Both default to `false`: a caller that cannot say
56+
stays lenient, so nothing starts rejecting merely because the evidence was
57+
unavailable.
58+
59+
**Nothing changes for an existing deployment until it runs the command.** A
60+
scan that is truncated, or that cannot read an object, fails the gate even with
61+
zero violations found — "none in the part we read" is not the claim the flag
62+
makes.
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.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/core": patch
4+
---
5+
6+
fix(spec)!: retire the never-built typed-event system; the lifecycle registry now lists the events that actually fire (#4212 follow-up)
7+
8+
The lifecycle-event surface promised a typed-event system that was never
9+
built, in three layers. `kernel/plugin-lifecycle-events.zod.ts` shipped ten
10+
payload schemas (`PluginRegisteredEvent`, `PluginErrorEvent`,
11+
`HookTriggeredEvent`, `KernelReadyEvent`, …) and a 21-name
12+
`PluginLifecycleEventType` enum — zero consumers for every export, and the
13+
enum was wrong in both directions: 17 names nothing fires, 10 real events
14+
missing. `contracts/plugin-lifecycle-events.ts` declared the same 17 dead
15+
names in `IPluginLifecycleEvents` next to 5 real ones, plus an
16+
`ITypedEventEmitter` interface nothing implements. All of it read as a
17+
promise; anyone who coded against it (hooking `plugin:started`, awaiting
18+
`plugin:error`) registered a handler that could never fire, with no error
19+
saying so — the same silent-drop shape as the #4212 lifecycle-hook family.
20+
21+
Removed, with zero consumers verified repo-wide:
22+
23+
- `kernel/plugin-lifecycle-events.zod.ts` and every export: `EventPhase`,
24+
`PluginEventBase`, `PluginRegisteredEvent`, `PluginLifecyclePhaseEvent`,
25+
`PluginErrorEvent`, `ServiceRegisteredEvent`, `ServiceUnregisteredEvent`,
26+
`HookRegisteredEvent`, `HookTriggeredEvent`, `KernelEventBase`,
27+
`KernelReadyEvent`, `KernelShutdownEvent`, `PluginLifecycleEventType`
28+
(schemas and inferred types).
29+
- `ITypedEventEmitter` from `contracts/plugin-lifecycle-events.ts`.
30+
- The 17 never-fired names from `IPluginLifecycleEvents`.
31+
32+
`IPluginLifecycleEvents` is now the registry of the **14 events with a real
33+
emitter**`kernel:{ready,bootstrapped,listening,shutdown}`, `app:seeded`,
34+
`metadata:reloaded` (payload `metadata` now optional, matching the documented
35+
contract), `external.schema.drift`, `ai:routes`, `auth:configure`, and the
36+
`{service}:ready` convention family (`mcp`, `automation`, `analytics`,
37+
`external-datasource`, `datasource-admin`) — each payload as observed at its
38+
fire site. A new `LifecycleEventName` union types
39+
`PluginContext.hook`/`trigger` in `@objectstack/core` as
40+
`LifecycleEventName | (string & {})`: known names autocomplete, custom
41+
cross-plugin names stay legal, existing callers compile unchanged. A pinning
42+
test asserts two-way equality between the interface keys and the fire-site
43+
inventory.
44+
45+
FROM → TO:
46+
47+
- `PluginLifecycleEventType``LifecycleEventName` (the union of names that
48+
fire). There is no runtime enum; the bus is open by design.
49+
- Event payload schemas (`KernelReadyEvent`, `PluginErrorEvent`, …) → the
50+
payload tuples on `IPluginLifecycleEvents`. No wire format existed or
51+
exists; payloads are in-process arguments.
52+
- `ITypedEventEmitter``PluginContext.hook`/`trigger` (the emitter that
53+
actually exists).
54+
- Handlers for the 17 dead names → delete them; they never ran. For plugin
55+
phase observation use the boot report (ADR-0084); for per-plugin errors the
56+
kernel throws/logs at the failing phase.
57+
58+
Plain deletion rather than `retiredKey()` tombstones, per the #4233
59+
precedent: these keys were never authorable — they described runtime event
60+
payload records no config author can write, so the silent-strip class the
61+
authorable-surface ratchet guards against is vacuous. Its baseline entries
62+
and the `json-schema.manifest.json` keys are dropped deliberately in this PR.
63+
No ADR-0087 conversion: no stack metadata names these types; there is nothing
64+
for `os migrate meta` to rewrite.

0 commit comments

Comments
 (0)