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

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 17, 2026 10:10am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation size/l labels Jul 17, 2026
@os-zhuang
os-zhuang merged commit 25a19be into mainJul 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/l

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang