Skip to content

Commit 94eeaa1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16464-shard-timings-refresh-workflow
2 parents fb51eee + 70e77ec commit 94eeaa1

150 files changed

Lines changed: 10084 additions & 678 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.

‎.changeset/config.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@objectstack/plugin-dev",
4242
"@objectstack/plugin-email",
4343
"@objectstack/plugin-hono-server",
44+
"@objectstack/organizations",
4445
"@objectstack/mcp",
4546
"@objectstack/plugin-pinyin-search",
4647
"@objectstack/plugin-reports",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`DatasetMeasureSchema.format` now documents what a DATE-valued measure can and cannot say, and the numeral-pattern examples no longer stand as the whole story.
6+
7+
The field was silent about date measures while advertising `e.g. "$0,0.00", "0.0%"` — the pattern grammar a date measure is precisely unable to read. An author with a `min` / `max` over a date field read that line, wrote `format: 'YYYY-MM-DD'`, parsed clean, and got the locale default.
8+
9+
The statement is carried by a `.describe()` where there was none, so it reaches the published surfaces an author actually reads: the generated JSON Schema (`json-schema/ui/Dataset.json`, `DatasetMeasure.json`) and the reference table in `content/docs/references/ui/dataset.mdx`, whose Description cell for `format` had been rendering the silence as a blank. The docblock above it carries the longer measured record.
10+
11+
What it now says, measured rather than assumed against the objectui pin this repo builds against: a numeral pattern applies to a numeric measure; a date-valued measure never reads a date PATTERN — a date-only value reads `format` as a display STYLE (`short`, `relative`), and a datetime value ignores `format` altogether.
12+
13+
Nothing accepts or rejects differently: `format` remains `z.string().optional()` and no measure is refused. Documentation over a published schema (objectui#7178 ruled A).
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
The authoring-time `defaultValue` gate now prescribes the key rename an author actually made, instead of a type error about a member they never wrote.
6+
7+
`checkLiteralDefaultValue` — the shared core of the field gate (`FieldSchema.defaultValue`) and the action-param gate (`ActionParamSchema.defaultValue`) — read a value-contract rejection positionally, `result.error.issues[0]`. zod reports per-member issues before the object-level `unrecognized_keys` one, so on a default whose keys were **renamed** the actionable message sorted last and was discarded. An `address` default authored as `{ street: 5, postal_code: '98101' }` answered `Invalid input: expected string, received number`, and a `location` default authored as the legacy `{ latitude, longitude }` pair answered `Invalid input: expected number, received undefined` — while `AddressValueSchema` and `LocationValueSchema` had each built the rename prescription and thrown it away. Which of the two the author got depended on whether some unrelated member happened to also be wrong: nobody chose that, and nobody could see it.
8+
9+
The gate now prefers the undeclared-key issue when the rejection carries one. `LiteralDefaultValueVerdict.detail` keeps its name, its type and its documented meaning — "the 'why' a refusal carries verbatim"; what changes is which of several already-reachable messages it carries.
10+
11+
⛔ No verdict moves. Exactly the same defaults are accepted and refused, on the same evidence — only the refusal text changes.
12+
13+
Scoped by measurement rather than inherited: the sixteen classes `valueSchemaFor(def, 'stored')` covers were swept again on this function, at both arities. Only `location` and `address` can emit `unrecognized_keys` at all, because only they are backed by a key-closed object schema — for the other fourteen the preference cannot change a single character. Both classes it does reach curate the alias map that makes the undeclared key the more actionable half of the rejection.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
The `DisasterRecoveryPlan` docblock example no longer teaches a cron dialect the platform's scheduler refuses.
6+
7+
`DisasterRecoveryPlanSchema`'s `@example` block spelled its six-hourly backup schedule `'0 0/6 * * *'`. A numeric prefix before the step (`0/6`) is Quartz-style stepping. The only cron parser this platform runs is `croner` — reached through `CronJobAdapter`, which hands every scheduled expression to `new Cron(...)` — and it refuses that spelling. Measured against the `croner` 10.0.1 copy installed for `@objectstack/service-job`:
8+
9+
```
10+
new Cron('0 0/6 * * *')
11+
-> TypeError: CronPattern: Syntax error, stepping with numeric prefix ('0/6')
12+
is not allowed. Use wildcard (asterisk-slash-step) or range (min-max/step) instead.
13+
```
14+
15+
The example now reads `'0 0,6,12,18 * * *'`, which the same parser accepts and which fires at 00:00, 06:00, 12:00 and 18:00 — the instants the old spelling was written to mean. The sibling example `'0 2 * * *'` on the same schema is accepted unchanged; it was the positive control for the measurement, so the refusal above is a reading rather than a broken probe.
16+
17+
The wildcard-step spelling croner's own error message suggests, and which this schema's tests use, is **not writable in this position**: inside a `/** … */` block comment the step separator closes the comment, and the file stops parsing (measured — esbuild refuses it at the example's own line). The enumerated form is the equivalent that survives a docblock, and both forms were measured to produce identical firing instants.
18+
19+
Nothing fires differently, because nothing fires at all: `BackupConfig.schedule` is declared-but-unwired and reaches no scheduler, and `CronExpressionInputSchema` judges no cron syntax at parse time by design (`shared/expression.zod.ts`) — so the bad example sat in a position that is deliberately undefended. The accept set of every schema is unchanged by this edit, and no export moves. What changes is what an author copying the example gets: the docblock publishes verbatim into the shipped `dist/system/index.d.ts`, so it is the text an editor shows on hover.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Every filter-operator member now carries a `.describe()`, so the published `data/filter` reference documents all of them instead of a subset.
6+
7+
The Description column of `content/docs/references/data/filter.mdx` is filled from `prop.description` — the JSON-Schema projection of a Zod `.describe()`. A JSDoc block above a member never reaches that column, so members documented by JSDoc alone rendered with an empty Description cell on a published reference page, including operators whose semantics an earlier correction campaign existed to fix.
8+
9+
Seven cells on that page were blank and are now filled: `EqualityOperator.$eq` / `$ne`, `StringOperator.$contains` / `$notContains` / `$startsWith` / `$endsWith`, and `QueryFilter.where`.
10+
11+
The descriptions are prose about behaviour that already ships — no operator semantics, accept-set, export or authorable key moved, and the JSDoc blocks are kept as-is:
12+
13+
- `$eq` / `$ne` state the default-operator role, the SQL and MongoDB lowerings, the `{ $field }` comparand position, and that `{ "$eq": null }` / `{ "$ne": null }` are the has-no-value / **has-a-value** predicates — the value question, never a key-presence one.
14+
- The four case-sensitive `$contains`-family members state their case contract, their SQL lowering, and the comparand contract they share: `%` and `_` are ordinary characters because the family escapes and anchors the comparand for the caller, which is what separates them from `$like` / `$ilike`.
15+
- `where` states the condition-tree shape plus the two semantics that were ruled rather than inherited — `$not` is NULL-safe, and empty `$and` / `$or` are the boolean identities.
16+
17+
Each description is now a module-level constant read by **both** copies of its operator — the documentation schema (`EqualityOperator`, `StringOperator`, `RangeOperator`, `SpecialOperator`) and the enforced `FieldOperatorsSchema` — extending the pairing `ORDERING_COMPARAND_DESCRIPTION` and `SET_MEMBER_DESCRIPTION` already gave the ordering and set slots. The two copies now share the text rather than a description of it, so an operator can no longer be documented in one and blank in the other.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
`filter-preset-comparand` gains a FIELD-TYPED arm (#16106, maintainer-ruled 1′): on a declared `date` / `datetime` field, a dashboard date-range preset name (`last_30_days`, `this_quarter`, …) is now refused in EVERY comparand position — bare (implicit equality), `$eq` / `$ne`, `$in` / `$nin`, and their view-rule (`equals` / `not_equals` / `in` / `not_in`) and triple (`=` / `!=` / `in` / `nin`) spellings — with the same located message and prescription the ordering positions already carry (`{ $gte: '{30_days_ago}' }` for `last_30_days`, and so on). The field type is read from the stack's own object graph: a dashboard widget or report through its `dataset` to that dataset's `object`, a view through `data.object`, a flow CRUD node through `config.objectName`, a page component through `dataSource` / `properties`, an object's own list views and `relatedListFilter`, a summary field's child object. A position the graph cannot bind, a registry-injected column, a `time` field, or a select / text column stays unjudged — equality against a picklist value that collides with a preset name is a working filter. The field-agnostic schema door in `@objectstack/spec` keeps its ordering-only boundary unchanged; this closes the authoring-time gap where `objectstack lint` and the runtime publish gate accepted a filter the engine then refused with `INVALID_FILTER` / 400 on first render.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
`SysMetadataRepository.listDrafts` emits the ISO-8601 string its own signature declares for `updatedAt`
6+
7+
`listDrafts` declares `updatedAt: string | null` on an inline TypeScript return type and reached the field through `row.updated_at ?? row.created_at ?? null`. `??` fires only on nullish, so the JS `Date` that Postgres and MySQL materialise for the builtin audit columns walked straight past it into a field the declaration calls a string. Driven through the published door, the pre-fix build answered `typeof "object"` and the visible text `Wed Mar 04 2026 05:06:07 GMT+0000 (Coordinated Universal Time)` where the same build's `dist/index.d.ts` promised `string | null`; it now answers `2026-03-04T05:06:07.089Z`.
8+
9+
`updated_at` / `created_at` are builtin audit columns: `SqlDriver#formatOutput` repairs them (and folds declared `datetime` columns) only inside its `if (this.isSqlite)` arm, and `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp` deliberately untouched because those are instants. Nothing reported the mismatch — the declaration is an inline return type rather than a Zod schema, so a schema search finds nothing, and `rows` is cast `as any[]` one line above the map, so tsc saw a `string` assignment that never happened.
10+
11+
Canonicalised at the producer through the same adapter boundary `rowToItem` already uses, with the terminal chosen per call site: `null` here, because the chain being replaced already ended in `?? null` and that is what "absent" already means to this projection's consumers. An Invalid `Date` — reachable on both live dialects — takes that same branch instead of raising. Already-canonical SQLite text passes through byte-identically, and `updatedBy` is unchanged: `updated_by` / `created_by` are `Field.lookup('sys_user')` string columns, which the dialect asymmetry never reaches.
12+
13+
No published declaration moves: `dist/index.d.ts` and `dist/index.d.cts` are byte-identical across the fix, which already declared `updatedAt: string | null` before it. A JavaScript consumer that read the raw value and called a `Date` method on it, or stringified it, sees the corrected shape.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/core": patch
4+
---
5+
6+
Documentation: the manifest surface no longer describes itself as an open object.
7+
8+
`ManifestSchema` became a `strictObject` when the manifest surface was closed against unknown keys, but five prose sites still described the earlier posture. They shipped, so an author (or an AI writing metadata) reading the declarations was told the manifest tolerates undeclared keys — while the runtime rejects them by name and offers the declared spelling for a near miss. Prose that contradicts a tightened contract teaches exactly the wrong reflex, so each site now states the current refusal rather than merely dropping the old claim:
9+
10+
- `AssembledPackageBodySchema`'s docblock no longer explains its lack of a `strictObject` spelling by calling `ManifestSchema` open. The posture is inherited: the schema is `ManifestSchema.extend(...)`, and `.extend()` carries the base's unknown-key handling, so an undeclared key on an assembled body is refused — measured, with the rename suggestion intact.
11+
- The artifact-registration seam kept the half of its reasoning that still holds (the schema applies defaults, so a parsed clone would not be byte-identical) and retired the half that does not ("Zod strips undeclared keys") — the key is now refused at that parse rather than dropped from the clone.
12+
- The `os compile` per-package rule pass explains why a body may be re-read as its own manifest: nothing parses that superset, and against `ManifestSchema` it would now be refused.
13+
14+
No schema, behaviour or export changed; `check:api-surface` and the generated reference pages are unmoved.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`navigationContributions[].group` now documents the mis-aimed case, not only the omitted one (#14925)
6+
7+
The `describe()` on that key said what happens when `group` is **omitted** and nothing about what happens when it is **present and names no group the target app declares** — which is the case that actually bites. A contributing package cannot see the target app's group ids at authoring time (the target app belongs to another package), so a wrong id is undetectable by reading the contributor's own source; and the platform **relocates** the items to the app's top level rather than refusing them, so the menu renders, a smoke test passes, and the information architecture has silently changed.
8+
9+
The description now names that third case: it is **not refused**, the items are appended at the app top level anyway, and a `nav_contribution_group_missing` diagnostic is emitted — by the runtime at `warn`, and by **both** `os build` and `os validate` at compile time, in each command's `--json` payload under the existing `warnings` key.
10+
11+
Prose only. `group` remains `SnakeCaseIdentifierSchema.optional()`, the accept set is unchanged and nothing is refused that was not refused before; the recorded authorable key surface (`authorable-surface.json`) and the schema manifest (`json-schema.manifest.json`) are byte-identical. What moves is the string an author reads: the generated reference rows in `content/docs/references/ui/app.mdx` and `content/docs/references/kernel/manifest.mdx`, and the `description` on the published JSON Schemas that embed `NavigationContribution` (its own schema, the bundled `objectstack.json`, and 22 `json-schema/api/*` and `json-schema/kernel/*` package envelopes).
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@objectstack/organizations': minor
3+
'@objectstack/plugin-security': patch
4+
'@objectstack/service-cluster': patch
5+
'@objectstack/spec': patch
6+
---
7+
8+
Ship the multi-organization runtime as open source: `@objectstack/organizations` is now an
9+
Apache-2.0 package in this repository (ADR-0132).
10+
11+
Single-database, row-level organization isolation was already open — the tenant Layer 0 wall,
12+
the three tenancy postures, the organization and invitation objects, better-auth's organization
13+
plugin, and the `requiresService: 'org-scoping'` Setup gates. What was closed was the one
14+
registrar of the `org-scoping` service, so an install that set `OS_TENANCY_POSTURE=isolated`
15+
could not enforce it: `serve` refused the boot, and the only way past was
16+
`OS_ALLOW_DEGRADED_TENANCY=1` — the wall configured but not enforced. This package is that
17+
missing registrar.
18+
19+
It provides:
20+
21+
- **`organization_id` auto-stamp on insert**, from the caller's active organization. A supplied
22+
— possibly forged — value is overwritten, never trusted.
23+
- **Per-organization seed replay** on `sys_organization` insert, from the app's own seed
24+
definitions. Never another organization's rows.
25+
- **Default-organization bootstrap** for the platform admin, idempotent.
26+
- **The walled-posture membership-policy gate**: a deployment that raises the wall must declare
27+
what a new user joins, or the boot is refused.
28+
29+
Only the commercial **entitlement** stays closed. The open class carries no licence check of any
30+
kind and offers no hook for one; an enterprise deployment resolves the same package name to a
31+
private, licence-gated subclass through its own `workspace:*` declaration, so which class is
32+
mounted is decided by the manifest that declares the name.
33+
34+
⚠️ Shipping the registrar is not yet the same as an open install raising the wall: `objectstack
35+
serve` still resolves the runtime from the served app's own declaration and is not yet wired to
36+
mount this package off `OS_TENANCY_POSTURE`. That, and the isolation matrix run against a real
37+
registrar rather than a posture stub, are tracked separately.

0 commit comments

Comments
 (0)