Skip to content

Commit c4e933e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15731-type-check-gates-skip-gitignored
2 parents 901044c + b237592 commit c4e933e

113 files changed

Lines changed: 9942 additions & 462 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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/service-analytics": minor
3+
"@objectstack/driver-sql": minor
4+
---
5+
6+
The analytics SQL compilers compile the case-sensitive text family per dialect, so a `$contains` policy on SQLite stops admitting rows it excludes (#15684)
7+
8+
`$contains` / `$notContains` / `$startsWith` / `$endsWith` are case-SENSITIVE on every backend (#4706 Q2 = A). All three of `service-analytics`' SQL compilers emitted `col LIKE ? ESCAPE ?` on every dialect, and SQLite's `LIKE` folds ASCII case unconditionally — the fold cannot be turned off per statement, because `PRAGMA case_sensitive_like` is a connection-global switch. Measured on sql.js over the shared `FILTER_TEXT_ROWS` fixture, `{ name: { $contains: 'acme' } }` answered `['1','2']``ACME Corp` **and** `acme corp` — where `FILTER_TEXT_CASES` says `['2']`.
9+
10+
On two of the three compilers that is a wrong chart. The third is `read-scope-sql.ts`, the ADR-0021 D-C read scope: a scope that **admits** rows the policy's case-sensitive predicate excludes is over-reach, not a loose filter — the same reading that file already applied to its own `LIKE` escaping. The `/analytics/sql` echo was wrong in a third way: it printed `LIKE` while the statement it claims to reproduce ran through a driver that has emitted `GLOB` on the SQLite dialects since #6518.
11+
12+
What changed:
13+
14+
- **The construct is chosen per dialect** (`text-match-sql.ts`), arm for arm with `driver-sql`'s own table: `GLOB` on SQLite (case-exact by definition, with its own `*` / `?` / `[` escaped class and no `ESCAPE` clause), `LIKE` over `CAST(… AS BINARY)` on MySQL, and `LIKE` **unchanged** on Postgres, where it is already exactly the ruled semantics. There is no single construct that is case-exact and parses on all three, so the dialect had to become an input rather than a guess.
15+
- **The dialect arrives from the driver that will execute the statement.** New optional `AnalyticsServiceConfig.sqlDialect`, wired by `AnalyticsServicePlugin` from `IDataEngine.getDriverForObject`. `SqlDriver.dialectName` is now public so that answer can be read without a second dialect-resolution table drifting behind the driver's own knex spellings; it is derived and read-only.
16+
- **A host that answers no dialect keeps the `LIKE` it always got** — "cannot answer, do not block". Postgres deployments see byte-identical SQL.
17+
18+
`$icontains` is untouched: it keeps its own ASCII-only fold on both sides, and collapsing the two families onto one path would hand the case-exact family back the fold the ruling took away from it. `LIKE` escaping is unchanged wherever a `LIKE` is still emitted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
The model-facing solution-blueprint mirror can no longer generate an identifier the applier rejects.
6+
7+
`SolutionBlueprintSchema` (what `apply_blueprint` validates against) and `SolutionBlueprintStrictSchema` (the OpenAI-strict structured-output contract the design model generates against) are two declarations of one shape. Their KEYS were pinned by an existing parity test; their VALUES had never been. Every identifier in the lenient schema carried `.regex(/^[a-z_][a-z0-9_]*$/)` and not one identifier in the strict mirror carried it — 20 leaves apart, measured.
8+
9+
The consequence was a build whose approval did nothing. Asked for a CRM, the design model emitted a `company_size` select whose option values came straight off the labels — `1_49` for 「1-49人」. Generating that was legal. Applying it was not: on the turn the user clicked 「确认,开始搭建」 the deterministic confirm replay handed that exact blueprint to `apply_blueprint`, which refused it wholesale (`objects.0.fields.2.options.0.value: Invalid string: must match pattern /^[a-z_][a-z0-9_]*$/`) and staged nothing. The app appeared only because the model noticed the error card and retried with a repaired blueprint the user had never seen.
10+
11+
Every identifier leaf in the strict mirror now carries the same `SNAKE_CASE` constraint the lenient schema enforces — object / field / view / dashboard / widget / app / nav names, `reference`, `nameField`, `columns`, `groupBy`, `measure`, roll-up `object` / `field` / `relationshipField`, condition `field`, and select option `value`. The constraint is emitted into the JSON Schema the model is given (`pattern`), so an out-of-pattern identifier is refused at generation instead of after approval. Option `value` additionally spells out the case that produced the incident: it may never start with a digit, so 「1-49人」 is authored as `size_1_49` — the `label` keeps the human wording untouched, and only the stored value is an identifier.
12+
13+
A new `strict mirror ↔ lenient schema — VALUE parity` test walks both schemas leaf by leaf and fails on any future divergence, the value-side twin of the key-parity gate that already guards this pair.
14+
15+
Refs cloud#1967.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): a published `BulkDataEvent` now names the ONE organization the tenant wall named for the batch
6+
7+
`BulkDataEventSchema.organizationId` (`@objectstack/spec/api`, declared by the
8+
contract half) is one organization for a whole predicate write, or absent. The
9+
only bulk producer — `publishBulkDataEvent`, behind the `multi: true` branches
10+
of `update()` / `delete()` — never set it, so every `data.records.updated` /
11+
`data.records.deleted` event read "not asserted" and a tenant-scoped consumer
12+
could deliver nothing per organization on the bulk path. This is the bulk half
13+
of the cross-tenant webhook fan-out leak; the single-record half (`DataEvent`)
14+
landed separately.
15+
16+
The producer now stamps the key from what it already holds — no second query
17+
on the publish path: under `isolated` the caller's active organization (the
18+
Layer 0 wall's equality term), under `group` the caller's membership set when
19+
it names exactly one organization. It is OMITTED — never the caller's active
20+
organization standing in — on a `single`-posture deployment, on an `isSystem`
21+
context (no wall composed), on a multi-membership `group` sweep, when no
22+
enforcement layer injected a posture (the `OS_TENANCY_POSTURE` env fallback is
23+
deliberately not consulted), when the caller may have crossed the wall as a
24+
`PLATFORM_ADMIN` or carries no resolved posture rung, and on an object the wall
25+
does not key on. `absent` here means "the producer did not assert one
26+
organization for the batch", deliberately NOT the `DataEvent` reading
27+
"belongs to no organization".
28+
29+
Which objects "the wall does not key on", stated exactly rather than claimed as
30+
a mirror: plugin-security's Layer 0 composes no wall when its `tenancyDisabled`
31+
input is true or the object carries no `organization_id`, and it folds THREE
32+
clauses into `tenancyDisabled``tenancy.enabled === false`,
33+
`systemFields.tenant === false`, and the deployment's `platformGlobalObjects`
34+
carve-out. The producer reads the registry's binding of that predicate
35+
(`carriesTenantScopeColumn`: the first two clauses plus the column clause) and
36+
answers absent on a federated (`external`) object; a custom
37+
`tenancy.tenantField` is therefore not an exit by itself — the object is walled
38+
iff it carries `organization_id`, and the key follows the wall. The third
39+
clause is deployment-declared and not readable by the engine: a
40+
deployment-exempted object under an armed wall is still stamped with the
41+
caller's organization by this producer alone, and that population's exact
42+
answer is decided by the seam ruled on in #15706.
43+
44+
`patch`, not `minor`: the act adds no member to this package's published
45+
surface. `carriesTenantScopeColumn` is exported at module level inside
46+
`registry.ts` only — `@objectstack/objectql`'s entries (`.`, `./core`) re-export
47+
named members and never `export *`, so `dist/index.d.ts`, `dist/core.d.ts` and
48+
both entries' runtime export lists are unchanged (measured on the built `dist`,
49+
with a firing control) — and the emitted event's member was declared, typed
50+
and paid for at `minor` by the spec half. Producer conformance to an existing
51+
optional member under `fix(` changes no public surface of this package.

.changeset/great-clouds-repair.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/plugin-hono-server': patch
3+
---
4+
5+
`GET /auth/me/localization` answers the deployment's resolved `currency` and `timezone` instead of `null`
6+
7+
The handler read both off the request `ExecutionContext`, citing ADR-0053, but the resolver serving this surface is a hand-rolled envelope that never carried them — so every authenticated caller was answered `currency: null, timezone: null` whatever the `localization` settings said, and the console's regional-formatting seed was fed nulls. All three values now come from one reading of the same `resolveLocalizationContext` cascade the dispatcher's shared assembler uses. `locale` resolution is unchanged. `timezone` now always answers (cascade floor `UTC`); `currency` still answers `null` when the deployment configures none — that value has no floor.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
fix(rest): metadata label lookup honours the stack's declared `i18n.fallbackLocale` / `defaultLocale` instead of falling through to the `en` bundle (#14882)
6+
7+
On a workspace whose labels are authored in `zh-CN` (`defaultLocale: 'zh-CN'`,
8+
`fallbackLocale: 'zh-CN'`) and which ships only a courtesy `en` translation bundle,
9+
`GET /api/v1/meta/object/:name`, the `/meta/:type` list, `GET /api/v1/meta` and the
10+
public-form schema served the ENGLISH bundle labels to a `zh-CN` request (`Entry Sheet`
11+
for an authored `填报单`, `KPI Assessment` for `KPI 考核管理`). The document translators walk
12+
`requested locale → fallback chain → authored label` and default the chain to a literal
13+
`['en']`; every REST seam passed none, so the declared fallback never reached the chain
14+
and `en` was consulted before the authored label.
15+
16+
Every metadata translation seam now passes `fallbackChain: [i18n.getFallbackLocale()]`
17+
the locale the i18n service's own `t()` falls back to, which `I18nServicePlugin` receives
18+
from the stack config as `fallbackLocale || defaultLocale || 'en'`. For the workspace
19+
above a `zh-CN` request now resolves `zh-CN → zh-CN → authored label` (the authored
20+
Chinese labels), an `en` request still gets the `en` bundle, and a `zh-CN` bundle, when one
21+
is shipped, still wins over the authored label.
22+
23+
Feature-detected: an i18n service that does not declare a fallback (the method is
24+
optional on `II18nService`; the core in-memory fallback has none) gets no chain and the
25+
resolver's own default applies exactly as before. A stack declaring `defaultLocale: 'zh-CN'`
26+
with `fallbackLocale: 'en'` is likewise unchanged — the declared `en` is honoured as it
27+
reads.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/service-i18n": minor
3+
---
4+
5+
feat(service-i18n): `FileI18nAdapter.getFallbackLocale()` reports the `fallbackLocale` the adapter was constructed with (#14882)
6+
7+
Implements the new optional `II18nService.getFallbackLocale()`. `I18nServicePlugin`
8+
already receives `fallbackLocale || defaultLocale || 'en'` from the stack's `i18n`
9+
config on both boot paths (`os serve`, the dev plugin); this makes that declaration
10+
readable, so the REST metadata reads pass the document translators the same fallback
11+
locale `t()` itself consults. Returns `undefined` when no `fallbackLocale` was given.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `II18nService.getFallbackLocale()` — the declared fallback locale is readable, so the metadata-document translators can be handed the chain the deployment declared (#14882)
6+
7+
`ResolveOptions.fallbackChain` on the `@objectstack/spec/system` label
8+
resolvers (`translateMetadataDocument`, `translateObject`, `translateApp`,
9+
`resolveViewLabel`, …) is the ordered list of locales consulted after the
10+
requested one and BEFORE the authored label. Nothing on `II18nService`
11+
exposed the deployment's declared fallback (`i18n.fallbackLocale`, else
12+
`defaultLocale`), so no serving layer could thread it, and every caller fell
13+
to the resolver's literal `['en']` default. A `zh-CN` workspace that shipped a
14+
courtesy `en` bundle therefore served English bundle text to a `zh-CN`
15+
request ahead of its own authored Chinese labels.
16+
17+
- New optional contract member `II18nService.getFallbackLocale?(): string | undefined`
18+
— the locale the service's own `t()` consults second. `undefined` (or the
19+
method absent) means nothing was declared, and a serving layer must then
20+
leave the resolver's default in place rather than invent a chain.
21+
- The `fallbackChain` documentation now states who supplies it (the serving
22+
layer, from `getFallbackLocale()`) and that the `['en']` default applies
23+
only when a caller declares no chain at all. The resolver's behaviour for
24+
a caller that passes nothing is unchanged.
25+
26+
Additive: no existing implementation or caller changes shape.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
The tenant-scope and owning-business-unit system columns now render a localised display name on the `/meta` read exits, as the other platform-injected columns already did.
6+
7+
`translateObject` carries a built-in label table for the columns the platform injects onto every eligible object, applied while a column still carries its injected English default, so a `zh-CN` / `ja-JP` / `es-ES` request never sees the English label on a custom object that ships no translation entries of its own. The table covered `owner_id`, `created_at`, `created_by`, `updated_at` and `updated_by` but not the two remaining injected columns, `organization_id` (`Organization`) and `owning_business_unit_id` (`Owning Business Unit`), so those two leaked English on every locale. Both rows are added, with the wording the platform bundles already use for the same columns on platform objects. The identity-stable column definitions are untouched, no new authorable key is introduced, and a label a tenant or author customised is still never overridden.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`chart-axis-not-selected` resolves a report chart against its own `chart.yAxis`, not `report.values` (#15734)
6+
7+
**Behaviour change — one false finding removed on the report surface.** A report chart whose `chart.yAxis` names a declared measure that `report.values` does not select no longer raises a `chart-axis-not-selected` warning. Nothing else about the rule moves, and no other surface moves at all.
8+
9+
The warning stated a query consequence the renderer refutes. Read at the `@object-ui` revision this repo pins (`.objectui-sha`), `plugin-report/src/DatasetReportRenderer.tsx` does not query `report.values` for the chart at all — it runs the chart's own, narrower query out of the two axis strings:
10+
11+
```
12+
const state = useDatasetRows(
13+
dataset,
14+
plan.kind === 'series' && xAxis ? [xAxis] : [],
15+
wantsQuery && yAxis ? [yAxis] : [],
16+
```
17+
18+
and says so in that file's own words at the `scopeOrder` docblock: *"the embedded chart queries only `chart.xAxis` × `chart.yAxis`"*. So the measure the warning said "the query does not return" is exactly the one the query asks for, and the chart plots it. `report.values` is the selection of the TABLE beneath the chart.
19+
20+
Both limbs follow from that one measurement:
21+
22+
- **No not-selected check at the report `chart.yAxis`.** That position IS the chart's query, so it cannot fail to select itself. `chart-measure-unknown` there is untouched: an UNDECLARED measure is still no column at all, and still an `error`.
23+
- **`chart.series[].name` resolves against the singleton `{ chart.yAxis }`.** The entry is a display-name override paired with a DERIVED series, and the chart derives exactly one (`buildChartSeries(…, [xAxis], [yAxis], …)`). An entry naming `chart.yAxis` now lands however the table is selected, and one naming any other declared measure is still reported — including a measure `report.values` does select, which it could not reach before.
24+
25+
The list-view and page-component surfaces are unchanged, and carry firing controls that say so: on both, `values` IS the measure set the query asks for (`ObjectView` hands it to the chart; `ObjectChart` queries `{ dimensions: schema.dimensions, measures: schema.values }`), so the existing resolution is the right one there.
26+
27+
The per-position tier and consequence wording is untouched — only the SET the report surface resolves against moves.
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+
`os serve` now says so when the SQLite file it is serving is no longer the file at its configured path.
6+
7+
Deleting the data directory under a running server — `rm -rf .objectstack/data`, which is what a `demo:reset` script does and what a fresh-database repro starts with — unlinks the inode without touching the process. SQLite keeps reading and writing the now-invisible file, health keeps answering `200`, and a later boot creates a brand-new database at the same path. From that moment every filesystem inspection of that path describes a *different* database than the running server answers from, and nothing anywhere says so: a row edited there has no observable effect on the live server, and a user who authenticates against the live server is not in that file. Both readings are true, both look like a broken write path, and one investigation that reported them as evidence cost a full P0 cycle.
8+
9+
A boot that serves an on-disk SQLite file now records that file's identity once the boot is complete and re-checks it on a 30-second interval. When the file is gone, or the path holds a different file, it reports **once** at `error` — naming the path, the consequence (every external observation of this deployment is now false, and it will keep looking healthy) and the fix (restart the server so it opens the file that is at that path now).
10+
11+
It refuses nothing and retries nothing: the running server is still correct, merely invisible, and breaking a working dev loop to fix a reporting gap would trade a bad hour for a worse one. Nothing is added to any payload, endpoint or state file. Silence from the check is not a claim that the file is intact — every uncertainty in it resolves toward staying quiet, because a false report would send an operator to restart a server whose database is fine.

0 commit comments

Comments
 (0)