Skip to content

Commit 7c71a9e

Browse files
committed
Merge origin/main into claude/issue-22614-meta-view-container-read
Claude-Session: https://claude.ai/code/session_01HGa2XMKBRUqPrZFxQavBwH Co-authored-by: Claude <noreply@anthropic.com>
2 parents f63e6a1 + ee3ae03 commit 7c71a9e

69 files changed

Lines changed: 4233 additions & 7589 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: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
'@objectstack/lint': major
3+
'@objectstack/spec': major
4+
---
5+
6+
A flow CEL expression that reads a root the flow does not bind is refused at `objectstack validate`. The run fails such an expression with `Unknown variable: X` (X being the root) on every evaluation, and the build door used to pass it. The reachable case is the run user: `user.id == "u1"`, `ctx.user.id == "u1"` and `os.user.id == "u1"` — the spellings formulas, row-level security and the client accept — are refused, naming `current_user`, the one spelling flow CEL binds.
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: registered flow-cel-unbound-root-refused -->
11+
12+
**BREAKING**: an accept-set narrowing on a published authoring surface (`objectstack validate`'s `expression-invalid` rule).
13+
14+
**Why.** Flow CEL evaluates in one scope per run: the flow's variables spread to top level, the trigger record's fields flattened beside them, and `record`, `previous`, `vars` and `current_user` bound by the engine. Formulas, row-level security and the client also bind the run's user as `user`, `ctx.user` and `os.user`; flow CEL does not, because a top-level `user` would collide with a variable or a lookup field of that name. So an author who wrote the alias by habit shipped a flow that faulted at its first evaluation, and nothing warned.
15+
16+
**What is refused.** At every flow CEL site the build door reads — a node's `config.condition` (the start node's trigger gate included), an edge `condition`, a `decision` branch `expression`, a screen field `visibleWhen`, and the CEL value envelopes of an `assignment` and of a `create_record` / `update_record` `fields` map — a root that is none of:
17+
18+
- the engine's roots: `record`, `previous`, `vars`, `current_user`;
19+
- a name the flow binds: a declared variable, an `outputVariable`, an `iteratorVariable` (a `loop`'s default `item` included), an `indexVariable`, an `errorVariable`, an `assignment` target, a node id, a screen field's `name`;
20+
- a field (or a registry-injected column) of any object the stack declares — the run flattens whichever record its entrance hands it (an object-less flow action hands only the row `id`).
21+
22+
The user spellings are told to write `current_user`, with the guard for a flow that can run without a user; any other root is named with what could have bound it, and the nearest in-scope name. The error is `expression-invalid`, `error`. Where the flattened-scope near-miss advisory ("`X` is not a field of … If `X` is a flow variable this is safe to ignore") named the same root, the refusal replaces it.
23+
24+
**Not judged — the door stands down where the run can bind a name it cannot see.**
25+
26+
- **A node:** a `script` or `connector_action` node (each hands its code the live variable map), a `wait`, `subflow` or `map` node (a resume folds the caller's `variables` bag in under their plain names), a screen with no field list, or a node type outside the builtin set.
27+
- **An entrance the stack declares that hands the flow a record whose keys are not in hand:**
28+
- a record trigger (`config.objectName`) or a time-relative sweep (`config.timeRelative.object`) on an object the stack does not declare;
29+
- an `api` trigger, whose inbound hook hands the request body in as the record;
30+
- a `type: 'flow'` action launching the flow on an undeclared object;
31+
- a `map` node feeding the flow whose `itemObject` is undeclared or absent;
32+
- a `subflow` or `map` parent that is itself such a flow.
33+
- **The runtime publish gate:** a flow write there is not judged for unbound roots yet. Its per-write snapshot carries no actions or other flows, so it cannot see those entrances. Every other expression verdict at that door is unchanged.
34+
35+
A `$` name has no CEL spelling (`$runId == "x"` does not parse), so the `$` roots stay the text-slot rule's.
36+
37+
## FROM → TO
38+
39+
| you wrote | write instead |
40+
|:--|:--|
41+
| `condition: 'user.id == "u1"'` | `condition: 'current_user.id == "u1"'` (`current_user != null && current_user.id == "u1"` where the flow can run without a user) |
42+
| `condition: 'ctx.user.id == record.owner'` | `condition: 'current_user.id == record.owner'` |
43+
| `assignments: { who: { dialect: 'cel', source: 'os.user.id' } }` | `assignments: { who: { dialect: 'cel', source: 'current_user.id' } }` |
44+
| `condition: 'deals.size() > 0'` with nothing binding `deals` | declare it (`variables: [{ name: 'deals', type: 'list' }]`) or bind it with a node (`outputVariable: 'deals'`) |
45+
46+
**The one-line fix: write `current_user` for the run user, and bind or correct every other root the refusal names.**
47+
48+
**Who is affected, measured.** The last published spec line, 17.x, judged a flow expression's syntax only, so it accepts every refused source. Measured with the judge over every flow CEL site: the example apps (`app-crm`, `app-todo`, `app-multi-package`, `app-showcase`: 35 flows, 45 sites), `packages/platform-objects` (no flows) and `objectstack-ai/hotcrm` at `f0afcbda07` (32 flows, 65 sites) — 0 refusals; every root those flows read is an engine root, a binding of the flow's own or a field of its trigger object. One flow of the 67 is left unjudged by an entrance (`showcase_inbound_task_webhook`, an `api` trigger). Deployed metadata and other repositories were not measured.
49+
50+
### The kit
51+
52+
- **The judge.** `flow-cel-root-scope.ts` in `@objectstack/lint`, called by `validateStackExpressions` at each flow CEL site. One bound set per flow, read once like the shadowing pass's variable set, and one entrance index per stack (`flowCelEntrances`).
53+
- **The ledger.** The D3 semantic entry `flow-cel-unbound-root-refused` (protocol 18). There is no D2 conversion: none of these roots ever evaluated in a flow, so there is no behaviour to carry over.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os migrate` reads the project's `.env` files as `os serve` does, and names the database it opened and who named it (#22581)
6+
7+
`os serve`, `os start` and `os dev` load the project's `.env*` files before they read any variable. `os migrate` read only the process environment. So a project keeping `OS_DATABASE_URL` in `.env` served one database and migrated another: `os migrate plan` reported `.objectstack/data/objectstack.db` while `os serve` opened the `.env` database. An `OS_AUTH_SECRET` kept in `.env` left the auth family (`sys_user`, `sys_account`, …) out of the plan.
8+
9+
Every command that boots the one-shot schema stack now loads the same files first, through the same `dotenv-flow` call, with `os serve`'s mode rule: `NODE_ENV`, else `production`, and `development` for `os migrate security-catalog-overlays --dev`, which composes as `os serve --dev`. That covers every `os migrate` subcommand, and also `os meta resync`, `os secret rewrap`, `os secret orphans` and `os storage orphans`. A variable exported in the shell still wins over `.env`, as it does for `os serve`. The SQLite occupancy check that `os migrate apply` and the other write commands run before booting reads the same files, so it checks the database the command is about to migrate.
10+
11+
This supersedes two phrasings in this release's `os migrate plan` / `apply` auth-family entry: `os migrate` now does read the project's `.env` files, and the deployment's `OS_AUTH_SECRET` composes the auth family whether it is exported or kept in `.env`.
12+
13+
`os migrate plan` and `os migrate apply` print the database with its source: `--database-url`, a variable from the process environment, a variable from a named `.env` file, the config's default datasource, or the default. In `--json` the source is the new `databaseSource` field (`{ kind: 'flag' | 'process-env' | 'env-file' | 'config-datasource' | 'default', variable?, file?, datasource? }`). `apply` now carries `database` and `databaseSource` on every payload after the boot, including `in_sync`, `nothing_safe_to_apply` and `confirmation_required`.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/objectql': minor
3+
'@objectstack/spec': minor
4+
---
5+
6+
A file field whose `sys_file` read was refused to the reader reads as refused, not as "no file"
7+
8+
Clause-②: yes
9+
10+
The engine expands a stored file id (`file` / `image` / `avatar` / `video` / `audio`) by reading `sys_file` as the caller. A reader who may read the record but not `sys_file` (the default for any member whose application grants no `sys_file` read) is refused that read, and the field used to come back as the bare id: the same value an id with no committed file row produces. A refused file and an absent one were one answer, and the server logged a `warn` per read telling the operator to check storage availability.
11+
12+
- **From.** `{ "attachment": "f_7cQx2Lm90a" }` for a reader refused `sys_file`, plus `warn: sys_file lookup failed; file fields keep their raw ids and will render as "no file"`.
13+
- **To.** `{ "attachment": { "id": "f_7cQx2Lm90a", "metadataRefused": true } }`. Multi-value fields mark each id. Nothing else is served, because nothing else was read: no `name`, `size`, `mimeType` or `url`. A consumer that needs the download link derives the stable `/api/v1/storage/files/:fileId` endpoint from the id, as it does for a bare id; the download door judges access by the record that owns the file. The refusal is logged at `debug` only, because the reader is told in the value.
14+
- **Unchanged.** A reader allowed to read `sys_file` gets the hydrated `{ id, name, size, mimeType, url }`. An empty field reads empty. A missing `sys_file` table still leaves the bare id, silently. Any other read failure (connection, timeout, a database-level ACL fault such as `42501`) still leaves the bare id with the `warn`. Only the security layer's `PERMISSION_DENIED` refusal takes the new form.
15+
16+
`@objectstack/spec` declares the shape: `FileRefusedValueSchema` / `FileRefusedValue` (`{ id, metadataRefused: true }`, closed), and `valueSchemaFor(def, 'expanded')` for the file types admits it beside the bare id and `FileValueSchema`. It is a read form only. The stored form stays the bare id.
17+
18+
**For consumers.** Code that reads a file field's expanded value as `string | FileValue` should handle the third form: an object with `metadataRefused: true` and no `url` is a file this reader may not see the details of, not an empty field.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/service-analytics': minor
3+
---
4+
5+
fix(service-analytics)!: the analytics door judges the two generic-exit declarations every other door judges — an object's `enable` block and a field's `internal: true` (#22634)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, export or stored shape is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is a runtime read door: analytics stops serving objects and fields whose existing declarations already withhold them from every other generic exit, and the remedy is the declaration the author already wrote, never a rewrite of anyone's code or metadata. One optional hook, `getObjectDeclaration`, is added to the exported `AnalyticsServiceConfig` type; it is additive, and nothing on the type surface narrows. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and no exported declaration is removed or narrowed (not runtime-interface-only or type-surface-only). -->
10+
11+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
Every generic exit judges an object's `enable` block through the spec's one exposure decision, `apiExposureDenialReason`: the REST data routes, the runtime dispatcher, the MCP data tools, and the cross-object search for the off switch. Every generic exit also withholds a field declared `internal: true`: the data door omits it from rows, the write responses strip it, and the engine's `aggregate()` refuses it as a group key. The analytics door read neither declaration.
14+
15+
**FROM.** On both strategies (native SQL and ObjectQL), and for a member holding read and an administrator alike:
16+
17+
- The ad-hoc query, the SQL face, the configured-cube door and the dataset door served the rows of an object declaring `apiEnabled: false`, and of an object whose `apiMethods` whitelist does not grant `list`.
18+
- On the native-SQL strategy, a field declared `internal: true` was served as a group key, as an aggregate input and as a filter operand.
19+
- On the ObjectQL strategy, the engine refused the same field as a group key or an aggregate input with an undeclared `500`. It still admitted the field as a filter operand.
20+
21+
**TO.** Ahead of strategy selection, the door asks the spec's decision for the `aggregate` operation over every object a query reads: the base object, declared joins and relationship hops. It answers with the data door's codes for the same declaration:
22+
23+
- `404 OBJECT_API_DISABLED` for `apiEnabled: false`;
24+
- `405 OBJECT_API_METHOD_NOT_ALLOWED` for a whitelist that does not grant the verb.
25+
26+
On the ad-hoc doors this happens before any cube is inferred, so a refused request mints nothing. A member that reads a field declared `internal: true` is refused `400 INVALID_FIELD`, naming the object and the field. This holds in every position: dimension, measure input, filter or sort key, dataset filter, and relationship hop.
27+
28+
- **Refused, not withheld.** On the data door, the row path omits such a field because a row is a projection. Every analytics member is evaluated instead: a group key, an aggregate input, a predicate. There is nothing to omit, and the data door's own aggregate face already refuses the field as a group key.
29+
- **Every caller.** Neither declaration takes a user, so administrators are refused exactly as members are. There is no system carve-out.
30+
- **Fails closed.** A declaration lookup that throws refuses the query with `403 PERMISSION_DENIED`, logged at `error`. `AnalyticsServicePlugin` wires the lookup from the data engine and refuses when no engine can answer. A host constructing `AnalyticsService` by hand with no `getObjectDeclaration` hook gets no gate, and is told so once.
31+
- **Unchanged.** An ordinary object and an ordinary field are served as before. An object that merely has an `internal` field is still served for every other field, and so is a bare count over it.
32+
33+
**Measured producers.** On `origin/main` `f368b7e980`, no authored cube, dataset, dashboard or example in this repository names an `apiEnabled: false` object, an object whose whitelist omits `list`, or an `internal` field. The platform's System Overview datasets count `sys_user`, `sys_organization` and `sys_session`, and group `sys_audit_log` by two ordinary fields. Each of those objects grants `list`, and none of those members is `internal`. Deployed and cloud-held analytics definitions were NOT MEASURED.
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+
fix(spec): the error-code ledger records `@objectstack/service-analytics` as an emitter of `OBJECT_API_DISABLED` and `OBJECT_API_METHOD_NOT_ALLOWED` (#22634)
6+
7+
The analytics door now answers the data door's two exposure codes for an object whose `enable` block refuses the aggregate operation, so `ERROR_CODE_LEDGER['@objectstack/service-analytics']` lists both. That is the provenance `check:error-code-provenance` holds every emitting package to. Value-only: `REGISTERED_ERROR_CODES` is unchanged, because both codes were already registered under `@objectstack/rest`. No export, schema or generated artifact moves.

‎.claude/skills/pm-dispatch/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
266266
- 权重只排推荐不授权:读缩小特例与契约增生,⛔ 不背书投机扩张、不放行人工地板事。
267267
- 四维分析从业务角度写;四棱是四轴的卡面序列化,一一对应,也是分诊代裁置信门的输入。
268268
- 卡先于弹窗:需裁决先落 `needs-user-decision` 卡;收件箱由维护者定期消化,⛔ 不 assign 推送。
269-
- 决策卡带选项、推荐、证据与四棱块;被阻塞的执行卡同笔挂 `pm:blocked` + `Blocked-by:`。
269+
- 决策卡带选项、推荐、证据、四棱块与工作量列;受阻执行卡同笔挂 `pm:blocked`+`Blocked-by:`。
270270
- `AskUserQuestion` 只是在场加速器:仅当维护者在本会话 ~30 分钟内有过人类输入才可发。
271271
- 每问必带推荐项,被 Skip 或长挂即转卡通道,⛔ 不重弹。
272272
- 项目总监席:维护者在所在仓召唤 `/pm-dispatch director`;⛔ 无 Routine/cron;未回字 = 不在 = 停。

‎.claude/skills/pm-dispatch/references/decision-analysis.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,28 @@
2222
带 re-check 命令的前提 / 具体问题 / 选项 / 推荐 / 相关单与 PR。正文六项:
2323

2424
- 一句话问题:开头一句话说清冲突是什么,⛔ 不带实现名词,写客户看得见的症状。
25-
- 选项 × 真实代价表:每选项一行,做什么 + 客户可感知的后果;有实测的注明实测。
25+
- 选项 × 真实代价 × 工作量表:每选项一行,做什么 + 客户可感知后果 + 工作量;实测注明。
26+
- 工作量 = 车道 · 阶段/PR 数 · 实测先例(具名落地 PR 及 +/− 行数)或「无先例」;⛔ 不猜。
2627
- 业务含义直译:每选项配一句业务上等于什么,允许行业类比。
2728
- 四轴从业务立场论证(含四棱行):论据是客户、产品或事故后果,机制名词只作括号补充。
2829
- 防 AI 犯错轴写清出错时谁看到什么:响亮拒绝,还是静默泄露。
2930
- 推荐 + 回退 + 置信缺口:荐一项、给回退项、明说看不见什么;⛔ 不沿用他席推荐。
3031
- 推荐先只按①得出:终态句写两年后该能力的平台样子与主流平台建模,点名参照。
31-
- ②③④ 只调时序分期 ⛔ 不翻字母;契约迁就实现、分期半量 ⛔ 不作推荐。
32+
- ②③④ 与工作量只调时序分期 ⛔ 不翻字母;契约迁就实现、分期半量 ⛔ 不作推荐。
33+
- 工作量恒逐选项列;①读数相近而工作量差 ≥3× 推荐须点名;翻字母只凭维护者卡上另裁。
3234
- 自检行「只看①选 X;②③④ 是否翻转:否 / 是(理由)」,答是不改字母。
3335
- 裁后执行段:写明裁定之后怎么执行,让维护者只裁方向不背执行。
3436

3537
## 四棱卡面块固定形状
3638

3739
- 落卡即带,⛔ 不留待维护者到场再补;四棱各一行,用中文写。
38-
- 语言例外见 SKILL.md 〈全体座位的不变量〉。
3940
- 首行机器可寻标记 `os-decision-facets`:纯文本单行与其 HTML 注释形等价,任一即满足。
4041
- 写后回读,提取按字面 grep;⛔ 注释形读不到永不读作无四棱块。
4142
- ① 项目长远合理性:本方案缩小还是扩大特例与契约增生。
4243
- ② 实际业务拉动:今天谁撞上;零拉动默认 defer 或 remove。
4344
- ③ 防 AI 犯错:闭合枚举优于自由结构,响亮拒绝优于静默容忍。
4445
- ④ 创业阶段不扩散:remove 优于 declare-and-maintain,每个已声明的键都是永久义务。
4546
- 四棱后:`Prior rulings read: <terms> → <n> hits; <ADR Dn|none>; thread: <k>|none`;未搜成整行写 unresolved。
46-
- 末尾推荐行 + 字母选项带自检「只看①选 X;②③④ 是否翻转:…」,再一行置信缺口。
4747

4848
## 应用仓推荐序特例
4949

0 commit comments

Comments
 (0)