Skip to content

Commit bedf91b

Browse files
authored
Merge branch 'main' into claude/issue-10495-objectui-pin-reachability
2 parents f871db8 + a7ea328 commit bedf91b

33 files changed

Lines changed: 3010 additions & 242 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
Fix `os serve` failing to boot with `OS_CLUSTER_DRIVER=redis` when the app
6+
declares `@objectstack/service-cluster` (#10645). The cluster gate and its
7+
driver were reached through a bare dynamic `import()`, which Node ESM resolves
8+
against the CLI's own realpath — inside the framework workspace — so packages
9+
installed under the host app were invisible to it and boot died with
10+
`Cannot find package '@objectstack/service-cluster'`. Both loads now go through
11+
the host-anchored importer `serve` already uses for its other optional and
12+
enterprise packages, so any package the app declares resolves the way the app
13+
declares it. The host importer is now defined at the top of the boot sequence
14+
rather than partway down, which is what made these two loads fall back to bare
15+
resolution in the first place. No change to what `serve` accepts or refuses:
16+
an undeclared package is still refused by the same declaration gate.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
**Fix:** `os datasource list-tables`, `os datasource introspect` and
6+
`os datasource validate` now read the response envelope the server actually
7+
emits, so all three work against a live server for the first time (#10675).
8+
9+
The three commands read the pre-#3843 **flat** shape — `body.tables`,
10+
`body.draft`, `body.results`, and `body.error` as a string — while every REST
11+
body the platform sends is the declared envelope written by `sendOk` /
12+
`sendError`: `{ success: true, data: { … } }` or
13+
`{ success: false, error: { code, message } }`. Nothing failed loudly, because
14+
each payload simply read `undefined` and every command reported that as an
15+
ordinary empty result:
16+
17+
- `list-tables` printed `No remote tables found.` while the server was
18+
returning two tables.
19+
- `introspect` printed `Failed to generate draft` for drafts the server had
20+
generated.
21+
- `validate` printed `No federated objects to validate.` and exited **0**
22+
against drift the server had flagged `missing_column … severity:error` — a
23+
schema gate green-lighting a CI-breaking condition it had never read.
24+
- An unknown datasource crashed with `TypeError: first argument must be a
25+
string or instance of Error`, because the error **object** was handed to
26+
oclif's `this.error()` instead of `error.message`.
27+
28+
`validate`'s exit code is the behaviour change to note: a datasource whose
29+
federated objects have drifted now exits **1** where it previously exited 0. If
30+
you have a pipeline that treats this command as advisory, it starts failing on
31+
drift that was always there.
32+
33+
A body that is **not** the declared envelope is now a loud failure rather than
34+
an empty payload. That distinction is the point: "nothing found" is reachable
35+
only from a server that really said so, never from a response the CLI could not
36+
read. The legacy flat shape is deliberately *not* also accepted — a
37+
consumer-side fallback would re-create the divergence as a second de-facto
38+
contract.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os doctor` no longer prints `✓ Test coverage` / `✓ Deprecations` about a tree it
6+
never examined, and no longer warns `@objectstack/spec Not built` about a
7+
workspace that is not part of the tree (#10679).
8+
9+
`findMissingTests()` and `findDeprecatedUsages()` both walk
10+
`<cwd>/packages/spec/src` — a path that exists in this monorepo and in no
11+
application built with the framework. Both answered "that directory is not here"
12+
with the same value they return for "I walked it and found nothing wrong" (an
13+
empty array), so in a stock `create-objectstack -t blank` scaffold every run
14+
printed, verbatim:
15+
16+
```
17+
✓ Test coverage All *.zod.ts files have matching tests
18+
✓ Deprecations No @deprecated tags found
19+
```
20+
21+
about files doctor never opened. The command exits 0 either way, so "no problems
22+
found" and "I never looked" were byte-identical to every downstream reader.
23+
24+
Doctor already refuses to do this one screen down: the ADR-0120 D5e advisory's
25+
`✓ Unique scope` is withheld unless `ledgerReadingIsComplete()` says the ledger
26+
half was read in full. These two checks escaped that discipline; this restores
27+
it, in the same shape #5413 used for the ledger — whether the tree was examined
28+
is now a fact in the return type rather than an absence, so the print site
29+
cannot reach the `✓` from the unexamined arm. Where the tree is absent doctor
30+
prints an informational, named-reason skip instead:
31+
32+
```
33+
ℹ Test coverage Skipped — no packages/spec/src in this directory (monorepo-only check)
34+
ℹ Deprecations Skipped — no packages/spec/src in this directory (monorepo-only check)
35+
```
36+
37+
`--verbose` adds the resolved directory it looked for. The skip is deliberately
38+
not a warning: nothing is wrong in an application that has no
39+
`packages/spec/src`, and withholding a false `✓` must not manufacture a false
40+
`⚠`.
41+
42+
The adjacent `⚠ @objectstack/spec Not built` probe read `<cwd>/packages/spec/dist`
43+
with no check that the workspace it names exists, so in an application it warned
44+
about an absent package and prescribed `pnpm --filter @objectstack/spec build`, a
45+
command that cannot succeed there. It is now gated on `packages/spec/package.json`
46+
being present. Inside the monorepo the row is unchanged; outside it there is no
47+
row, and an application's spec dependency stays covered by the `Dependencies`
48+
check and by the spec-version-gap advisory.
49+
50+
Exit codes are untouched — 1 exactly when an error row exists, warnings never
51+
flip it. One visible consequence: a stock scaffold with no other findings now
52+
ends on `✅ Environment is healthy and ready for development!` instead of
53+
`⚠️ Environment is functional but has some warnings`, because the warning it
54+
used to carry was about a workspace that was never there.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
Apply a dataset's definition-level `filter` on the ObjectQL analytics path
6+
(#10413, phase 1). `/api/v1/analytics/query` served by a driver that reports
7+
`objectqlAggregate` but not `nativeSql` (MongoDB, the memory driver) reached
8+
`engine.aggregate` with no `filter` key at all: the dataset's own scope — a
9+
`filter: { is_deleted: false }` on the dataset definition — was dropped, so
10+
every measure aggregated the whole table while the dashboard door, on the same
11+
cube and the same measure names, answered the scoped numbers. The scope is now
12+
ANDed into the strategy's whole-call filter (never merged key-by-key, so a
13+
caller's own `where` and the time windows cannot be overwritten by it), and the
14+
representative SQL echo renders it too.
15+
16+
Per-MEASURE `filter`s on this path are still not applied: an
17+
`engine.aggregate` aggregation is `{ field, method, alias }` and cannot carry a
18+
predicate of its own. Widening that contract is #10576; lowering the measure
19+
filters into it is phase 2 of #10413. The native-SQL path already applies both
20+
(#10298).

‎apps/docs/next.config.mjs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@ const withMDX = createMDX();
66
/** @type {import('next').NextConfig} */
77
const config = {
88
reactStrictMode: true,
9+
// `next dev` otherwise writes `apps/docs/AGENTS.md` and `apps/docs/CLAUDE.md`
10+
// into the working tree whenever it detects an AI coding agent (Next 16
11+
// `ensureAgentRulesForDev`). Neither path is tracked or ignored, so both land
12+
// as `??` and any `git add -A` after booting the docs server sweeps them into
13+
// an unrelated PR. Those two filenames are load-bearing in this repo: the
14+
// root `AGENTS.md` is the binding playbook and agents are told to read the
15+
// `AGENTS.md` governing the files they touch, so a Next-authored stub
16+
// pointing at `node_modules/next/dist/docs/` can be read as this repo's
17+
// discipline -- and the generated text argues for committing itself, which
18+
// nudges an agent the wrong way. The generated `CLAUDE.md` is the single
19+
// line `@AGENTS.md`, an import directive that pulls the stub in.
20+
//
21+
// Disabled at the source rather than gitignored on purpose: an ignore rule
22+
// for `apps/docs/AGENTS.md` would silently swallow a future repo-authored
23+
// `AGENTS.md` for this app, failing invisibly at exactly the moment someone
24+
// was trying to add real guidance there. A visible untracked file is a better
25+
// failure than an invisibly ignored one for this filename specifically.
26+
//
27+
// This only stops future generation: a checkout that already ran `next dev`
28+
// keeps its copies until they are removed once by hand
29+
// (`rm -f apps/docs/AGENTS.md apps/docs/CLAUDE.md`).
30+
agentRules: false,
931
// No `output: 'standalone'` here on purpose. The standalone packer
1032
// (`writeStandaloneDirectory` -> `copyTracedFiles`) opens
1133
// `.next/next-server.js.nft.json`, and that open is the ONLY thing in a

‎examples/app-showcase/test/approval-resume-relation-expand.test.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,20 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
6868
const openKernels: Array<{ shutdown?: () => Promise<void> }> = [];
6969
const openDrivers: Array<{ disconnect?: () => Promise<void> }> = [];
7070
afterEach(async () => {
71-
while (openDrivers.length) {
72-
try { await openDrivers.pop()?.disconnect?.(); } catch { /* noop */ }
73-
}
71+
// Kernels first, drivers second: the kernel's own teardown still wants a
72+
// live driver to drain against -- that is the rule, regardless of what it
73+
// costs on any given day.
74+
//
75+
// Measured here (#10373): this file's own DATABASE_ERROR lines are all
76+
// "no such table" probes against sys_* tables bootShowcaseApprovals()
77+
// never provisions, not post-disconnect reads -- swapping the order left
78+
// the count unchanged (36 suite-wide / 31 in this file, before and after).
7479
while (openKernels.length) {
7580
try { await openKernels.pop()?.shutdown?.(); } catch { /* noop */ }
7681
}
82+
while (openDrivers.length) {
83+
try { await openDrivers.pop()?.disconnect?.(); } catch { /* noop */ }
84+
}
7785
});
7886

7987
interface Booted {

‎examples/embed-objectql/src/index.ts‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
// shape you would ship in a `*.object.ts` to a full ObjectStack backend. One
1414
// object model, two hosts; only the installed capability set differs.
1515

16+
import { realpathSync } from 'node:fs';
17+
import { join, resolve } from 'node:path';
18+
import { fileURLToPath } from 'node:url';
1619
import { ObjectQL } from '@objectstack/objectql/core';
1720
import { InMemoryDriver } from '@objectstack/driver-memory';
1821
import { ObjectSchema, Field, type ServiceObject } from '@objectstack/spec/data';
@@ -56,8 +59,41 @@ export async function runEmbeddedEngine(): Promise<AccountRow[]> {
5659
}) as Promise<AccountRow[]>;
5760
}
5861

59-
// Allow `node`/`tsx`-style direct execution to print the result.
60-
if (import.meta.url === `file://${process.argv[1]}`) {
62+
// ─── entry guard ───────────────────────────────────────────────────────
63+
// ⛔ NOT ``import.meta.url === `file://${process.argv[1]}` ``. Node symlink-resolves
64+
// `import.meta.url` but leaves `process.argv[1]` exactly as the caller typed it, and
65+
// the template also skips the percent-encoding `pathToFileURL` applies — so that
66+
// spelling goes INERT (exit 0, no output) through a symlink AND on any checkout path
67+
// containing a character that needs encoding (a `#` in a parent directory name is
68+
// enough, with no symlink involved). Compare RESOLVED PATHS, never URL strings.
69+
//
70+
// Same predicate as `packages/cli/src/utils/invocation.ts` (`isProcessEntry`) and
71+
// `scripts/invoked-as.mjs` (`invokedAs`). Spelled out rather than imported because
72+
// neither home is legally reachable from this file — the PR for #10269 carries the
73+
// boundary measurement. ⚠️ Two predicates answering this question differently IS the
74+
// defect this closes; change one, change all of them.
75+
function isProcessEntry(): boolean {
76+
const entryArg = process.argv[1];
77+
if (!entryArg) return false; // `node --eval` / the REPL
78+
const self = resolve(fileURLToPath(import.meta.url));
79+
const entry = resolve(entryArg);
80+
// `node <dir>` gives the ENTRY ARGUMENT, and only it, directory resolution.
81+
const candidates = [entry, join(entry, 'index.js'), join(entry, 'index.mjs'), join(entry, 'index.ts')];
82+
if (candidates.includes(self)) return true;
83+
const realSelf = realOrSelf(self);
84+
return candidates.some((candidate) => realOrSelf(candidate) === realSelf);
85+
}
86+
87+
/** `realpathSync`, degrading to the input for a path that cannot be read. */
88+
function realOrSelf(p: string): string {
89+
try {
90+
return realpathSync(p);
91+
} catch {
92+
return p;
93+
}
94+
}
95+
96+
if (isProcessEntry()) {
6197
runEmbeddedEngine()
6298
.then((rows) => {
6399
// eslint-disable-next-line no-console

0 commit comments

Comments
 (0)