Skip to content

Commit e9d1a26

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20280-mysql-date-read-century
2 parents f3afc10 + e6b7d8c commit e9d1a26

37 files changed

Lines changed: 1336 additions & 107 deletions

‎.changeset/19953-rls-check-default-composition-text.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Text only. No schema shape, accepted value or runtime behaviour changes.
1616

1717
A policy is applicable when it is not `enabled: false`, its `object` is the written object or `'*'`, its `operation` is the write's own or `'all'`, and the caller holds one of its `positions` when it lists any. A `check` on a `select` or `delete` policy is never evaluated.
1818

19-
The check runs on the new row of a single-record insert and of a by-id update. An array insert and a `multi: true` update are not post-image checked; those are tracked in #19964 and #19950, and the texts now say so instead of implying every insert and update is checked.
19+
When this text change was written, the check ran on the new row of a single-record insert and of a by-id update only, and the texts said so. The same release extends it: every row of an array insert and of a `multi: true` update is judged (#19964, #19950), and a by-id update is also judged on the row its `beforeUpdate` hooks leave (#19989). The texts now state that every row an insert or an update writes is judged (#19967).
2020

2121
- **`@objectstack/spec`**: the `check` describe and TSDoc state this composition and that scope. The `rowLevelSecurity[].priority` refusal no longer gives "applicable policies OR-combine (most permissive wins)" as its reason, which is not true of the write check, and the file overview limits "OR-combine" to reads. The generated reference pages (`references/security/rls`, `references/security/permission`) are regenerated from the describe.
2222
- **`@objectstack/lint`**: the `rls-predicate-*` findings on a `using` now also say what the dropped `using` does to an insert. On an `insert` or `all` policy, when no applicable policy for the insert declares a `check`, that `using` is also the single-record insert check. If nothing else in that set compiles, every single-record insert the policy governs is refused with `PermissionDeniedError`. The findings on a `check` now say the refusal is a blanket one only when no other applicable policy declares a `check` that compiles.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the RLS `using` / `check` texts say what the write gate enforces today — every row an insert or an update writes is checked, a check-only `update` policy is legal, and an `insert` policy's `using` is its check when none is declared (#19967)
6+
7+
Clause-②: no
8+
9+
Text only. No schema shape, accepted value or runtime behaviour changes.
10+
11+
- **`RowLevelSecurityPolicySchema.check`**: the describe and TSDoc said the check ran on the new row of a single-record insert or a by-id update, and that an array insert and a `multi: true` update were not post-image checked. Every insert and update shape is now judged: each row of an insert, an array insert included, as its `beforeInsert` hooks leave it, and each row an update changes, by id or `multi: true`, as the prior row merged with the final payload after its `beforeUpdate` hooks. One failing row refuses the whole write. A by-id update is also judged, before its hooks, on the change set as sent.
12+
- **`RowLevelSecurityPolicySchema.using`**: the describe called it a filter for SELECT/UPDATE/DELETE and "optional for INSERT-only policies", and the TSDoc said UPDATE requires it. It now says, per operation, which rows it admits, that it stands in as the check on an insert or update when no applicable policy declares `check` (on an `insert` policy that is its only effect), that a `select` or `delete` policy needs it, and that an `insert`, `update` or `all` policy may declare `check` alone.
13+
- **The "at least one of `using` or `check`" refusal**: its head is unchanged. It no longer says an UPDATE policy must provide `using` or that an INSERT policy must provide `check`. It names what each operation takes.
14+
- **OR-combination**: the schema overview, the `priority` tombstone notes and the `os migrate meta --from 16` prose for the `priority` removal no longer say applicable policies OR-combine with "most permissive wins" without qualification. That holds on reads. On a write, the check is chosen per operation across the applicable policies first, and the chosen predicates then OR-combine.
15+
- **Default deny**: the overview now limits "default deny" to the policies that apply. A caller to whom no policy applies is not restricted by the policies; the tenant wall still applies.
16+
- The generated reference pages (`references/security/rls`, `references/security/permission`) and `docs/protocol-upgrade-guide.md` are regenerated from these sources.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`ERROR_CODE_LEDGER['@objectstack/lint']` now lists `INVALID_ARTIFACT_PACKAGES`, the code `packages/lint`'s `packagesOf` reader stamps for a malformed `stack.packages` (#20206) — required by `check:error-code-provenance`, which refuses a registered code stamped by a package whose own owner key does not list it.
6+
7+
Clause-②: yes
8+
9+
Provenance, not identity: the code was already registered under `@objectstack/core` (`resolveArtifactPackageOrder`, the producer `packagesOf` deliberately mirrors rather than mints a new code for), so the `ErrorCode` union, the wire, and every other package's rows are unchanged. What widens is the per-package face a consumer reads from `ERROR_CODE_LEDGER['@objectstack/lint']`, newly present where it was absent before. Nothing to migrate.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
`packages/lint`'s five `stack.packages` readers (four named by #20206, plus one added by #20208 after that card's site census) now refuse a PRESENT non-array `packages` — `{}`, `0`, `'x'`, a keyed object, and (as of this round) `null` too — instead of silently reading it as "no packages" (#20206, ruling A on #15293 comment 5634034754; the `null` leg is ruling A on #19926, comment 5805260775: `null` is malformed, everywhere). For every shape other than `null`, this is the same way `@objectstack/core`'s `resolveArtifactPackageOrder` already refuses it, with the same registered code; `@objectstack/core`'s `resolveArtifactPackageOrder` refuses `null` the same way (#19926).
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) an already-malformed `packages` value is refused rather than converted; no key, export or stored value moves, and nothing in this repo emits the shape today -->
10+
11+
- **What changes**: `validateObjectReferences`, `validateTranslationReferences` and `validateMappingTargetFields` (the three public `@objectstack/lint` functions these readers sit behind) now throw an `INVALID_ARTIFACT_PACKAGES` error (ADR-0112, `status: 422`) instead of returning findings, when the stack they are handed carries a `packages` key that is present but not an array — `null` included. Only `os lint` reaches this refusal — exit 1, the message on stdout (`printError`), `code` under `--json`; `os validate` and `os build` already refuse a malformed `packages` earlier, at `ObjectStackDefinitionSchema.safeParse`, before these rules ever run.
12+
- **What does not change**: an absent `packages` (the key omitted, or explicitly `undefined`) is still read as "no packages" — unchanged. A well-formed `packages[]` array is read exactly as before, junk entries dropped exactly as before.
13+
- **Fix**: write `packages` as an array of `{ manifest: … }` entries, or omit the key entirely for a single-package stack.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
fix(lint)!: a view container whose `object` names no object is refused by `os validate`, `os build` and `os lint` (`object-reference-unknown`), and the refusal names the namespace-prefixed object when that is the one the stack declares (#20216)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING** — an accept-set narrowing on one authored key, shipped as `minor` under the
10+
launch-window convention (`check-changeset-no-major` refuses `major` until GA; breaking-ness
11+
is carried by this banner and the ADR-0087 disposition below, not by the level).
12+
13+
**What changed.** `ViewSchema.object` is how a stack-level `views: [...]` container says
14+
which object its views belong to, and it is the key the runtime indexes views by
15+
(`getViewsByObject()` / `GET /meta/view?object=`). The schema declares it `z.string()`, and
16+
nothing resolved it: `defineStack`'s cross-reference check reads a container's
17+
`list.data` / `form.data` bindings, never the container's own key. So a container bound to a
18+
name no object carries passed: `os validate` printed "Validation passed" and exited 0,
19+
saying nothing about the view, and `os build` / `os lint` run the same rule table. At
20+
runtime none of its views was found for any object. The common case is not a typo but a
21+
missing namespace prefix — `object: 'order_line'` in a project whose object is
22+
`my_app_order_line` — which is exactly what `os generate view` wrote in every namespaced
23+
project until its template learned the prefix.
24+
25+
The key now joins `validateObjectReferences` and rides the ladder every other object-name
26+
site on that rule uses, resolved against the same set as a field's relationship target:
27+
28+
1. the stack's own objects, or an object an entry of the artifact's `packages[]` provides → ok;
29+
2. a known platform object (`PLATFORM_PROVIDED_OBJECT_NAMES`) → ok;
30+
3. unresolved and not platform-prefixed → **`error`** `object-reference-unknown` at
31+
`views[N].object`, so `os validate` / `os build` / `os lint` exit 1;
32+
4. unresolved, platform-prefixed, registered by nothing → the existing
33+
`object-reference-unregistered-platform` advisory.
34+
35+
The refusal lists the objects the stack does declare, and when the bound name is exactly a
36+
declared object minus the stack's `manifest.namespace` prefix, the hint names that prefixed
37+
object outright. Not judged, on purpose: a container that carries no `object` (its binding
38+
then falls back to `list.data.object` / `form.data.object` / its `name`, a different
39+
reference), and a container authored at runtime (this rule does not run on a `view` write at
40+
the runtime publish gate; that door is unchanged).
41+
42+
## The accept set, before and after
43+
44+
This is a behaviour table, not a rewrite: the FROM column is what the door did, the TO
45+
column is what it does now.
46+
47+
| where | FROM | TO |
48+
|:--|:--|:--|
49+
| `os validate`, `os build`, `os lint` on a view container bound to a name no object carries | exit 0, no finding | exit 1, `object-reference-unknown` at `views[N].object` |
50+
| the same, on a platform-prefixed name nothing registers | exit 0, no finding | the `object-reference-unregistered-platform` advisory, exit unchanged |
51+
| a runtime `view` write | unchanged | unchanged |
52+
53+
Nothing an author writes changes spelling, and no key or value is retired. A container that
54+
is refused was already dead at runtime; the finding's own hint says which object to bind it
55+
to.
56+
57+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable moves: `packages/spec` is untouched, `ViewSchema.object` keeps its key, its type and its legality, and no stored metadata representation changes shape, so `objectstack migrate meta` has nothing to rewrite and the ledger has no row to gain. What narrows is the set of VALUES the author-time rule accepts for a reference that must resolve to a declared object, and which declared object an author meant is a fact about their stack, never something a mechanical conversion can derive; the refusal carries its own correction. The other categories are closed on facts: `@objectstack/lint` publishes (not `unpublished`); no ADR-0087 id covers it (not `registered` / `already-registered`); and the change is rule behaviour, not a TypeScript declaration (not `runtime-interface-only` / `type-surface-only`). -->
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+
Every example predicate in `packages/spec/src/data/validation.zod.ts` now reads fields through the `record.` root and describes the violation, so an author or agent who copies one gets a rule that evaluates and fires on the records it names as invalid. The fix covers the three `cross_field` "Salesforce Examples" in the `CrossFieldValidationSchema` TSDoc, the header `script` example, and the `.describe()` example on `CrossFieldValidationSchema.condition` (#20252).
6+
7+
Clause-②: no
8+
9+
- Example 1, "Close Date Must Be In Current or Future Month": `MONTH(close_date) >= MONTH(TODAY()) AND YEAR(close_date) >= YEAR(TODAY())` becomes `date(record.close_date) < addDays(today(), 1 - today().getDate())`. The old string did not parse (`AND` is not CEL, and `close_date` had no `record.` root). It was also inverted: a `cross_field` condition that evaluates TRUE is the violation, and the old condition was TRUE on the records the rule should accept. The new condition is TRUE when the close date falls before the first day of the current month. It uses only the formula stdlib's `date()`, `today()` and `addDays()` plus CEL's built-in `getDate()` timestamp accessor.
10+
- Example 2, "Discount Validation": `discount > (amount * 0.40)` becomes `record.discount > (record.amount * 0.40)`. The bare fields were unknown variables. The direction is unchanged.
11+
- Example 3, "Opportunity Must Have Products": `products = null AND stage = "closed_won"` becomes `isBlank(record.products) && record.stage == "closed_won"`. A lone `=` is a CEL parse error and `AND` is not CEL. The direction is unchanged.
12+
- The header `script` example: `discount_percent > 0.40` becomes `record.discount_percent > 0.40`. The bare field was an unknown variable. The direction is unchanged.
13+
- The `CrossFieldValidationSchema.condition` description: its example `record.end_date > record.start_date` refused every valid end-after-start range, because a TRUE condition is the violation. It becomes `record.end_date < record.start_date`, and the description now says that a TRUE condition fails validation.
14+
15+
The Salesforce-formula side of each example is unchanged. This changes documentation text only (TSDoc and one `.describe()` string, with the generated reference page regenerated to match). There is no schema, behaviour or export change.

‎.changeset/rls-check-defaults-to-using.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Clause-②: no (narrowing)
1010

1111
**BREAKING**: this narrows the set of writes the write gate accepts. A write that is admitted today can be refused after this change. It ships as `minor` under the launch-window convention, the same way the insert-side `check` reorder did (#16805).
1212

13-
The published contract has always said this. `RowLevelSecurityPolicySchema.check` reads "defaults to USING clause if not specified", and PostgreSQL treats a policy without `WITH CHECK` the same way. The write gate did not do it. It compiled only the policies that declared `check`, so a policy with only a `using` never checked a write. With `using: "record.status != 'closed'"`, a caller could INSERT a closed row. The row was stored even though the same caller could not read it afterwards.
13+
The published contract has always said this. `RowLevelSecurityPolicySchema.check` read "defaults to USING clause if not specified" (it now states the default per operation across the applicable policies, #19953), and PostgreSQL treats a policy without `WITH CHECK` the same way. The write gate did not do it. It compiled only the policies that declared `check`, so a policy with only a `using` never checked a write. With `using: "record.status != 'closed'"`, a caller could INSERT a closed row. The row was stored even though the same caller could not read it afterwards.
1414

1515
**Writes that are now refused.** Each refusal is the existing row-level CHECK denial, `403 PERMISSION_DENIED`, and nothing is stored. There is no transition switch.
1616

‎.claude/skills/pm-dispatch/references/contract-review.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414
- 同形 = `## Contract review` 题头、所审 head sha 独占码段、①②③ 逐项、独立性对、PASS/FAIL。
1515
- 同形含首行 `Served-tier:`:值写常量名 `CONTRACT_REVIEW_TIER`;无此行不成裁决,
1616
模板见 `node scripts/pm/record-recognisers.mjs --template`。
17+
- 同形另带 `Local-runs:` 行:`none`,或 `probe — REASON` 具名唯一本地探针;
18+
有行必校,缺行旧记录不改判。
1719
- ① derived judgments 逐项:diff 引出的接受集与公开面变化逐条点名判对错。
1820
- ② semver 定级与 changeset 声明一致;③ 边界旗:dev 挂旗与 `open_questions` 逐旗答复或升级。
1921
- 复核形状:只读 diff 与卡片,check 结论取 head 的 check-runs,⛔ 永不本地重跑派生门禁族。
2022
- 只喂卡片、既有裁决与 PR 本体,⛔ 不喂派发令与派发席自己的结论;简报写成对抗性。
23+
- 简报从 `node scripts/pm/record-recognisers.mjs --brief-template` 起稿:
24+
只读条款与输入集内建,⛔ 不删 ⛔ 行。
2125
- 隔离复核子代理暂存全写按所审 PR 命名的 `<scratchpad>/pr-<n>/`,⛔ 不读非本轮自写的暂存。
2226
- 独立性对:`Implemented-by:`/`Reviewed-by:` 取值见
2327
`node scripts/pm/record-recognisers.mjs --template`;子代理记采纳它的席位。

‎.claude/skills/pm-dispatch/references/execution-duties.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@
104104
- 豁免够不到的地板只有维护者裁决能设。
105105
- 席位档策略:skills 车道外的执行席与分诊席默认判断档会话。
106106
- 条款②自审资格按车道:spec 与 skills 席达档席内审;未达档 ⛔ 不自审,起隔离达档子代理。
107-
- 降档出口两条:额度耗尽豁免,与主动预降(余量吃紧可预先降档)。
108-
- 额度耗尽豁免仅当契约复审档实测不可用才落默认判断档,⛔ 不再往下。
107+
- 降档出口两条:额度耗尽豁免(达档实测不可用才落默认判断档,⛔ 不再往下)与主动预降。
108+
- 额度墙只向下(降档或降并发)⛔ 永不向上;临时偏离记座位贴带结束条件,逐次巡检重读。
109109
- 降档的档位与理由记入认领评论 Container & model 行;档位逐次派发显式传参,永不省略。
110110
- 解析顺序、pin 语义与允许名单回退陷阱以 os-dev 定义 frontmatter 注释为权威。
111111
- 分诊 suggestion 行是输入不是决定,不采纳给理由。

‎content/docs/permissions/authorization.mdx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,14 @@ implementation detail:
107107
Keys never collide across packages because object api names are
108108
package-namespaced.
109109
3. **RLS: OR within an object, AND with tenant-global.** Multiple row policies
110-
for the same object/operation OR-combine; the tenant wall (Layer 0, ADR-0095
111-
D1) is a separate always-first AND conjunct, not an OR-mergeable policy.
110+
for the same object/operation OR-combine their `using` on reads and on the
111+
rows a write may target. The `check` on the rows an insert or update writes
112+
is chosen per operation first: when any applicable policy declares `check`,
113+
only the declared checks decide (OR-combined) and a USING-only policy adds
114+
nothing; otherwise each `using` stands in (OR-combined) — see
115+
[the fail-closed contract](/docs/permissions/rls#the-fail-closed-contract).
116+
The tenant wall (Layer 0, ADR-0095 D1) is a separate always-first AND
117+
conjunct, not an OR-mergeable policy.
112118
`viewAllRecords` / `modifyAllRecords` (super-user bypass, posture-gated)
113119
short-circuit the object's *business* RLS. Crossing the **tenant wall**,
114120
though, requires the **`PLATFORM_ADMIN` posture** (ADR-0099 D1). That posture

0 commit comments

Comments
 (0)