Skip to content

Commit 083c4cc

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/console-screen-flow-submit-jfpjy4
2 parents b9443ba + 7733604 commit 083c4cc

117 files changed

Lines changed: 4470 additions & 935 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(spec,kernel): #4093-series tail — the dev-plugin protocol header stops describing the retired stub design and points at #4149 (the enforce-or-remove evaluation for that declared-but-unconsumed schema family), and services-checklist.mdx gets a ten-fix accuracy pass (verified adversarially). Documentation only; releases nothing.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
---
3+
4+
Two additions to AGENTS.md's multi-agent discipline. Deliberately empty
5+
frontmatter: documentation, this releases nothing.
6+
7+
**#9 — refresh a long-lived worktree's build state after merging `main`.** Four
8+
distinct stale artefacts each fail *as if your change broke something*, naming
9+
other people's exports, other packages' files, or config you never touched:
10+
`packages/spec/dist` (makes `check:api-surface` report someone else's exports as
11+
breaking, and `check:i18n-coverage` reject a valid example config), `node_modules`
12+
(a package cannot resolve a dependency it plainly declares), `packages/runtime/
13+
.objectstack/` (fixture rows accumulating across runs), and `.cache/objectui-*`
14+
(dozens of lint errors in files you have never opened). None is CI-visible — CI
15+
checks out fresh — so the cost lands entirely on whoever is debugging. Also notes
16+
that `OS_SKIP_DTS=1` leaves no `.d.ts`, which makes `gen:api-surface` impossible
17+
rather than merely slow.
18+
19+
**#10 — a clean merge is not a working merge.** Git conflicts on overlapping
20+
lines; nothing warns when two changes are individually fine and jointly wrong.
21+
Both examples are real and recent: a test pinning a response body's exact shape
22+
landed while that shape was being changed elsewhere, and a domain file was deleted
23+
while another agent's guard still declared it. The first merged clean and failed
24+
CI; the second was caught only because the guard existed. Hence: pull `main` and
25+
re-run before opening a PR, and again before merging.
26+
27+
Written from one branch's lifetime — every row is a failure that cost a debugging
28+
round, so the list is what was actually hit rather than what might happen.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(analytics): a new spec aggregate can no longer silently return a row count
6+
7+
Track C item 4 of objectstack-ai/objectui#2945*"`AggregationFunction`: three
8+
places in lockstep"*. They agreed only by coincidence, and the failure mode when
9+
they stopped agreeing was silent wrong numbers.
10+
11+
The three:
12+
13+
1. `AggregationFunction` (`@objectstack/spec/data`) — eight members, what an
14+
author may declare as a dataset measure's `aggregate`.
15+
2. `UNSUPPORTED_AGGREGATES` (`dataset-compiler.ts`) — `array_agg`/`string_agg`,
16+
rejected at compile time with a clear error.
17+
3. The aggregate `switch` in `native-sql-strategy.ts` — six cases, then
18+
`default: return 'COUNT(*)'`.
19+
20+
8 − 2 = 6 = the six cases, today. Add a ninth member to the spec — `median`,
21+
`percentile`, anything — and it would:
22+
23+
- pass the compiler's gate, since it is not in `UNSUPPORTED_AGGREGATES`;
24+
- be **advertised as supported** by that gate's error message, which listed
25+
`count, sum, avg, min, max, count_distinct` as hand-written prose — a third
26+
copy of the vocabulary;
27+
- reach the strategy's `switch`, match no case, and fall to
28+
`default: COUNT(*)`.
29+
30+
The author asks for a median and gets a row count. No error, no log, wrong
31+
figures on a dashboard — the same silent-wrong-answer shape as the filter
32+
operators in #3948, in the analytics SQL builder.
33+
34+
**The fix is derivation plus a guard, with no behaviour change.** The `switch`
35+
becomes `AGGREGATE_SQL`, a table whose coverage is assertable; the error
36+
message's prose list becomes `SUPPORTED_AGGREGATES`, derived as
37+
`AggregationFunction.options` minus `UNSUPPORTED_AGGREGATES`; and
38+
`aggregation-lockstep.test.ts` asserts the arithmetic — the lowered set equals
39+
the admitted set, every spec member is either lowered or explicitly rejected,
40+
nothing is both, and the rejection list names only aggregates the spec has.
41+
42+
Verified by adding a hypothetical `median` to the spec, which now fails three
43+
assertions naming it, including *"these would fall through to the COUNT(*)
44+
fallback and return a row count"*. Before this change the same edit was green.
45+
46+
Nothing is narrowed and no SQL changes: the same six aggregates lower to the
47+
same six expressions, and the `COUNT(*)` fallback still catches everything else.
48+
49+
**Reported, not fixed:** that fallback is also reached by a measure whose `type`
50+
is `number`/`string`/`boolean` — a custom SQL *expression*, per
51+
`AggregationMetricType` — whose expression is then replaced by a row count.
52+
Datasets cannot produce one (`aggregateToMetricType` only ever returns an
53+
`AggregationFunction` member), so it is reachable only from a hand-authored
54+
Cube. Emitting `col` instead is a behavioural change in an analytics SQL path
55+
and deserves its own change with its own tests; the strategy's doc comment now
56+
records it.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
**A config-booted app no longer loses its `onEnable` — every `script` action's
6+
handler reaches the engine again instead of 404'ing at dispatch (#4095).**
7+
8+
`os serve <config>` calls `createStandaloneStack()`, which reads
9+
`dist/objectstack.json` and returns a ready-made `AppPlugin` for the app. That
10+
satisfied serve's "does the host already wrap itself with an AppPlugin?" guard,
11+
so the `new AppPlugin(config)` built from the LOADED MODULE — the only one
12+
carrying the module's `onEnable` — was skipped. A JSON artifact cannot hold a
13+
function, so the app booted with all of its metadata and none of its code.
14+
15+
On `examples/app-todo` that meant eight declared `script` actions, zero
16+
registered handlers, and every button answering
17+
`404 Action 'complete_task' on object 'todo_task' not found`. The example is
18+
correctly authored: it declares `target: 'completeTask'`, registers
19+
`todo_task:completeTask`, and exports `onEnable`. serve carried that hook intact
20+
all the way to the branch that discarded it.
21+
22+
Serve now grafts the module's executable members onto the app bundle already
23+
registered, rather than dropping them with the wrap:
24+
25+
- Only members `AppPlugin` actually executes travel — `onEnable` and the
26+
`functions` map that string-named hook/job handlers resolve against. (`onDisable`
27+
is deliberately excluded: it is declared in `packages/spec` but no kernel,
28+
runtime or service ever calls it, so grafting it would wire a hook nothing
29+
runs.)
30+
- The artifact stays the metadata source of truth. Neither side is a superset —
31+
the artifact carries compile-time enrichment the config never has (ADR-0046
32+
packaged docs, which serve already grafts the other way) — so this moves code
33+
only, and never metadata.
34+
- Targeting is by `manifest.id`, so a host composing several `AppPlugin`s can
35+
never have one app's handlers attached to another. With no id to match, it
36+
falls back to the single app bundle present and refuses when there are several.
37+
- A bundle's own value always wins, so a host that wrapped itself on purpose is
38+
untouched.
39+
- Code that finds no bundle to land on is now reported with a boot warning naming
40+
the consequence ("they 404 at dispatch") instead of vanishing. That silent drop
41+
is what hid this.
42+
43+
Verified end to end on `examples/app-todo`: `POST /api/v1/actions/todo_task/complete_task`
44+
went from `404 RESOURCE_NOT_FOUND` to `{"success":true}`, `export_csv` now returns
45+
real CSV, and the `[action-governance]` boot warning naming all eight actions is
46+
gone. 14 unit cases pin the graft and — as importantly — the cases where it must
47+
refuse; one end-to-end case boots a real stack through `bin/run-dev.js` and fails
48+
against the pre-fix command.
49+
50+
Note that `os serve <config>` still cannot boot at all when `dist/objectstack.json`
51+
is absent (#4085, `Service 'manifest' is async - use await`). That was verified to
52+
be a **separate** defect on the other side of the same fork, not this one: the
53+
failure reproduces unchanged with this fix applied.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
---
3+
4+
chore(ci): a nightly rerun-safety gate, job timeouts, and a compiled-tests-in-dist guard
5+
6+
Three CI changes, all of them lessons #4065 taught the hard way. No package
7+
changes — CI configuration only.
8+
9+
**1. Nightly rerun-safety gate (`rerun-safety-nightly.yml`).** Every job in this
10+
repo runs on a fresh clone, which makes CI structurally incapable of seeing a
11+
suite that pollutes its own working tree and therefore passes exactly once. CI
12+
always runs pass #1, so it is always green. #4065 sat in the repo through every
13+
CI run it ever had and surfaced only because somebody ran the full suite twice in
14+
one checkout while doing unrelated work — where it looked like *their* change had
15+
broken something. The new job runs the full suite twice in one tree with
16+
`--force` (turbo would otherwise replay the cache and report green without
17+
executing anything) and fails if the second pass disagrees with the first. It
18+
also prints any `.objectstack/` directories left behind between passes, so a
19+
failure names a file instead of reading as flakiness.
20+
21+
**2. `timeout-minutes` on all eight `ci.yml` jobs.** There were none, so every
22+
job inherited GitHub's 6-hour default. On PR #4100 the Test Core job hung with no
23+
output for 80 minutes and would have held a runner for six hours — and the whole
24+
time the PR read as "still running" rather than broken, which is the worst
25+
failure mode a gate can have. Ceilings are ~3-4× the healthy observed duration,
26+
so a genuinely slow run still passes.
27+
28+
**3. A build-output guard against compiled test files.** A package built with
29+
plain `tsc` that does not exclude tests emits `dist/**/*.test.js`. `files:
30+
["dist"]` then publishes them to npm — and, worse, a package with no vitest
31+
config *collects* those compiled copies alongside its sources, so every
32+
`src/**/*.test.ts` also runs as a stale `dist/**/*.test.js` frozen at the last
33+
build. `@objectstack/cli` shipped exactly that (81 test files / 849 tests where
34+
its sources hold 58 / 581) until #4065 excluded them. That silently defeats
35+
edits: a fix to a source test appears not to work because the run is still
36+
executing the pre-fix duplicate. Everything else here builds with tsup, which
37+
emits only declared entry points — so this gate exists to stop the *next*
38+
tsc-built package repeating it, not to re-check the one already fixed.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/runtime": patch
3+
"@objectstack/cli": patch
4+
---
5+
6+
fix(runtime,cli): `projectRoot` reaches the metadata repository; stop compiling tests into the CLI's dist (#4065)
7+
8+
Two defects behind the last of #4065's stray `.objectstack/` directories — the
9+
one under `packages/cli/`. Neither is cosmetic.
10+
11+
**1. `projectRoot` only got half the stack.** `createStandaloneStack`'s
12+
`projectRoot` is documented as scoping a boot's on-disk state to the project
13+
folder "so different examples / apps don't share a single database by accident",
14+
and it did redirect the default sqlite database. But it was never passed to
15+
`MetadataPlugin`, whose `FileSystemRepository` kept rooting at `process.cwd()`.
16+
So one "project root" meant two different directories: a boot pointed at project
17+
A wrote `A/.objectstack/data/` and `<cwd>/.objectstack/metadata/`. It now
18+
forwards `rootDir`, and `bootSchemaStack` accepts a `projectRoot` to pass down
19+
(defaulting to `process.cwd()`, which is right for every real `os migrate` — the
20+
CLI runs from the project directory). The two migrate integration suites, which
21+
build a fixture project in a tempdir, now scope their boots to it.
22+
23+
**2. The CLI compiled its own tests into `dist/` — and vitest ran them.**
24+
`tsconfig.build.json` included all of `src` with no exclude, so every
25+
`src/**/*.test.ts` was emitted as `dist/**/*.test.js`. Two consequences:
26+
27+
- `files: ["dist"]` **published** them.
28+
- This package has no vitest config, so `vitest run` collected the compiled
29+
copies alongside the sources: **81 test files and 849 tests where the sources
30+
hold 58 and 581**. Every `src/` test also ran as a stale `dist/` twin built
31+
from whatever the source said at the last build.
32+
33+
That is not just noise — it silently defeats edits. A fix to a source test
34+
appeared not to work, because the run was still executing the pre-fix compiled
35+
duplicate; that is exactly how the `.objectstack` residue survived a correct
36+
fix long enough to look like a different bug. It also means a source test could
37+
be edited to pass while its stale twin kept asserting the old behaviour, and
38+
neither would be obviously wrong. Test files are now excluded from the build.
39+
40+
No other package is affected: the rest build with `tsup`, which emits only
41+
declared entry points. Verified by scanning every `packages/*/dist` for
42+
`*.test.js` — the CLI was the only hit.
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/service-automation": patch
4+
"@objectstack/runtime": patch
5+
---
6+
7+
fix(spec,runtime,service-automation): `IAutomationService` declares the connector registry it already serves (#4127)
8+
9+
The fourth and last of the dispatcher call sites #4127 found calling a method its
10+
contract never declared. The first three shipped in #4143; this one was held back
11+
because the fix is a **type move**, not a type addition — `ConnectorDescriptor`
12+
was declared in `@objectstack/service-automation`'s engine, which is one
13+
*implementation* of `IAutomationService`. A contract cannot name a type that
14+
lives inside its own implementation, so `getConnectorDescriptors` could not be
15+
declared at all until the type had a home in the spec.
16+
17+
**`IAutomationService` += `getConnectorDescriptors?()`.** It is the sibling of
18+
`getActionDescriptors`, which the contract has declared since ADR-0018: the two
19+
fill the flow designer's `connector_action` node together — node vocabulary from
20+
one, the connector → action → input pickers from the other. Only one of them was
21+
written down. `GET /api/v1/automation/connectors` has served the other since
22+
ADR-0022 by probing for the method and then re-typing its own result as `any` to
23+
filter on `?type=`, which is a filter on a field the type system did not know
24+
existed — one typo from silently matching nothing and answering an empty
25+
registry, which is also what this route legitimately returns when the method is
26+
absent, so the failure had no distinguishable symptom.
27+
28+
Optional for the same reason `getActionDescriptors` is: a connector registry is a
29+
capability of the flow-engine implementation, not a property of every automation
30+
slot. A script-runner filling the slot has no connectors to describe, and the
31+
route answers an empty registry rather than a 404 — the `handlerReady` posture
32+
does not apply, since the slot is serveable and only this capability is absent.
33+
34+
**`ConnectorDescriptor` / `ConnectorActionDescriptor` / `ConnectorOrigin` /
35+
`ConnectorState` move to `@objectstack/spec/integration`**, beside the ADR-0097
36+
provider contract, for the reason that file already states about itself: they are
37+
pure types, so a connector plugin — or a designer client, or the dispatcher —
38+
speaks about registered connectors depending only on the spec, with no runtime
39+
coupling to the engine. `ConnectorOrigin` is ADR-0097 §4 vocabulary and
40+
`ConnectorState` is #3017 vocabulary; neither was ever engine-private in meaning,
41+
only in location.
42+
43+
Nothing is renamed and no shape changes. `@objectstack/service-automation`
44+
imports the four back and re-exports them from its index — the same names, from
45+
the same entry point — so every existing importer compiles unchanged.
46+
`ConnectorState` joins that re-export, which it should have been in all along: it
47+
is a required field of the descriptor the index has always exported.
48+
49+
**The test fixture had already drifted, which is the concrete cost.** The
50+
dispatcher's connector mock declared `{ name, label, type, actions }` and omitted
51+
`origin` and `state` — both **required** on `ConnectorDescriptor`, and both the
52+
fields a designer reads to tell a live declarative instance from a plugin one
53+
(ADR-0097 §4), or a dispatchable connector from a degraded one that is listed
54+
honestly rather than hidden (#3017). Nothing caught it, because an undeclared
55+
return type cannot be checked against. The fixture is typed now, so it cannot
56+
drift again, and a new test pins that `origin` / `state` / `degradedReason`
57+
survive the hop through the route rather than only `name` and `type`.
58+
59+
Verified: `@objectstack/spec` **7089 tests / 272 files** (2 new contract tests),
60+
`@objectstack/service-automation` **457 / 41**, `@objectstack/runtime`
61+
**218 http-dispatcher tests** (1 new), `tsc --noEmit`, `pnpm lint`, the liveness
62+
and empty-state gates, and the three generated-artifact gates — all clean.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/plugin-hono-server": minor
3+
---
4+
5+
fix(plugin-hono-server): the current-user endpoints answer from the kernel that OWNS the request (cloud#927)
6+
7+
`/api/v1/auth/me/permissions`, `/auth/me/localization` and `/me/apps` resolved
8+
their answer from the service locator captured at REGISTRATION time. On a
9+
single-environment host that is the only kernel, so it is right. On a
10+
**multi-tenant** host it is the routing shell — and identity is not there.
11+
cloud's `ArtifactKernelFactory` mounts `AuthPlugin` per environment, and its host
12+
kernel deliberately has none ("AuthPlugin is intentionally NOT injected on the
13+
host"), so `getService('auth')` threw, the session resolver fell to its catch, and
14+
every authenticated tenant caller got `{authenticated:false}`.
15+
16+
That is worse than an error: objectui's `MePermissionsProvider` reads
17+
`authenticated:false` as ANONYMOUS and keeps its permissive default
18+
(`return data.authenticated !== true`), because a guest surface has no resolvable
19+
permissions by design. So the console's FLS / `apiOperations` hints were
20+
systematically wrong — not a bypass (the server still enforces per request), but
21+
exactly the client/server divergence `foldWildcardSuperUser` and
22+
`clampManagedObjectWrites` exist to close, one layer up.
23+
24+
These endpoints now consult the host's ADR-0006 **`kernel-resolver`** seam per
25+
request — the same seam the runtime dispatcher has used since Phase 5, so
26+
multi-tenant routing has one strategy rather than two:
27+
28+
- **No `kernel-resolver` registered** → unchanged. Single-environment hosts,
29+
`os serve`, and the QA conformance host see no difference.
30+
- **A kernel** → that kernel's `auth` / `objectql` / `metadata` /
31+
`security.permissions` answer.
32+
- **`undefined`** → the registration-time locator, which is the seam's contract
33+
for an unscoped / control-plane request.
34+
- **A throw** → no answer at all: the thrown status when it carries one (cloud's
35+
`KernelWarmingError` is 503 + `Retry-After`), else 503
36+
`environment_unavailable`. Falling back to the default kernel would hand back a
37+
confidently-wrong `{authenticated:false}` that the client fails OPEN on.
38+
39+
The seam is read **lazily, per request**, never captured at registration — a host
40+
may register these routes before `kernel.bootstrap()` (to outrank an
41+
`/api/v1/auth/*` wildcard), which is before the plugin that registers the
42+
resolver has run its `init()`.
43+
44+
**FROM → TO for host adapters.** `CurrentUserEndpointsContext` gains an optional
45+
`getKernel(): unknown`, the `defaultKernel` argument the seam takes. A
46+
`PluginContext` already satisfies it, so hosts that mount `HonoServerPlugin` need
47+
no change. A host passing a hand-rolled locator to
48+
`registerCurrentUserEndpoints` should add it:
49+
50+
```diff
51+
registerCurrentUserEndpoints({
52+
rawApp: httpServer.getRawApp(),
53+
- ctx: { getService: (n) => kernel.getService(n) },
54+
+ ctx: { getService: (n) => kernel.getService(n), getKernel: () => kernel },
55+
});
56+
```
57+
58+
Without it a multi-tenant host cannot be asked which kernel owns the request and
59+
keeps the old provenance — a silent downgrade, so it is worth adding even where
60+
the host is single-environment today.

0 commit comments

Comments
 (0)