Skip to content

Commit c8607d3

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16510-seed-locale-axis
Second sync round for PR #16592. Sole conflict was the generated total row of packages/spec/liveness/state-counts.md, where main's #16784 re-grade of ActionSchema operation/patch (planned -> live) met this branch's added experimental seed.locale row. Resolved by taking either side of the anchor numbers; the table is regenerated from the merged ledger in the follow-up commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mr75Roij7XFWE6Cn2UyBq6
2 parents ba564fc + d4401f7 commit c8607d3

379 files changed

Lines changed: 33712 additions & 6142 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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Liveness ledger: `ActionSchema.operation` and `ActionSchema.patch` re-graded `planned``live`, and their author warning dropped.
6+
7+
Both keys were seeded `planned` with an `authorWarn` whose hint said, in as many words, that "nothing performs the write yet". That premise is gone: the runtime half of the declarative row-level field write is merged, so the ledger now says what the tree does.
8+
9+
- **`operation``live`.** It is the executor's own discriminator (`isDeclarativeUpdateAction`, a bare read of the declared key) and it is consulted *before* `type` at every reader: the REST `/actions` door, the MCP `run_action` door, the headless-invokability predicate, the type-error prescription, and the MCP listing summary.
10+
- **`patch``live`.** `declarativeUpdateWrite` reads it as the base of the write bag `{ ...patch, ...params }` — the static values sit *under* the ones the dialog collected — and `executeDeclarativeUpdateAction` hands that bag to a single data-plane update of the routed row, under the caller's own execution context.
11+
12+
Judged separately and both measured, not inferred: deleting the `operation` read fails 24 of the executor's 27 pins, deleting the `patch` read fails 16 of them, and the unmutated tree passes all 27.
13+
14+
**What moves for consumers.** `@objectstack/spec` ships `liveness/` in its published files, and `@objectstack/lint` resolves that directory off the installed package to build its author-warning map. Dropping `authorWarn` on these two rows therefore removes a real `os lint` finding: authoring `operation: 'update'` + `patch` no longer draws `liveness-planned-property`. Nothing else moves — no schema, no `.describe()`, no export, no accept-set change.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `AGGREGATE_FIELD_TYPE_COMPATIBILITY` accepts `boolean` / `toggle` for `sum` / `avg` / `min` / `max` — ruling #11152 (booleans aggregate as numbers on every backend) stands over batch #59's blanket default (#16685)
6+
7+
The aggregate × field-type table declared by `@objectstack/spec/data` gains the boolean class (`boolean`, `toggle`) on its four arithmetic / order rows. Two maintainer rulings collided on that class: decision batch #59 (2026-09-06) said "every other pair: refused" without ever naming booleans, while ruling #11152 (2026-08-28) pins that booleans aggregate as NUMBERS on every backend with no per-aggregate exception — `sum(flag)=3`, `avg(flag)=0.5`, `min(flag)=0`, `max(flag)=1`, enrolled on six backends by the spec's own `AGGREGATION_CASES`, and implemented by `driver-sql`'s Postgres cast (#11635). The director ruling of decision batch #80 (2026-09-08, #16685, maintainer verbatim 「其他同意」, option A) holds that the specific ruling stands over the blanket default: the four rows carry both boolean members, and nothing else moves — `AGGREGATION_CASES` and the driver cast are untouched.
8+
9+
| Aggregate | Accepted field types |
10+
|---|---|
11+
| `count`, `count_distinct` | every `FieldType` |
12+
| `sum` | `number`, `currency`, `rating`, `slider`, `progress`, `summary`, **`boolean`, `toggle`** |
13+
| `avg` | the numeric class (`percent` included), **`boolean`, `toggle`** |
14+
| `min`, `max` | the numeric class, `date`, `datetime`, `time`, **`boolean`, `toggle`** |
15+
| every other pair | refused |
16+
17+
Why it matters: `avg(flag)` is the win-rate / SLA-violation-rate shape (#11065) — the reason the conformance table exists. A compatibility table refusing it would refuse a pair every backend is REQUIRED to answer, and the two consumer legs that execute this table (the compile-time refusal in the dataset compiler, #16099, and the authoring-time lint rule) would have turned a supported measure into an authoring error.
18+
19+
**Additive, `minor`.** No export is added, removed or renamed — the exported declarations are unchanged (the table's declared type is the same; only its value gains members), the new `BOOLEAN_AGGREGATE_FIELD_TYPES` constant is module-private — absent from `dist/*.d.ts` and from the bundles' export lists — and `api-surface/` and `export-origins/` are untouched — and the accept set only WIDENS: every pair accepted before is still accepted. It rides the same release as the table's own changeset, so the version outcome is the same either way; `minor` is declared because widening a published accept set is the same class of change the table's introduction was (decision batch #35 puts additive widening at `minor`), and because the two consumer legs are the change's real audience: a measure over a boolean field compiles and lints clean.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): declare the aggregate × field-type compatibility matrix a dataset measure is judged against — `AGGREGATE_FIELD_TYPE_COMPATIBILITY` and `isAggregateCompatibleWithFieldType` (#16353, spec half of #16099)
6+
7+
A dataset measure pairs an `aggregate` with a `field`, and nothing between author and driver correlated the two: `avg` over a `Field.datetime` compiled to `AVG(col)` and reached the backend, where one SQL family averages the column's storage form and another rejects the call — one metadata document, two answers. Which pairs are accepted is a contract, so it is now declared once in `@objectstack/spec/data`:
8+
9+
| Aggregate | Accepted field types |
10+
|---|---|
11+
| `count`, `count_distinct` | every `FieldType` |
12+
| `sum` | `number`, `currency`, `rating`, `slider`, `progress`, `summary` — the numeric class EXCEPT `percent` (a rate does not add; `isIncoherentAggregate` already says so) |
13+
| `avg` | the numeric class, `percent` included |
14+
| `min`, `max` | the numeric class plus `date`, `datetime`, `time` — both return a value of the field's own type |
15+
| every other pair | refused |
16+
17+
The ruling (director, decision batch #59, 2026-09-06) named its buckets by category; the table resolves them against the real `FieldType` membership through the `field-value.zod` semantic classes: "numeric" is `NUMERIC_VALUE_TYPES` (`integer` is a driver-internal column alias, not a `FieldType` — the integer-valued authorable members are `rating` / `slider` / `progress`); "temporal" is the three temporal classes, `time` included because its stored form is a dialect question exactly like `date` / `datetime` (native TIME on Postgres and MySQL, canonical `HH:MM:SS[.fff]` TEXT on SQLite), the canonical form orders chronologically on every dialect, and `AnalyticsResult.fields[].type` already describes `min` / `max` over it as temporal (#15768). `formula` is refused for arithmetic aggregates whatever its declared `returnType`: it is virtual in SQL storage, no column exists to aggregate.
18+
19+
**Booleans** (`boolean`, `toggle`) are NOT in the "every other pair" default: maintainer ruling #11152 has every backend answer them as numbers (`sum(flag)=3`, `avg(flag)=0.5`, `min(flag)=0`, `max(flag)=1`, pinned in the spec's `AGGREGATION_CASES`; `driver-sql` casts the aggregand on Postgres to make it hold), and the director ruling of decision batch #80 (2026-09-08, #16685) holds that specific ruling over batch #59's blanket default — the four arithmetic / order rows carry both boolean members (see that changeset). One refused row overrides an existing opinion and is recorded as such, not presented as agreement: **the string classes** are refused for `min` / `max` here, while `service-analytics` (#15768) already types `min` / `max` over them as a supported `'string'` result; the refusal is defensible (string order is collation-dependent) but it overrides that opinion.
20+
21+
**The narrowing, stated plainly.** Every pair outside the table — `avg` × `datetime`, `sum` × `text`, `min` × `json`, `sum` × `percent`, and so on — is an authoring shape `DatasetMeasureSchema` accepts today and will be REFUSED once the two consumer legs land: the compile-time refusal in the dataset compiler (#16099) and the authoring-time lint rule (its devx sub-card). A measure whose pair is refused is fixed by changing the aggregate to one the field's type supports (`min` / `max` for a temporal field; `avg` for a `percent`; `count` for anything), never by widening the table.
22+
23+
**Not breaking in this release, `minor` on purpose.** This changeset ships a table and a predicate that nothing yet enforces: `DatasetMeasureSchema` accepts byte-for-byte what it accepted before, no export is removed or narrowed, and no runtime path reads the table yet. It is an additive widening of the published surface — two new exports in `dist/*.d.ts` — which the maintainer ruling of 2026-09-04 (decision batch #35) puts at `minor`. The refusal itself arrives with the consumer legs, whose changesets carry the breaking declaration, its migration prescription and the ADR-0087 disposition; this one names the narrowing so an upgrading author can read the contract before it is executed.
24+
25+
`isIncoherentAggregate` is unchanged and stays the semantic opinion beside this table. The two diverge on exactly one pair: `count_distinct` × `percent` is flagged there and accepted here (the ruling reads `count_distinct` as "any type"). That divergence is pinned in the table's test and reported on #16353 rather than resolved silently.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/runtime": minor
4+
---
5+
6+
feat(spec)!: `timeDimensions[].dateRange`'s string arm closes to the date-range preset vocabulary; any other string is refused with `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED` (#16041)
7+
8+
<!-- adr-0087: registered analytics-time-dimension-date-range-vocabulary-closed -->
9+
10+
**BREAKING** — an accept-set narrowing on a published analytics contract.
11+
`AnalyticsQuerySchema.timeDimensions[].dateRange` (and with it the
12+
`POST /analytics/query` / `/analytics/sql` bodies, `AnalyticsQueryRequestSchema`,
13+
and the `AnalyticsQuery` type every driver and `AnalyticsService.query` caller is
14+
typed against) used to accept ANY string. It now accepts exactly the thirteen
15+
dashboard date-range preset names, derived from `data/date-range-presets.ts`
16+
(`z.enum(DATE_RANGE_PRESETS)` — the vocabulary's single source of truth since
17+
#4614, so the two cannot drift), or the unchanged `[start, end]` array arm.
18+
Shipped as `minor` under the repo's launch-window convention for breaking
19+
changes; the hand-migration prescription is registered under protocol major 18.
20+
Maintainer ruling on #16041 (2026-09-06, decision batch #57, option A —
21+
contract first, 「同意」): 「本项目以协议为基准。所以开发应该对其协议,协议有问题应该立卡修改协议」.
22+
23+
## What was wrong
24+
25+
The arm was a bare `z.string()` whose only documented example — `"Last 7 days"`,
26+
in the schema's own comment — was a value no driver could parse. `driver-memory`
27+
recognised exactly `today` and a case-sensitive `last N <unit>` and fell every
28+
other string through to a `[range, range]` pseudo-window that (measured through
29+
mingo, 2026-09-05) matched **every `Date`-typed row**, 2099 included, because a
30+
`Date` compares above a `String` under BSON cross-type ordering. The SQL
31+
strategies read the same bare string as a single ISO day. A dashboard asking for
32+
one week silently got all of history on one backend and one day on the other,
33+
at HTTP 200 on both.
34+
35+
## What it does now
36+
37+
- The string arm is `AnalyticsDateRangePresetSchema = z.enum(DATE_RANGE_PRESETS)`
38+
(`today`, `yesterday`, `this_week`, `last_week`, `this_month`, `last_month`,
39+
`this_quarter`, `last_quarter`, `this_year`, `last_year`, `last_7_days`,
40+
`last_30_days`, `last_90_days`); the schema example is corrected to
41+
`'last_7_days'`.
42+
- Any other value raises ONE prescriptive issue at `timeDimensions.N.dateRange`
43+
(`analyticsDateRangeRefusalMessage`: the value, the vocabulary, the array
44+
spelling for an explicit window). `@objectstack/spec/data` exports the
45+
structural predicate `isAnalyticsDateRangeRefusalIssue` for doors.
46+
- `POST /analytics/query` and `/analytics/sql` answer the ADR-0112 envelope
47+
**`400 ANALYTICS_DATE_RANGE_UNRECOGNIZED`** — a new `ERROR_CODE_LEDGER` member
48+
registered under `@objectstack/runtime` — and the analytics service is never
49+
reached. A body wrong in more places than the `dateRange` stays the generic
50+
`400 VALIDATION_FAILED` + `details.fields[]`.
51+
52+
## FROM → TO
53+
54+
| you wrote | write instead |
55+
|:--|:--|
56+
| `dateRange: 'Last 7 days'` / `'last 7 days'` | `dateRange: 'last_7_days'` |
57+
| `dateRange: 'Last 30 days'` / `'last 30 days'` | `dateRange: 'last_30_days'` |
58+
| `dateRange: 'last 3 months'` | `dateRange: 'last_90_days'`, or an explicit `['{90_days_ago}', '{today}']` |
59+
| `dateRange: '2026-01-20'` (the SQL single-day dialect) | `dateRange: ['2026-01-20', '2026-01-20']` |
60+
| `dateRange: 'This week'` | `dateRange: 'this_week'` |
61+
| `dateRange: ['2026-01-01', '2026-01-31']` | unchanged |
62+
63+
Measured in this repository at the ruling: three authored `'Last 7 days'`, all
64+
in `packages/spec` tests (re-spelled here), and no published dashboard authors
65+
the string arm at all — the shipped console lowers presets to the array arm
66+
before querying. The drivers' own refusal of a non-conforming value that reaches
67+
them in-process (past the schema) is the sibling card #16322, blocked by this
68+
one; the fenced `service-analytics` fixture that authors the retired bare-ISO
69+
spelling is that card's to re-triage.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
`@objectstack/plugin-auth` pins the `better-auth` family to an exact `1.7.2`, so a fresh install of a published `@objectstack/*` release loads the auth plugin again — and with it creates the system tables and seeds the admin.
6+
7+
Published 17.1.0, 17.2.0 and 17.3.0 declared `"@better-auth/core": "^1.7.2"` and imported `createLocalAccountIssuer` / `createOAuthAccountIssuer` from `@better-auth/core/db`. `@better-auth/core@1.7.3` — a **patch** — deleted both names, and the `account.issuer` column behind them, because upstream rolled the issuer-scoped account identity back to opt-in (better-auth/better-auth#10909). A static ESM named import of a missing export is a link-time `SyntaxError`, so the plugin could not load at all. Every symptom followed from that one failure and every one of them was quiet: the scaffolded project's CLI printed the `SyntaxError` as a scrollable oclif warning and carried on, the server printed `✓ Server is ready` on the broken boot, `sys_user` / `sys_organization` / `sys_permission_set` / `sys_position` were never created, the seeded admin sign-in never answered, and the Console's sign-in form answered `Auth request failed with status 404`.
8+
9+
**This is a stopgap, deliberately, and it is labelled as one.** Upstream removed the export on purpose; adopting 1.7.3 means dropping `sys_account.issuer` — a required column with a unique `(issuer, accountId)` index — from the platform object, retiring the boot-time backfill that stamps it, and migrating every existing deployment. That is its own change with its own decision to make; this one restores a working install today.
10+
11+
All five members `plugin-auth` declares move together (`better-auth`, `@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/scim`, `@better-auth/sso`), because they are only correct as one line: `@better-auth/core@1.7.2` and `@better-auth/kysely-adapter@1.7.3` are mutually incompatible in both directions. `better-auth@1.7.2` declares its own siblings exactly, so pinning those five resolves all twelve family members to 1.7.2 — measured on a fresh `npm install` with no lockfile.
12+
13+
The workspace `overrides` move to the same exact target in step, so the version this repository tests is the version a consumer resolves. In-repo resolutions are unchanged: the lockfile already held 1.7.2 for all eleven overridden members.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
A scoped service that has no instance for your environment no longer answers as if you had forgotten to name one.
6+
7+
`HttpDispatcher`'s classified service lookup — the read behind the identity step, the `POST /keys` mint gate, the install-wide activation write and `POST /automation/:name/toggle` — took the scope it was handed, missed on it, and then re-resolved on the request's own kernel **without** that scope. A service registered `ServiceLifecycle.SCOPED` and resolved without a scope id is rejected by the plugin loader with `Scope ID required for scoped service '<name>'`, and that rejection is not the branded "never registered" the lookup absorbs — so it was re-raised, and each of those four doors answered `503 SERVICE_UNAVAILABLE` on a deployment where nothing was unwell. A caller that passed its environment correctly was told it had passed nothing.
8+
9+
Concretely: a `tenancy` factory that serves one environment and legitimately returns `undefined` for another made every one of those four doors fail for the second environment — no API key could be minted, no activation switch flipped, and the identity step itself raised the outage.
10+
11+
- **The scope now travels with every leg of the chain**, which is what the leg before it and the fallback tail already did. Nothing else about the resolution order changes: which registry answers is unchanged, only whether it is asked the question the caller actually asked.
12+
- **The lookup tells its three answers apart.** "Nothing was ever registered under this name" and "this name is registered and produced no instance in the scope you passed" are two different facts. They still license the same quiet `undefined` at the door — a factory that returns `undefined` for a scope has *answered*, so it is an absent fact rather than an unread one, and ADR-0093 D4/D5 reads a scope with no tenancy service the way it reads a deployment with none — but they are no longer the same answer inside the lookup.
13+
- **The loader's message is untouched, and so is the caller it is about.** A door that really resolves a scoped service without a scope still receives `Scope ID required for scoped service '<name>'` and still answers 503. That direction is pinned explicitly, because an implementation that answered every scoped miss with `undefined` would fix the misattribution by deleting a correct diagnostic.
14+
15+
No exported type changes: `DomainHandlerDeps.resolveServiceOrLoud` keeps its signature and keeps answering the service or `undefined`.

0 commit comments

Comments
 (0)