Skip to content

Commit 3043750

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21409-star-count-only
2 parents fce0c9a + 3a6d92f commit 3043750

45 files changed

Lines changed: 2803 additions & 245 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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
The list-view field-reference rule no longer walks a list view's own `tabs[].filter`
6+
7+
Clause-②: no
8+
9+
The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape, and this rule judges the parsed stack, so the key could never reach the walk: the parse refuses it first, with its prescription. The dead branch is deleted. The rule still judges `filter` and `userFilters.tabs[].filter` exactly as before.
10+
11+
No finding changes for any stack that `os validate`, `os lint` or `os build` accepts.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
`computeViewReferenceDiagnostics` no longer walks a list view's own `tabs[].filter`
6+
7+
Clause-②: no
8+
9+
The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape. The write door refuses it, and a stored or artifact-shipped body has it stripped by the conversion replay before it is served, so the read could never see it. A served body that still carries it is already badged by the spec diagnostics (`computeMetadataDiagnostics`), with the tombstone's prescription. The `userFilters.tabs[].filter`, `filterableFields` and `kanban` checks are unchanged.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Liveness ledger: the view container's body `name` row stays `dead`, and its note now states what the platform actually does with the key
6+
7+
Clause-②: no
8+
9+
- The old note said the body copy was "a copy nobody reads". Measured, the metadata door stamps the save name into every saved view body that has none, containers included (`normalizeViewMetadata` in `@objectstack/metadata-protocol`). Its overlay paths key on that stamped copy: `hydrateOverlayIntoRegistry` registers no body without a `name`, and `mergePackageAwareOverlay` slots an overlay row by it.
10+
- The verdict is unchanged, because the ledger's `live` means that authoring the key changes runtime behaviour. An authored container `name` only restates the key the container already registers under, or contradicts it. `os validate` and `os lint` keep warning `liveness-dead-property` ("drop it").
11+
- The note records why the key is kept rather than tombstoned: the door's own saves stamp it, so a tombstone would refuse the platform's own writes. A maintainer ruling also refused a spec-level forbid of a container's `name`.
12+
- It corrects the old attribution too. Artifact-shipped containers and the metadata-validation sweep author no `name`; what was read as theirs is the door's stamp.
13+
- The ledger README's `view` cell says the same. The `view.list.tabs` row's note now records that the two author-time walks that still read a list view's own `tabs` are deleted.
14+
- A comment in `system/i18n-resolver.ts` that still called the list view's own `tabs` a live carrier now says the key is a tombstone and `UserFiltersSchema.tabs` is the one carrier.
15+
- ⛔ No schema, parse, export, status or accept-set change.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `record:activity`'s props row names `items` / `loading` as the host's feed slot when it refuses them
6+
7+
Clause-②: no
8+
9+
`ComponentPropsMap['record:activity']` (`RecordActivityProps`) refused an authored `properties.items` or `properties.loading` with only the generic "Unrecognized key(s) on this `record:activity`" line. Both are keys the objectui `record:activity` renderer reads, as a feed a host that composes the block in code already owns, so an author copying a TSX composition into a JSON page met no reason for the refusal.
10+
11+
- The refusal now says who reads each key on each mount the row reaches. On a standalone `record:activity`, `items` is the host's data channel and `loading` the host's fetch state for that feed. On a `record:chatter` / `record:discussion` `feed`, which is the same object, nothing reads either. The remedy is the same on both: omit them. The block then presents the record page's discussion feed, and a standalone `record:activity` with no discussion context fetches the record's own `sys_activity` rows. This is the same `guidance` shape `record:history`'s row already uses for `entries` / `loading`.
12+
- The accept set does not change. Both keys stay refused, through the row and through `record:chatter` / `record:discussion`'s `feed`, which is the same object. Only the message text changes; `record:history` is unchanged.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: a `pie` / `donut` / `funnel` / `treemap` / `sankey` dashboard widget takes ONE measure with a dimension too — two or more are refused at `values`, and the check export is renamed `checkDashboardWidgetChartMeasureArity` (#21293; extends #20958)
6+
7+
Clause-②: yes (narrowing) — the accept set NARROWS (that is the change), and the published surface swaps one export for another: `checkDashboardWidgetDimensionlessMeasureArity` is removed and `checkDashboardWidgetChartMeasureArity` is added in its place, the same check with a second arm.
8+
9+
<!-- adr-0087: registered dashboard-widget-single-series-multi-measure-refused -->
10+
11+
**BREAKING** accept-set narrowing at `dashboard.widgets[].values`, plus one renamed
12+
export, shipped as `minor` under this repo's launch-window convention for breaking
13+
changes (`check-changeset-no-major` refuses `major` while the window is open, so
14+
breaking-ness is carried by this banner and by the ADR-0087 disposition above,
15+
never by the bump level). The prescription is registered under protocol major 18
16+
as `dashboard-widget-single-series-multi-measure-refused`.
17+
18+
**What was wrong.** The previous release refused two or more measures on a
19+
dimensionless `pie` / `donut` / `funnel` / `scatter` / `radar` / `treemap` /
20+
`sankey`, and stepped aside for any widget that declared a dimension. Five of those
21+
types draw ONE series whatever the dimension: objectui's chart renderer binds the
22+
first series on its `pie` / `donut`, `funnel`, `treemap` and `sankey` arms and reads
23+
no other, so `{ type: 'pie', dimensions: ['stage'], values: ['revenue', 'cost'] }`
24+
drew one slice per stage for `revenue` and no trace of `cost`. Measured on this tree
25+
before the change: that body parsed through `DashboardWidgetSchema` on all five
26+
types (and on `scatter` / `radar` / `bar` / `table`), while `bogusProp` on the same
27+
widget was refused by name, the lit control. After it, the five are refused at
28+
`widgets[N].values`; `scatter` and `radar` with a dimension are outside the ruling
29+
and parse as before.
30+
31+
### Write instead
32+
33+
| wrote | write instead |
34+
|---|---|
35+
| `{ id: 'mix', type: 'pie', dataset: 'sales', dimensions: ['stage'], values: ['revenue', 'cost'] }` | `{ id: 'mix', type: 'table', dataset: 'sales', dimensions: ['stage'], values: ['revenue', 'cost'] }` — a column per measure |
36+
| the same, wanting a chart | `type: 'bar'` (or `column` / `horizontal-bar`) — one bar per measure in each stage |
37+
| the same, wanting the pie | `{ id: 'mix', type: 'pie', …, values: ['revenue'] }` **and** `{ id: 'mix_cost', type: 'pie', …, values: ['cost'] }` — one widget per measure, each with its own `id` (and `layout`, if you pin positions) |
38+
| `import { checkDashboardWidgetDimensionlessMeasureArity } from '@objectstack/spec/ui'` | `import { checkDashboardWidgetChartMeasureArity } from '@objectstack/spec/ui'` — same `(widget, ctx)` signature; chain it where the old name was chained |
39+
40+
No conversion does this for you: whether a two-measure pie by stage meant a table, a
41+
grouped bar chart or two pies is an authoring choice. The refusal is ONE `custom`
42+
issue at `widgets[N].values` naming the widget's `id`, the number of measures and
43+
the authored `type`, and saying that type draws one series whatever its
44+
`dimensions`.
45+
46+
**Why the export is renamed.** The dimensionless rule's check now has a second arm
47+
that judges widgets WITH a dimension, so its old name described a boundary that no
48+
longer exists. It refuses everything the old name refused, word for word on a
49+
dimensionless widget. No first-party consumer chained the old name: objectui's
50+
`DashboardWidgetSchema` mirror chains `checkDashboardWidgetStageOrder` and
51+
`checkDashboardWidgetMetricMeasureArity` only, measured at the pinned objectui
52+
commit and on objectui's `main`.
53+
54+
**Nothing else moves.** One measure parses on every type; `scatter` and `radar`
55+
keep accepting several measures with a dimension; every type in
56+
`DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` keeps accepting any number of measures with
57+
or without a dimension; a dimensionless widget of the five keeps the dimensionless
58+
refusal, word for word and still ONE issue; the metric family's refusal is
59+
unchanged; an empty `values` keeps its `too_small`; a `type` outside
60+
`ChartTypeSchema` reports the type refusal alone. Census at the branch point
61+
(`4b20c8474`), every tracked `.ts` / `.tsx` / `.js` / `.mjs` / `.cjs` / `.json` /
62+
`.md` / `.mdx` / `.yml`: 496 literals carry `values: [...]`, 33 of them on one of
63+
the seven types, and the only dimensioned multi-measure one on the five is a spec
64+
test fixture that pinned the old acceptance (moved to the refusal in this change).
65+
The same scan over objectui at its pinned commit (`89cad75d5`) finds no authored
66+
widget of that shape — its one hit is the prose example in a changeset.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
'@objectstack/service-analytics': minor
4+
---
5+
6+
Row-level security policies and the analytics native-SQL path judge a comparand against a declared boolean field by the platform's boolean-comparand rule, the one the data engine's `where` already applies
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) a refusal or narrowing of a filter comparand against a declared boolean column at two compilers outside the engine's where door, the same comparand that door already judges: no authorable key, spelling, export or stored shape moves. RowLevelSecurityPolicySchema, every permission set, every dataset, cube and analytics query parse and save as before, the predicate's and the filter's text are untouched, @objectstack/plugin-security and @objectstack/service-analytics export the same names with the same types, and no stored row is read or rewritten. Which boolean the author meant by a refused comparand is not something a ledger entry can decide, so there is nothing for objectstack migrate meta to rewrite. The other categories are closed on facts: both packages publish (not unpublished); no ADR-0087 id covers a filter comparand's type and this diff adds none (not registered / already-registered); and the change is runtime behaviour with no published interface or type changed (not runtime-interface-only / type-surface-only). -->
11+
12+
**BREAKING**: this narrows what two compilers outside the engine's `where` door accept. The RLS compile seam now drops a row-level policy, and the analytics native-SQL face now refuses a query, when either compares a declared boolean field with a comparand outside the accepted set. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, type or error code changes.
13+
14+
- **Row-level security (`@objectstack/plugin-security`).** A compiled `using` / `check` predicate on a `boolean` or `toggle` column (or a `formula` returning `boolean`) is judged by `booleanComparandDoorVerdict` from `@objectstack/spec/data`, in the same pass as the number rule. `'true'` / `'false'`, `'1'` / `'0'` and `1` / `0` are read as the boolean each names. Anything else the rule refuses (a string such as `'yes'`, `'TRUE'` or `''`, a number other than `1` / `0`) drops the policy as a refused comparand: the read is filtered by the deny sentinel, the write is refused 403, and the WARN line names the clause, the field and the position. Before, `record.flag != 'true'` kept every row on SQLite and the write check admitted every row, so the exclusion the author wrote was not applied.
15+
- **Analytics native SQL (`@objectstack/service-analytics`).** The query's `where` (and the dataset query's `runtimeFilter`, which is merged into it), each measure's own `filter` and a dataset's own `filter` are judged by the same rule before the statement compiles. An accepted spelling is read as its boolean, and anything else the rule refuses is refused `INVALID_FILTER` / 400 with the rule's own message, before any statement runs. The native strategy now answers what the engine-aggregate strategy answers. Before, `{ flag: 'true' }` counted no rows on SQLite, `{ flag: { $ne: 'true' } }` counted every row, and `{ flag: 'yes' }` answered 200.
16+
- **What you may notice.** A policy or analytics filter that compared a boolean field with a value outside the accepted set now refuses instead of answering. Write `true` / `false`. A policy `record.flag == 1` now admits writing a `true` row, which its read already showed.
17+
- **Unchanged.** A boolean literal, a column that is not boolean, a `{ $field }` reference, and an object whose declaration cannot be read (nothing is judged without one).
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-audit': patch
3+
---
4+
5+
fix(plugin-audit): an activity row recording an update whose every changed field the reader is withheld is no longer served to that reader, on any listing face
6+
7+
Clause-②: no
8+
9+
A `sys_activity` row's recorded change (`metadata.old` / `metadata.new`) is narrowed key by key for each reader, through the security service's served-fields answer. An update whose every changed field the reader is withheld still reached that reader as a row with an empty change, and its summary, actor and timestamp said that the record changed, and when. An org member holding object-level `sys_activity` read was served that row for each sign-in stamp on a colleague's identity record (`last_login_at`), and for each failed-sign-in counter bump, lockout, password-change stamp and MFA-required stamp.
10+
11+
Such a row is now withheld from that reader as a row:
12+
13+
- **What counts as one.** An update row (its stored change has both an `old` and a `new` side) whose stored change had at least one key, where the reader is served none of those keys. The keys are read from the STORED change, not the redacted one.
14+
- **What is unaffected.** A create or a delete keeps its row. A row whose stored change is empty on both sides (an update that touched only `internal` fields) is unaffected. A mixed update keeps its row, with the served keys only. A reader served every field (an administrator) still reads every row with its change, within the pre-scan's bound. A system-context read is not narrowed.
15+
- **Every face agrees.** The rule is a WHERE built from a system-context pre-scan on `find`, `findOne`, `count` and `aggregate`. So a list's `total`, its pages, a by-id read (`404`) and a grouped count agree with the rows served. A pre-scan that reaches its 2,000-row bound answers a broad read from the rows it judged, for every reader, administrators included, and logs a warning. The remedy is to scope the query by `object_name` and `record_id`.
16+
17+
No migration: no key, export or config changes. A reader the security service gives no answer for (no security plugin wired) is not narrowed, as before.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the null ordering-comparand refusals name only evaluation faces that exist, and say only what was measured
6+
7+
Clause-②: no
8+
9+
`FieldOperatorsSchema` and `ComparisonOperatorSchema` refuse a `null` comparand of `$gt` / `$gte` /
10+
`$lt` / `$lte` with a pointed message. Its example of the evaluation faces disagreeing named
11+
driver-memory's reference matcher, which has been deleted, so an author or agent reading the
12+
refusal went looking for a face that no longer exists. The example now names two faces that exist
13+
and were measured to disagree: driver-memory's query path reads a stored `null` as equal to the
14+
comparand, so `{"$gte": null}` admits that row, while driver-sql compares against SQL `NULL` and
15+
admits no row.
16+
17+
That refusal and its runtime twin, the `parseFilterAST` refusal for the same comparand
18+
(`Operator "$gt" on field "…" does not accept a null comparand …`), both said "no two evaluation
19+
faces agree" on what an ordering against `null` matches. Measured, two faces do agree (driver-sql
20+
and formula both admit no row), so both now say "the evaluation faces do not agree".
21+
22+
Text only: each message's first sentence, its prescription (`{"$eq": null}` / `{"$ne": null}`), the
23+
schema door's ruling sentence and the runtime door's "NOT applied" sentence are unchanged, and both
24+
doors accept and refuse exactly the same filters. A client or log filter that matches the old
25+
wording needs the new spelling.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
`PermissionDeniedError` declares its 403 as `status` as well as `statusCode`, so a permission refusal answers 403 at every door (#21405).
6+
7+
Clause-②: no
8+
9+
The class declared `statusCode` alone, unlike every other error class in `errors.ts`, and a door that reads `status` alone derived no status from it. On a showcase boot, a plain member's `POST /api/v1/share-links` on a record they cannot read answered `500` with code `PERMISSION_DENIED` through `plugin-sharing`'s route door, while the runtime dispatcher's `/share-links` domain answered the same refusal with `403`. Both doors now answer `403 PERMISSION_DENIED`. The code, the message and `statusCode` are unchanged.

‎content/docs/permissions/rls.mdx‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,23 @@ Layer 1 (business RLS) ─┘
197197

198198
## The fail-closed contract
199199

200-
Four ways a policy denies rather than leaks:
200+
Five ways a policy denies rather than leaks:
201201

202202
1. A policy exists but **every** applicable expression fails to compile → a
203203
deny-everything filter.
204204
2. A referenced context variable is missing, null, or an empty array → that
205205
policy drops out (it cannot match).
206206
3. A policy references a column the object doesn't have → deny.
207-
4. `check` is omitted → `using` stands in as the `check`. The choice is
207+
4. A policy compares a column with a literal its declared type cannot be
208+
compared with → that policy drops out, for `using` and `check` alike: on a
209+
`boolean` / `toggle` column, anything but `true` / `false` and the
210+
spellings `'true'` / `'false'`, `1` / `0` and `'1'` / `'0'`
211+
(`active == 'yes'`, `active != 'TRUE'`, `active == 2`); on a numeric
212+
column, a comparand that is not a number (a string with no numeric reading,
213+
a boolean). These are the comparisons a caller's `where` is refused for
214+
(`INVALID_FILTER`). An accepted spelling is read as the value it names, so
215+
`active != 'true'` hides the `true` rows exactly as `active != true` does.
216+
5. `check` is omitted → `using` stands in as the `check`. The choice is
208217
made per operation across all the applicable policies, not policy by
209218
policy: when any of them declares a `check`, only the declared checks
210219
decide, and a USING-only sibling's `using` is not part of the check.

0 commit comments

Comments
 (0)