Skip to content

Commit f151fe1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15298-declared-permission-sets-docblock
2 parents 876f5a5 + 3a4373f commit f151fe1

59 files changed

Lines changed: 3995 additions & 423 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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os serve` / `os dev` / `os build` / `os migrate` resolve `packages[]` when a stack carries no flattened top level
6+
7+
The CLI holds four independent config-load boundaries, and every read of a
8+
package-owned collection behind them was an inline expression against the
9+
FLATTENED top level. A multi-package stack that carries each definition once
10+
under `packages[]` — the shape ADR-0130 D4's option B produces — reached those
11+
expressions with the key simply absent, and nothing threw:
12+
13+
- `os serve` / `os dev` auto-register the ObjectQL engine and the storage driver
14+
when the stack declares objects. Both gates read `config.objects`, so the app
15+
booted with **no query engine and no storage driver** and reported healthy.
16+
Nothing between the artifact and the gate could notice: the standalone stack
17+
omits the `objects` key entirely when the array is absent rather than setting
18+
`[]`, and the boot-config merge is a plain spread.
19+
- `os serve` auto-registers the i18n service plugin when the stack carries
20+
translations. `translations` is package-owned while `i18n` is an envelope key a
21+
translations-only stack never sets, so the REST i18n routes silently did not
22+
exist.
23+
- `os dev` diffs the artifact's object inventory across recompiles to name a
24+
newly added `*.object.ts`. It went permanently empty, so every recompile read
25+
as all-green.
26+
- `os build` runs the author-time rule table twice — once over the union, once
27+
per package. The per-package run already read `packages[]`; the union run,
28+
which is the only one of the two that can see a finding spanning packages,
29+
judged an empty stack and published green.
30+
31+
All of them now resolve through one seam, in the dependency-topological order
32+
`resolveArtifactPackageOrder` gives. Each answer starts from the expression it
33+
replaced, so every stack that boots or builds today takes the identical branch —
34+
including a stack declaring an empty `objects: []`, which stays a stack that gets
35+
an engine — and `packages[]` is consulted only where the old read returned
36+
nothing. A malformed `packages` list is refused with its ADR-0112 envelope on
37+
that leg instead of resolving to the silent empty.
38+
39+
The predicate `os serve` and `os migrate` each carried their own copy of — "does
40+
this config carry app metadata that needs an `AppPlugin` wrap" — is now one
41+
function. Measured, it does not lose under the new shape; it is folded in because
42+
it is the master gate for everything `AppPlugin` then reads.
43+
44+
No command emits anything different: the compiled artifact still carries both
45+
copies, and the folded stack is a rule INPUT that reaches no writer.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/driver-sql': minor
3+
---
4+
5+
feat(driver-sql): `update()` publishes its honest type — the contract's `Record<string, unknown> | null`, not `any` (#14438)
6+
7+
**BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, shipped as `minor` under the launch-window convention (the one PR #14434 used for the same door on `@objectstack/driver-memory`). `SqlDriver.update()` was written out with an explicit `Promise<any>` while it has always answered a missing id with `null` (`formatOutput(...) || null` on the un-rotated path, `null` once every rotation shard has been probed). `IDataDriver.update()` declares `Promise<Record<string, unknown> | null>`, and an explicit `any` satisfies that structurally — so the emitted `.d.ts` read `Promise<any>` and no caller holding a `SqlDriver`, or a `SqliteWasmDriver` (which inherits the door unchanged), was ever asked to narrow. It is now declared as the contract declares it, and the protected rotation-path producer `rotatedUpdateById()` carries the same type. A caller that read fields off `update()`'s result through the `any` now narrows the `null` arm first; a caller that leaned on `any` to read undeclared members now types them. No runtime behaviour changes.
8+
9+
`@objectstack/driver-sqlite-wasm` re-declares no `update` member of its own (measured on its emitted `.d.ts`), so it carries no entry: the narrowing reaches its consumers through this package's `.d.ts`. `@objectstack/driver-turso` overrides the door and carries its own entry.
10+
11+
<!-- adr-0087: not-required (type-surface-only packages/drivers/driver-sql/src/sql-driver.ts#update) A published driver method's declared return moves off an explicit `any` onto the contract's own shape. No metadata key is removed, renamed or re-shaped, `packages/spec` is untouched, and nothing exists for `objectstack migrate meta`, `spec-changes.json` or the upgrade guide to rewrite; the obligation is a TypeScript narrowing at the consumer's own call site, delivered by the compiler. -->
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/driver-turso': minor
3+
---
4+
5+
feat(driver-turso): the `update()` override publishes its honest type — `Record<string, unknown> | null`, not `any` (#14438)
6+
7+
**BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, shipped as `minor` under the launch-window convention. `TursoDriver` overrides `update()` rather than inheriting it, and the override was written out with its own explicit `Promise<any>` — so this package's emitted `.d.ts` re-declared the door as `any` on its own and would not have picked up the `@objectstack/driver-sql` narrowing. Both of its branches already answered the contract's type: the local branch forwards to `SqlDriver.update()` (narrowed alongside, #14438) and the remote branch passes `RemoteTransport.update()`'s `Record<string, unknown> | null` (#14428) through the generic `formatRemoteRow`. The override now declares what it answers. A caller that read fields off the result through the `any` now narrows the `null` arm first. No runtime behaviour changes.
8+
9+
<!-- adr-0087: not-required (type-surface-only packages/drivers/driver-turso/src/turso-driver.ts#update) A published driver method's declared return moves off an explicit `any` onto the contract's own shape; no metadata key moves, `packages/spec` is untouched, and the obligation is a TypeScript narrowing at the consumer's own call site, delivered by the compiler. -->
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/service-storage": patch
3+
---
4+
5+
fix(service-storage): put the test layer in front of tsc, and repair what it was hiding (#15050)
6+
7+
`packages/services/service-storage` had **no `typecheck` script at all** — its
8+
scripts were `build` and `test` — so no tsc program anywhere read this
9+
package's test layer, and its errors were carried instead as a 51-error DEBT
10+
entry in `scripts/check-type-check-coverage.mjs`. Gives it the #14062 /
11+
#14181 "checked test zone" shape: a sibling `tsconfig.test.json` (module
12+
semantics only — `esnext` / `bundler` / `lib: ES2022` — matching how vitest
13+
actually executes these files; strictness inherited and untouched) plus a
14+
`tsconfig.scripts.json` for `scripts/i18n-extract.config.ts` (the ninth
15+
instance of #11351, previously excluded from that ledger only because this
16+
package had no `typecheck` script to hang it on), both named by a new
17+
`typecheck` script.
18+
19+
Measured before repair: 51 errors under BUILD semantics (`tsc --noEmit -p
20+
tsconfig.json`, which already includes the tests — matching the DEBT entry's
21+
recorded number exactly), 10 under the split. Unlike `service-cluster`
22+
(#14181), this package's BUILD reading was *not* already clean, so both
23+
programs needed genuine repair, not just the test-only split: 23 `TS2835`
24+
(relative imports missing their `.js` extension, required under BUILD's
25+
NodeNext resolution) were fixed by *adding* the extension — which resolves
26+
correctly under both NodeNext and the split's bundler mode — and clearing
27+
that also cleared all 15 `TS7006` "implicitly any" as a downstream cascade
28+
from the same unresolved imports (the shape `@objectstack/core` reported at
29+
98 → 4). The remaining 3 `TS2550` (`Array.prototype.at` needing `lib`
30+
es2022) are rewritten to indexed access rather than widening the shared
31+
BUILD `tsconfig.json`. The 8 code-tier errors (`TS2339` × 4 — a test
32+
helper's object-spread dropped its `Record<string, unknown>` index
33+
signature, fixed with an explicit return-shape annotation; `TS2347` × 4 — a
34+
fake `ctx: any`'s `getService<T>(...)` calls converted to `getService(...)
35+
as T`, the pattern one call site in the same file had already adopted for
36+
exactly this reason) are genuine test-file fixes. Both readings now agree at
37+
0/0 — the same result `service-cluster` reported, reached by a longer road.
38+
39+
The package's DEBT entry (51 errors) is **deleted**, not lowered — the
40+
graduation this ratchet's invariant requires. No `test-typecheck-debt.json`
41+
is added: residue is 0, so none is owed (#5286, maintainer-only to open).
42+
`check:type-source-resolution` went red from onboarding the two new
43+
programs (the documented onboarding-limb case): a registry entry is added
44+
rather than `paths`, measured both ways — `paths` takes this package's test
45+
layer from 0 errors to 306, all in other packages' source.
46+
47+
No runtime code changes: `src/**` excluding tests is byte-identical, so no
48+
shipped behaviour moves. The `patch` level reflects the published
49+
`package.json` gaining `typecheck` / `check:test-typecheck` scripts and a
50+
`tsx` devDependency.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/platform-objects": patch
3+
---
4+
5+
fix(platform-objects): `sys_email.error` field help now covers pre-delivery rejections, not only transport failures
6+
7+
`sys_email.error` was declared as *"Transport error message when status=failed"*.
8+
Since `EmailService.recordRejectedMessage` landed, the same column also carries
9+
the reason a message was rejected by `normalizeMessage` **before** it reached a
10+
transport (an unsendable `from`, no recipient, no subject, no body) — those rows
11+
are written with `status: 'failed'` too, prefixed `rejected before delivery: `.
12+
13+
Nothing was misleading in the *data*: the row prefixes its own reason, so an
14+
operator reading a failed row is never sent chasing an SMTP host for a message
15+
that never reached one. What was stale was the field's declared `description`,
16+
which Studio surfaces as the field's help text — it named only the transport
17+
case, narrower than what the column has held since that change landed.
18+
19+
The description now reads: *"Why the message failed — a transport error, or the
20+
validation that rejected it before delivery."* It stays true under both row
21+
shapes and deliberately does not name the row's own `rejected before delivery:`
22+
prefix, so it will not go stale again if that prefix's wording changes.

packages/adapters/hono/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,9 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
159159
// use a matcher function — Hono's cors() middleware does exact-string matching only and
160160
// treats '*' in patterns as a literal character, so passing wildcard strings straight
161161
// through would silently drop the Access-Control-Allow-Origin header on every real
162-
// request (preflight can still succeed via apps/objectos's short-circuit, but the
163-
// subsequent POST/GET would be blocked by the browser).
162+
// request (preflight can still succeed via the short-circuit in `apps/objectos`,
163+
// which lives in the separate `objectstack-ai/cloud` repo and is NOT a path in
164+
// this one, but the subsequent POST/GET would be blocked by the browser).
164165
//
165166
// This mirrors `plugin-hono-server`'s CORS wiring and uses the shared pattern matcher
166167
// from `@objectstack/plugin-hono-server` so all Hono-based code paths stay in sync.

packages/cli/src/commands/compile.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from '@objectstack/spec';
1616
import { loadConfig } from '../utils/config.js';
1717
import { lowerCallables } from '../utils/lower-callables.js';
18+
import { authoringRuleUnionStack } from '../utils/stack-collections.js';
1819
import { buildAccessMatrix, diffAccessMatrix } from '@objectstack/lint';
1920
import { runAuthoringRules, splitBySeverity, authoringRulesFor } from '@objectstack/lint';
2021
import { resolveSduiManifest } from '../utils/sdui-manifest.js';
@@ -356,9 +357,24 @@ export default class Compile extends Command {
356357
// is declared in `lint/authoring-rules.ts`. Do not add a call site here.
357358
const registered = authoringRulesFor('build');
358359
if (!flags.json) printStep(`Running author-time rules (${registered.length})...`);
360+
// [ADR-0130 D4 / option B, #15006] The UNION run judges the flattened
361+
// top level. Under option B that top level is gone — `packages[]`
362+
// carries every definition once — so this run's input would be an
363+
// EMPTY stack and `os build` would publish green having judged
364+
// nothing. `authoringRuleUnionStack` folds each absent collection
365+
// back in from `packages[]`, in `resolveArtifactPackageOrder`'s
366+
// dependency order. It changes what the rules JUDGE and nothing this
367+
// command EMITS: the artifact is written from `lowering.lowered` /
368+
// `result.data`, which this does not touch, and a stack that still
369+
// carries its collections is returned by identity.
370+
//
371+
// The per-package run below needs no such fold — it already reads
372+
// `packages[]`. The union run is the only one of the two that can see
373+
// a finding spanning packages, which is exactly what an empty input
374+
// silently stops reporting.
359375
const findings = runAuthoringRules('build', {
360-
normalized: normalized as Record<string, unknown>,
361-
parsed: result.data as Record<string, unknown>,
376+
normalized: authoringRuleUnionStack(normalized as Record<string, unknown>),
377+
parsed: authoringRuleUnionStack(result.data as Record<string, unknown>),
362378
sduiManifest: resolveSduiManifest(),
363379
});
364380
const { errors: ruleErrors, advisories } = splitBySeverity(findings);

packages/cli/src/commands/dev.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
formatMtimeGap,
1818
} from '../utils/dev-restart.js';
1919
import { childEnvWithResolvedArtifact } from '../utils/internal-artifact-channel.js';
20+
import { artifactObjectNames } from '../utils/stack-collections.js';
2021
import { readEnvWithDeprecation, isMcpServerEnabled } from '@objectstack/types';
2122
// The ONE port contract, shared with `start` and with the `serve` child this
2223
// command spawns (#12673). ⛔ Nothing about ports is declared in this file —
@@ -650,16 +651,16 @@ export default class Dev extends Command {
650651
// newly added *.object.ts is called out explicitly (15.1 third-party
651652
// eval: "recompiled" alone read as all-green while the new object's
652653
// table/seed sync was invisible to the user).
654+
// [ADR-0130 D4 / option B, #15006] The envelope unwrap and the object
655+
// read are `artifactObjectNames` — one of this package's four reads of a
656+
// PACKAGE-OWNED collection, and the only one whose loss is non-fatal: with
657+
// the flattened top level gone this inventory went permanently EMPTY, so
658+
// `os dev` stopped naming a newly added *.object.ts and every recompile
659+
// read as all-green. The file read and the `null`-on-failure contract stay
660+
// here; the seam is what the acceptance probe can call.
653661
const readArtifactObjects = (): Set<string> | null => {
654662
try {
655-
const raw = JSON.parse(fs.readFileSync(opts.artifactPath, 'utf8'));
656-
const meta = raw?.metadata ?? raw?.data?.metadata ?? raw;
657-
const objects = Array.isArray(meta?.objects) ? meta.objects : [];
658-
return new Set(
659-
objects
660-
.map((o: any) => o?.name)
661-
.filter((n: any): n is string => typeof n === 'string'),
662-
);
663+
return new Set(artifactObjectNames(JSON.parse(fs.readFileSync(opts.artifactPath, 'utf8'))));
663664
} catch {
664665
return null;
665666
}

0 commit comments

Comments
 (0)