Skip to content

docs: P1 wave 2 — RLS, translations, flow expressions/API, approval lifecycle#3113

Merged
os-zhuang merged 1 commit into
mainfrom
docs/p1-wave2
Jul 17, 2026
Merged

docs: P1 wave 2 — RLS, translations, flow expressions/API, approval lifecycle#3113
os-zhuang merged 1 commit into
mainfrom
docs/p1-wave2

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

背景

审计修复第三批(P1 wave 2),补齐四个功能点缺口。每页先派源码调研 agent(只收带 file:line 证据的事实),结果修正了审计自身的三处判断——这正是"文档不撒谎"该有的做法。

1. 新增 permissions/rls.mdx

  • 审计以为三处文档 operation 取值互相矛盾(select/all/update)。实际不矛盾:rls.zod.ts:111 枚举五值全合法,三处只是不同用例。故本页不"裁决",而是讲何时选哪个
  • 真正的地雷(全站零文档):RLS 表达式只有 4 种形式能编译(col == current_user.prop / col == 'literal' / col IN (current_user.array) / 1 = 1);AND/OR/比较符/IS NULL/LIKE/子查询/NOW() 全部拒绝,写错即 fail-closed 静默拒绝。补"用多条策略代替 AND/OR"的正解。
  • 另含:裸列名(无 record. 前缀,与 UI/flow 方言不同)、current_user.* 变量表(及为何不暴露 name——重名=越权)、叠加语义(Layer0 租户墙 AND Layer1;层内策略 OR)、四条 fail-closed 路径、"无适用策略=不限制"陷阱/security/explain 验证。

2. 新增 ui/translations.mdx

12 项 landing 能力里唯一指南层完全空白的一项。bundle 形态(locale map)、注册、可翻译面速查表、locale 解析(Accept-Language → ?locale= → default;zh-CNzhen→字面量,永不抛错)、三种文件组织、os i18n extract/check --strict --threshold 作 CI 门。诚实边界:validationMessages 无 runtime 消费者、ICU 未实现、文件 bundle(objects.)与运行时 translation 元数据(o.)两套 shape 未桥接(故建议以文件授权)。

3. automation/flows.mdx 两节 + 三处示例 bug

  • Expressions in flows:三方言确实并存且按位置决定——start/edge condition裸 CEL、decision expression 必须 {花括号}、字段值是插值。顺手修掉该页两个真 bug:expression: 'order_amount > 10000'(缺花括号)会退化成字符串比较恒真,永远走第一分支且不报错;due_date: 'TODAY() + 7'(缺花括号)把字面量文本写进日期字段。另 object:objectName:(canonical;别名会被发布门拒绝)。
  • Run a flow via API:补上该页此前只提及、从不给出的路径 POST /api/v1/automation/:name/trigger,含 runs/resume/screen、isInput 与 body 映射、身份转发与 runAs、以及 success:true + skipped:true(condition_not_met) 陷阱。

4. automation/approvals.mdx 生命周期 walkthrough

  • 审计以为待办队列缺失。实际存在:sys_approval_request 自带 my_pending 视图 + Setup→Approvals 导航 + GET /approvals/requests?approverId=&status=(approverId 可传 user id/email/role:x 多值)。
  • 真正的缺口:开启审批不发任何通知(reminder/escalation/return 有 topic,初始请求没有)——如实写明 + 两条出路(flow 加 notify 节点 / 装 messaging 用 remind)。
  • 另含:approve/reject 端点与权限判定(actor 必须在 pending_approvers)、**unanimous 下非最后一位 approve 不终结(仍 pending)**的易错点、type:'role' ≠ 职位(是 better-auth 层级,写 sales_manager 静默无人)、字段镜像与记录锁、escalation 四种 action 及其"需 job 服务否则 display-only"前置、6 条错误码映射。

验证

pnpm --filter @objectstack/docs build ✅(exit 0)。(types:check 首跑因 .source 生成顺序偶发失败,重跑通过——既有 flake,与本改动无关。)

🤖 Generated with Claude Code

…ifecycle

Four audit gaps closed, all grounded in source research. Three of them
corrected the audit's own assumptions — noted below.

New: permissions/rls.mdx
- The audit believed three pages contradicted each other on `operation`.
  They don't: 'select' | 'insert' | 'update' | 'delete' | 'all' are all
  valid. The real undocumented landmine is the grammar — only four forms
  compile (col == current_user.prop, col == literal, col IN (array),
  1 = 1); AND/OR/comparisons/IS NULL/LIKE/subqueries/NOW() are rejected,
  and a non-compiling policy denies everything.
- Also documents: bare column names (no record. prefix), the context
  variable set (and why current_user.name is withheld), Layer 0 AND
  Layer 1 with policies OR-ing inside a layer, the four fail-closed
  paths, "no applicable policy = no restriction" (the trap), and
  verification via /security/explain.

New: ui/translations.mdx — the one landing-promise capability with zero
guide coverage. Bundle shape, registration, translatable surfaces,
locale resolution (Accept-Language → ?locale= → default; zh-CN → zh → en
→ literal), file organization, os i18n extract/check as a CI gate, and
honest limits (validationMessages has no consumer, ICU unimplemented,
file vs runtime bundle shapes unbridged).

automation/flows.mdx
- New "Expressions in flows": the three dialects are real and differ by
  position — start/edge conditions are bare CEL, decision expressions
  need {braces}, field values are interpolation. Fixed two example bugs
  this exposed: `expression: 'order_amount > 10000'` (no braces) degrades
  to a string compare that is ALWAYS TRUE, and `due_date: 'TODAY() + 7'`
  writes the literal text. Also `object:` → `objectName:` (canonical;
  the alias is publish-gate rejected).
- New "Run a flow via API": the endpoint the page previously referenced
  without ever naming (POST /api/v1/automation/:name/trigger), plus
  runs/resume/screen, input binding via isInput, identity forwarding,
  and the success:true + skipped:true trap.

automation/approvals.mdx — end-to-end lifecycle Steps walkthrough. The
audit called the approver queue missing; it exists (sys_approval_request
my_pending view + Setup nav + GET /approvals/requests). The actual gap is
that opening a request notifies nobody — now stated with the two
workarounds. Adds approve/reject endpoints, the unanimous
not-finalized-yet trap, type:'role' ≠ position, field mirroring, real
escalation (with its job-service precondition), and the error table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 17, 2026 10:10am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation size/l labels Jul 17, 2026
@os-zhuang
os-zhuang merged commit 25a19be into main Jul 17, 2026
14 of 15 checks passed
@os-zhuang
os-zhuang deleted the docs/p1-wave2 branch July 17, 2026 10:10
xuyushun441-sys pushed a commit that referenced this pull request Jul 17, 2026
…g the ratchet

#3113's lifecycle walkthrough re-stated a warning the page already makes
better 80 lines up (§3 The approval node: position vs the membership tier,
including the `os lint` rule that flags it). The duplicate pushed the
reserved-word count (ADR-0090 D3) from 5 to 9, so check-role-word failed
on every PR cut from main since — #3121 tripped over it.

Removing the duplicate and pointing at the original brings the count back
to exactly the baselined 5: **no baseline change needed**. The walkthrough
keeps a one-line pointer that names the approver kinds and links to the
existing callout, which is what a reader needs at that point anyway.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 17, 2026
…g the ratchet (#3130)

#3113's lifecycle walkthrough re-stated a warning the page already makes
better 80 lines up (§3 The approval node: position vs the membership tier,
including the `os lint` rule that flags it). The duplicate pushed the
reserved-word count (ADR-0090 D3) from 5 to 9, so check-role-word failed
on every PR cut from main since — #3121 tripped over it.

Removing the duplicate and pointing at the original brings the count back
to exactly the baselined 5: **no baseline change needed**. The walkthrough
keeps a one-line pointer that names the approver kinds and links to the
existing callout, which is what a reader needs at that point anyway.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
xuyushun441-sys pushed a commit that referenced this pull request Jul 17, 2026
The README opened with a category label ("A metadata protocol and
TypeScript toolkit") and spent its first 100 lines on architecture layers
and protocol theory — none of it the point a visitor is here for. It now
mirrors the landing page (objectstack.ai, #3090) and tells the story the
project is actually about:

- hero: "AI writes the app. ObjectStack is what it writes." + the
  correctness chain (typed → validated → reviewed → governed)
- The loop: create → describe to Claude Code → preview in the browser;
  requirement changes run the same loop
- What can it actually build? — the 12 metadata capabilities, framed
  against the alternative (an agent on an empty repo writes a one-off
  codebase; here it composes from a vocabulary the runtime already runs)
- Why the mistakes don't ship — the four gates, why TypeScript was the
  right host language (errors become corrective text the agent fixes
  itself), and the context-window argument
- Your app is AI-operable, for free — MCP in three lines
- This repo / architecture / ObjectOS cross-link, compressed to the tail

Reference sections (packages, CLI, scripts) are kept as-is. Drops the
stale v4.0.1 version badge (CLI is 15.x — the badge had drifted three
majors). Quick Start's duplicate scaffold snippet folds into the loop.

Also fixes a third missing-braces decision expression in flows.mdx:155
(same class as the two fixed in #3113 — without braces the comparison
degrades to a string compare that never matches as intended).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 17, 2026
The README opened with a category label ("A metadata protocol and
TypeScript toolkit") and spent its first 100 lines on architecture layers
and protocol theory — none of it the point a visitor is here for. It now
mirrors the landing page (objectstack.ai, #3090) and tells the story the
project is actually about:

- hero: "AI writes the app. ObjectStack is what it writes." + the
  correctness chain (typed → validated → reviewed → governed)
- The loop: create → describe to Claude Code → preview in the browser;
  requirement changes run the same loop
- What can it actually build? — the 12 metadata capabilities, framed
  against the alternative (an agent on an empty repo writes a one-off
  codebase; here it composes from a vocabulary the runtime already runs)
- Why the mistakes don't ship — the four gates, why TypeScript was the
  right host language (errors become corrective text the agent fixes
  itself), and the context-window argument
- Your app is AI-operable, for free — MCP in three lines
- This repo / architecture / ObjectOS cross-link, compressed to the tail

Reference sections (packages, CLI, scripts) are kept as-is. Drops the
stale v4.0.1 version badge (CLI is 15.x — the badge had drifted three
majors). Quick Start's duplicate scaffold snippet folds into the loop.

Also fixes a third missing-braces decision expression in flows.mdx:155
(same class as the two fixed in #3113 — without braces the comparison
degrades to a string compare that never matches as intended).

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
xuyushun441-sys pushed a commit that referenced this pull request Jul 17, 2026
…l` (#3133)

`ApproverType.role` was the last platform surface projecting the word
ADR-0090 D3 reserved. Renaming it also unbreaks `lint.yml` on main: the
role-word ratchet has been red since #3113 (approvals.mdx 5 → 9), and the
docs stop needing the word once the type is spelled correctly.

D3's exception does not cover this enum. It protects better-auth's own
`sys_member.role` COLUMN — third-party schema we cannot rename. `ApproverType`
is ours: an authoring surface, i.e. the *projection*, which D3 says is spelled
`org_membership_level` and labelled "organization membership", never "role".

The sentence licensing the leak is itself false. ADR-0090 D3:203 claims
`sys_member.role` is "already relabelled `org_membership_level` in the platform
projection (ADR-0057 D7)" — but `org_membership_level` appeared nowhere in the
codebase (one comment in position.zod.ts), and ADR-0057 D7:335 lists that
relabel under "Deferred (evidence-gated, P4)". The projection never landed, so
the word reached authors.

The name manufactured a silent failure ("hotcrm class"): every sibling surface
renamed to `position` (`sys_role`, `ShareRecipientType.role`, `ctx.roles[]`),
so `{ type: 'role', value: 'sales_manager' }` reads as a position's legacy
spelling. It resolves against the membership tier, finds no member row, falls
back to an inert `role:sales_manager` literal, and the request waits forever.
Repo-wide, `type: 'role'` had ZERO real callers — only lint tests and the docs
warning that exists to undo the confusion the name creates.

- spec: `ApproverType` gains `org_membership_level`; `role` kept as a
  deprecated alias for one window so a published 15.x flow keeps loading.
  `DEPRECATED_APPROVER_TYPES` + `canonicalApproverType()` are the single
  source for the mapping (runtime and lint both read it).
- plugin-approvals: resolves on the canonical type, warns on the deprecated
  spelling, `expandRoleUsers` → `expandMembershipTierUsers`. The `type:value`
  fallback literal deliberately keeps the AUTHORED spelling — 15.x wrote
  `role:<v>` into `sys_approval_approver` / `pending_approvers`, and
  canonicalising it here would orphan every stored slot.
- lint: `approval-role-not-membership-tier` →
  `approval-approver-not-membership-tier` (the rule id carried the word too),
  plus `approval-approver-type-deprecated`. Mutually exclusive: a bad VALUE
  wins, because prescribing `org_membership_level` for a position name is
  wrong advice — the fix there is `position`.
- docs/skill/reference + role-word baseline ratcheted DOWN (approvals.mdx
  5 → 1, automation SKILL.md 3 → 1); api-surface snapshot regenerated
  (0 breaking, 2 added).

Studio still offers "Role" and its picker calls `client.list('role')` on a
metadata type D3 deleted — that picker is already dead, and the dropdown is
objectui's own hardcoded copy of this enum. Tracked as objectui follow-up in
degrades to free text (strictly better than a picker that lists nothing).

Closes #3133. Supersedes #3120 and #3122, which only deleted the duplicated
docs callout without touching the name that causes the confusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
xuyushun441-sys pushed a commit that referenced this pull request Jul 17, 2026
…l` (#3133)

`ApproverType.role` was the last platform surface projecting the word
ADR-0090 D3 reserved. Renaming it also unbreaks `lint.yml` on main: the
role-word ratchet has been red since #3113 (approvals.mdx 5 → 9), and the
docs stop needing the word once the type is spelled correctly.

D3's exception does not cover this enum. It protects better-auth's own
`sys_member.role` COLUMN — third-party schema we cannot rename. `ApproverType`
is ours: an authoring surface, i.e. the *projection*, which D3 says is spelled
`org_membership_level` and labelled "organization membership", never "role".

The sentence licensing the leak is itself false. ADR-0090 D3:203 claims
`sys_member.role` is "already relabelled `org_membership_level` in the platform
projection (ADR-0057 D7)" — but `org_membership_level` appeared nowhere in the
codebase (one comment in position.zod.ts), and ADR-0057 D7:335 lists that
relabel under "Deferred (evidence-gated, P4)". The projection never landed, so
the word reached authors.

The name manufactured a silent failure ("hotcrm class"): every sibling surface
renamed to `position` (`sys_role`, `ShareRecipientType.role`, `ctx.roles[]`),
so `{ type: 'role', value: 'sales_manager' }` reads as a position's legacy
spelling. It resolves against the membership tier, finds no member row, falls
back to an inert `role:sales_manager` literal, and the request waits forever.
Repo-wide, `type: 'role'` had ZERO real callers — only lint tests and the docs
warning that exists to undo the confusion the name creates.

- spec: `ApproverType` gains `org_membership_level`; `role` kept as a
  deprecated alias for one window so a published 15.x flow keeps loading.
  `DEPRECATED_APPROVER_TYPES` + `canonicalApproverType()` are the single
  source for the mapping (runtime and lint both read it).
- plugin-approvals: resolves on the canonical type, warns on the deprecated
  spelling, `expandRoleUsers` → `expandMembershipTierUsers`. The `type:value`
  fallback literal deliberately keeps the AUTHORED spelling — 15.x wrote
  `role:<v>` into `sys_approval_approver` / `pending_approvers`, and
  canonicalising it here would orphan every stored slot.
- lint: `approval-role-not-membership-tier` →
  `approval-approver-not-membership-tier` (the rule id carried the word too),
  plus `approval-approver-type-deprecated`. Mutually exclusive: a bad VALUE
  wins, because prescribing `org_membership_level` for a position name is
  wrong advice — the fix there is `position`.
- docs/skill/reference + role-word baseline ratcheted DOWN (approvals.mdx
  5 → 1, automation SKILL.md 3 → 1); api-surface snapshot regenerated
  (0 breaking, 2 added).

Studio still offers "Role" and its picker calls `client.list('role')` on a
metadata type D3 deleted — that picker is already dead, and the dropdown is
objectui's own hardcoded copy of this enum. Tracked as objectui follow-up in
degrades to free text (strictly better than a picker that lists nothing).

Closes #3133. Supersedes #3120 and #3122, which only deleted the duplicated
docs callout without touching the name that causes the confusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant