Skip to content

Commit 7646ba9

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/platform-migration-gating-764pf2
2 parents dafa3ca + c2603b9 commit 7646ba9

97 files changed

Lines changed: 7682 additions & 313 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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
---
3+
4+
ADR-0106 (Proposed): metadata-plane FLS — per-caller masking of object
5+
schemas (#3661 step ③). Docs-only; releases nothing.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(analytics): apply the EFFECTIVE date granularity to bucket labels and drill ranges (#3588 follow-up)
6+
7+
`selection.dateGranularity` (shipped in #3652) reached the `GROUP BY` but not the
8+
post-processing: the bucket-label formatter and the drill-range inverter both
9+
kept reading the DATASET dimension's default. A query was grouped one way and
10+
described another. Found by driving a real dashboard query in a browser against
11+
a dataset whose dimension declares `dateGranularity: 'month'`:
12+
13+
- selection `year` → the row came back labelled **`1970-01`** — a year bucket
14+
re-formatted with the dataset's month granularity, its `"2026"` key re-read as
15+
2026 *milliseconds* past the epoch;
16+
- selection `day` → day buckets were re-labelled as months, so ten distinct days
17+
collapsed into two duplicated keys;
18+
- selection `quarter` / `year` / `day` / `week``drillRanges` came back empty,
19+
silently removing drill-through from every bucketed chart.
20+
21+
Granularity precedence now lives in one exported function,
22+
`resolveDimensionGranularity`, called from all three sites that must agree — the
23+
query's `GROUP BY`, the label formatter, and the range inverter. The drift was
24+
possible only because each site resolved it independently.
25+
26+
Two consequences beyond the override case:
27+
28+
- A dataset dimension that declares **no** granularity but is bucketed by the
29+
widget now gets drill ranges too. Previously the range sidecar keyed off the
30+
dataset's own `dateGranularity`, so this case — the one #3588 is actually
31+
about — could never drill.
32+
- `formatDateBucket` no longer mistakes a bare year key for an epoch timestamp.
33+
A year bucket's canonical key IS `"2026"`, which is the only bucket key that
34+
collides with the pure-digit epoch heuristic (`"2026-Q2"`, `"2026-07"` and
35+
`"2026-07-15"` all fail it). Being idempotent over already-formatted keys is
36+
that function's stated contract; the year case just never held.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/service-analytics": minor
4+
"@objectstack/verify": minor
5+
---
6+
7+
feat(analytics): the executeAggregate bridge carries ExecutionContext — ADR-0021 D-C second belt
8+
9+
The analytics→engine bridge now forwards the request's `ExecutionContext` to
10+
`engine.aggregate`, so the engine's own middleware chain scopes analytics reads
11+
independently of the analytics layer's `getReadScope`.
12+
13+
**Why.** `BaseEngineOptions.context` has always been `.optional()`, so nothing
14+
forced the bridge to pass it — and it did not. An authenticated aggregate
15+
reached the engine with no principal, plugin-security's principal-less fall-open
16+
skipped its RLS injection, and the only thing left scoping the query was the
17+
strategy remembering to call `getReadScope`. #3597 was a strategy that did not,
18+
and both belts were off at once.
19+
20+
`getReadScope` stays: the two resolve scope through different paths (engine
21+
middleware vs `security.getReadFilter`), and a deployment without
22+
plugin-security has only the analytics layer. This is depth, not a replacement.
23+
24+
- `StrategyContext` gains `context?: ExecutionContext`, bound per call by
25+
`AnalyticsService` from `query()` / `generateSql()` / `queryDataset()`.
26+
- `StrategyContext.executeAggregate` and the `AnalyticsServicePlugin` /
27+
`AnalyticsService` `executeAggregate` config options gain `context?:
28+
ExecutionContext`. **Custom bridges should forward it** to their engine; the
29+
built-in auto-bridge does. Purely additive — an existing bridge that ignores
30+
it keeps working exactly as before.
31+
- `DimensionLabelDeps.fetchRecordLabels` and `resolveDimensionLabels` each gain
32+
an optional trailing `context`, beside the `scope` / `resolveScope` that
33+
#3639 added — the same two-belt split as the aggregate path.
34+
- `BootOptions.analytics` (`@objectstack/verify`) overrides the
35+
AnalyticsServicePlugin instance, so a gate can boot with the analytics belt
36+
off and assert the engine-side belt alone still scopes.
37+
38+
**Also fixed on the same seam:**
39+
40+
- `fetchRecordLabels` — the dimension display-label lookup — is row-granular
41+
(one row per record, real display names). #3639 gave it the analytics-layer
42+
belt (the referenced object's own read scope); it now also carries the
43+
context, so the engine scopes the same read independently.
44+
- `ObjectQLStrategy.generateSql` emitted no `WHERE` at all, so the
45+
`/analytics/sql` preview read as an unscoped table scan while the real
46+
aggregate was scoped. It now renders the caller's filters and the read scope.
47+
The preview never executed, so this was misleading output rather than a leak.

.changeset/auth-route-ledger.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
"@objectstack/client": patch
4+
---
5+
6+
test(plugin-auth): enumerate better-auth's route table — the `/auth/**` wildcard becomes 55 exact rows (#3656)
7+
8+
The widest hole the #3642 capstone measured. That guard reports how many SDK
9+
calls match only a `**` prefix family rather than a resolvable route, and the
10+
answer was 60 of ~196 — with 54 on `* /auth/**`, the largest and most
11+
security-relevant namespace in the client. `auth.me` builds
12+
`/api/v1/auth/get-session`; a prefix claim cannot tell you better-auth still
13+
calls it that, and better-auth is a third-party dependency on its own release
14+
cadence (this repo already chased its 1.7 column drift in #3624 / #3647).
15+
16+
`plugin-auth` mounts it with a single catch-all, so there are no per-route
17+
registration calls to capture the way tranche 3 captured
18+
`registerStorageRoutes`. The seam is `auth.api`: every better-auth endpoint
19+
carries `.path` and `.options.method`, so a live instance is the route table.
20+
21+
`auth-route-ledger.ts` reads it, in two halves checked differently on purpose:
22+
23+
- **55 reviewed rows** — every route the SDK calls, each naming its client
24+
method, checked strictly against the live table. This is the rename detector.
25+
- **129-path mounted-surface inventory** — checked for exact equality both
26+
ways, so a version bump that adds publicly-mounted auth endpoints becomes a
27+
reviewable CI diff. Machine-maintained rather than reviewed prose: demanding
28+
a rationale for all 129 would make every better-auth upgrade a hundred-row
29+
review and the ledger would rot into rubber-stamping.
30+
31+
Enumeration is config-dependent, so the inventory is pinned at the
32+
configuration enabling every plugin the SDK targets — the maximal surface —
33+
with the participating `OS_*` env vars cleared so a developer's shell cannot
34+
produce a spurious diff. Mutation-checked: renaming a ledgered route fails the
35+
suite naming it.
36+
37+
The capstone guard now includes this ledger in its union and prefers exact rows
38+
over wildcard families when matching — without that ordering fix every
39+
`/auth/*` URL would still have been absorbed by `* /auth/**` and the new ledger
40+
would have changed nothing. Wildcard-only matches fall **60 → 3**; the ratchet
41+
moves with them. What remains is `* /ai/**`, whose routes `service-ai` builds
42+
at plugin start.
43+
44+
No runtime change: a ledger, a guard, and the header/audit-doc notes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
CI-only: turbo cache for the Release job, console-dist cache keyed on the objectui pin, restore-only turbo caches on PRs (main seeds them), CodeQL off pull_request (main push + weekly schedule), spec coverage moved to a nightly workflow. Releases nothing.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/plugin-security": minor
4+
"@objectstack/rest": minor
5+
"@objectstack/client": minor
6+
---
7+
8+
feat(authz): expose the caller's delegable scope — the read half of the
9+
delegated-administration gate (ADR-0090 D12 / ADR-0105 D8)
10+
11+
`adminScope` decided writes but could not be READ: `assignablePermissionSets`
12+
lived only inside `delegated-admin-gate.ts`, so a UI offering "place this
13+
person in a unit, with these positions" (the D8 scoped-invitation form) had no
14+
way to narrow its pickers. It would list the whole tree and let the user
15+
discover the boundary by being refused — which turns an authorization gate into
16+
a validator and makes the boundary invisible until it bites.
17+
18+
`ISecurityService.describeDelegableScope(callerContext)` answers it, exposed as
19+
`GET /api/v1/security/my-delegable-scope` and `client.security.describeDelegableScope()`:
20+
21+
- `placeableBusinessUnitIds` — union of the subtrees where the caller may place
22+
people (scopes granting `manageAssignments`);
23+
- `assignablePositions` — positions whose every distributed permission set the
24+
caller may hand out (containment check included);
25+
- `scopes` — the held `adminScope`s with subtrees resolved, for attribution;
26+
- `isTenantAdmin` — unconstrained, with everything enumerated so a consumer
27+
renders ONE uniform picker instead of special-casing.
28+
29+
Computed by the same helpers the write gate enforces with, so an option this
30+
reports is one `assert()` accepts — a test asserts that agreement directly. It
31+
NARROWS; the gate still decides.
32+
33+
Strictly self-scoped: no target-user parameter, so it discloses nothing beyond
34+
the authority the caller already holds (unlike `explain`, which has one and
35+
gates it). Fail-closed — unresolvable scopes contribute nothing, a caller with
36+
no delegated authority gets empty lists, and a deployment without
37+
`@objectstack/plugin-security` gets 501.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
CI-only: the Dogfood Regression Gate no longer reports a superseded (cancelled) run as a failure — `cancelled` joins the pass branch, backed by a fail-fast experiment showing a real shard failure always aggregates as `failure`. Releases nothing.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/platform-objects": patch
3+
---
4+
5+
fix(i18n): clear the accumulated drift in the generated translation bundles
6+
7+
The committed bundles had fallen behind the spec on three independent axes.
8+
`os i18n extract` (merge mode — every existing translation is preserved)
9+
reconciles all of them:
10+
11+
**Keys the spec no longer has**, still carrying translations in
12+
`*.metadata-forms.generated.ts`. All three were removed deliberately and are
13+
now *rejected* by the schema, so their entries were dead weight:
14+
15+
- `capabilities.trash` / `capabilities.mru``enable.trash`/`enable.mru`
16+
retired in the 16.x line (#2377), with tombstone guidance in
17+
`UNKNOWN_KEY_GUIDANCE`.
18+
- agent `visibility` — removed 2026-07 (#1901).
19+
20+
**Keys the spec gained** but the bundles never learned: the
21+
`summaryOperations.*` sub-fields (`object` / `function` / `field` /
22+
`relationshipField` / `filter`), and `sys_invitation.business_unit_id` /
23+
`positions` from the ADR-0105 D8 placement work.
24+
25+
**Objects stuck on empty strings.** `sys_migration`'s labels and help text were
26+
committed as `""` in the ja-JP and es-ES bundles, which renders as *blank* in
27+
those locales rather than falling back to anything readable. They now carry the
28+
schema text like every other untranslated key.
29+
30+
No API or schema change — this only affects what the UI displays.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
feat(cli): `os i18n extract --check` — fail instead of writing when translation bundles have drifted
6+
7+
Generated translation bundles had no freshness gate, so they rotted silently
8+
until someone happened to re-run the extractor by hand. #3670 found three
9+
distinct drifts sitting in the committed bundles at once: translations left
10+
behind for schema keys that had been REMOVED, keys the schema had GAINED with
11+
no entry at all, and an object whose labels were committed as empty strings
12+
(which renders blank rather than falling back to anything readable).
13+
14+
`--check` writes nothing and exits non-zero when a fresh extract differs from
15+
what is committed in `--out`, listing each stale or missing file and printing
16+
the exact regenerate command. It runs the identical render path as a real
17+
extract — both branches iterate the same rendered set — so the check can never
18+
disagree with what writing would produce.
19+
20+
It runs in **merge mode** like any other extract, so it never asks anyone to
21+
re-translate: an up-to-date bundle re-extracts byte-identically. Requires
22+
`--out`, since there is nothing to compare against without it.
23+
24+
In this repo it is wired up as `pnpm check:i18n` and gated in CI, but the flag
25+
is on the CLI, so any consumer shipping generated bundles can gate them the
26+
same way.

.changeset/naming-drift-recheck.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(doctor): point the retired `reference_filters` hint at `lookupFilters` (#1878 §3 recheck)
6+
7+
`os doctor`'s snake→camel rule table advised "Use `referenceFilters`
8+
(camelCase)" — a key REMOVED from `FieldSchema` in #2377/ADR-0049, which the
9+
non-strict schema silently strips. The live successor is `lookupFilters` (read
10+
by the objectui lookup picker). The rule now matches both spellings and names
11+
the right key.

0 commit comments

Comments
 (0)