Skip to content

Commit bbeeb31

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15556-subflow-bubble-swallow
2 parents 853eefc + 99a5bc6 commit bbeeb31

113 files changed

Lines changed: 6063 additions & 1280 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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: an evaluated expression slot requires a non-blank `source``EvaluatedExpressionSchema`, composed by the `assignment` value envelope (#15430)
6+
7+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key is renamed, retired or re-typed: `source` keeps its name and meaning, and every envelope that carried a non-blank `source` parses byte-identically. The two newly refused spellings — an envelope carrying only `ast`, and a `source` that is blank after trimming — never evaluated on any release (the `ast`-only one faulted at run time with the engine's own "persist `source`" prescription, the blank one with a parse error), and a repo-wide census found no in-repo instance of either, so `objectstack migrate meta` has nothing to rewrite and the remedy is authoring a `source`, which the refusal itself prescribes. -->
8+
9+
**BREAKING** in the accept-set sense, landing in the launch window as `minor`
10+
(the lockstep convention): on the schemas that type an EVALUATED expression
11+
slot — today the `assignment` node's value envelope,
12+
`AssignmentExpressionValueSchema` — an envelope with no `source` the engine can
13+
evaluate is now **refused at authoring**, where it used to parse, register,
14+
pass `objectstack validate`, and then fault at run time.
15+
16+
Two spellings of one seam, refused by ONE rule with one message at `source`
17+
(`EVALUATED_EXPRESSION_SOURCE_REQUIRED`):
18+
19+
```yaml
20+
assignments:
21+
digest: { dialect: cel, ast: { kind: const } } # `ast` only — no engine evaluates it
22+
greeting: { dialect: cel, source: ' ' } # blank after trimming — parses to EOF
23+
```
24+
25+
> An expression in an evaluated slot needs a non-blank `source`: the expression
26+
> engine evaluates `source` (the canonical persisted form of phase M9.1) and
27+
> cannot evaluate `ast` alone, so an envelope carrying only `ast`, or a `source`
28+
> that is blank after trimming, would validate and register and then fault at
29+
> run time. Write `{ dialect: 'cel', source: '…' }`.
30+
31+
- **`ExpressionSchema` is NOT narrowed.** It is the persistence contract —
32+
`source` OR `ast` — and its docblock declares that `ast` becomes required in
33+
build output at phase M9.2. The new export `EvaluatedExpressionSchema` (and
34+
its type `EvaluatedExpression`) is a sibling: the same envelope with `source`
35+
required and non-blank, spelled once and composed by every evaluated slot, so
36+
when AST-only evaluation lands the flip is one edit there rather than a
37+
per-slot unwinding. The rule is worded as "an evaluated slot requires whatever
38+
the engine can actually evaluate"; what that is today is `source`.
39+
- **The notion of blank is the engine's own** — `.trim()`, which
40+
`cel-engine.ts`'s helpers already apply — not a third one beside the shape
41+
rule's `min(1)` and `validateExpression`'s trim.
42+
- **Three doors agree.** `registerFlow` refuses the flow, `objectstack validate`
43+
and the runtime publish gate report a located `error` at the author's own
44+
variable (`config.assignments.<name>.source`), and the executor's own shape
45+
pass refuses the same set — all through the spec schema, so none of them
46+
grew a rule of its own.
47+
48+
**What an author does with a refused envelope.** An assignment value that
49+
carried only `ast` has no evaluable form under M9.1: author its `source`. A
50+
whitespace-only `source` was never an expression: delete the entry, or write
51+
the expression. Every envelope with a non-blank `source` is unchanged, and
52+
nothing is renamed, retired or rewritten — the refusal itself carries the
53+
prescription.
54+
55+
Not touched here: the `predicate` half of the same seam — `evaluateCondition`'s
56+
silent `false` on an envelope without a `source` — is a behaviour change on a
57+
live path with its own card, and the edge-condition schema that carries that
58+
envelope is narrowed in a follow-up once the in-flight change to
59+
`automation/flow.zod.ts` lands.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
A self-registration grant is refused, not silently redirected, when a permission-set row is malformed — and the fourteen dead `{ records }` / `{ data }` normalizer limbs behind that code are gone.
6+
7+
`plugin-auth` carried fourteen array-or-envelope normalizer blocks of the shape `Array.isArray(x) ? x : x.records ?? []` (thirteen on a `records` limb, one on a `data` limb, four of them written as a guard clause rather than a ternary). All fourteen read the same concrete engine — the `ObjectQL` instance the kernel registers as the `objectql` / `data` service — which answers a bare array on every path, populated or empty. The envelope limb was unreachable code that read as a contract, so the next author writing a defensive normalizer here believed an envelope was possible. The limbs are removed, and the three local engine ports that declared `Promise<unknown>` (`BootProbeEngine`, `DevAdminSeedProbeEngine`, `PhoneSmsTemplateEngine`) now declare the array they always returned.
8+
9+
The user-visible change is in `settleSelfRegistrationGrant`, which carried the opposite defect. Its candidate filter dropped any permission-set row whose `id` was missing or blank, silently, before choosing which row to grant:
10+
11+
- When the malformed row was the only one, the operator was told `no active sys_permission_set row named 'X' resolves` — false, since an active row named exactly that was present. That report is the only signal this path emits, and nothing retries it.
12+
- When the malformed row was the **organization-scoped** one and a global row also carried the declared name, dropping it let the `organization_id == null` arm match instead, and the self-registrant was granted the **global** permission set their organization never declared — with a success log and no other trace.
13+
14+
`active !== false` remains a selection predicate: a deactivated set still reports the ordinary "does not resolve". A malformed row is no longer a selection at all — the grant is refused and the report names the malformed row, so the ambiguity is surfaced instead of resolved by accident. A well-formed family grants exactly as before.
15+
16+
**Upgrade note — one family now gets a refusal where it previously got a grant.** If a deployment's `sys_permission_set` already contains a row that is active and carries the declared name but whose `id` is missing or blank, self-registration grants against that name now stop and report, including the case where the malformed row is one nobody was relying on: a malformed **global** row sitting alongside a well-formed **organization-scoped** row used to be dropped silently, letting the org row be granted, and is now refused. This is deliberate — the old behaviour could not tell that family apart from the one where the silent drop granted the *wrong* set — and it is fully reversible without a code change: repair or delete the malformed row and the grant proceeds exactly as before. The refusal is loud and names the row, so it is visible rather than something to discover later; nothing is written while it stands.

.changeset/tidy-cups-smile.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/objectql': minor
3+
'@objectstack/metadata-protocol': minor
4+
'@objectstack/service-automation': patch
5+
'@objectstack/lint': patch
6+
'@objectstack/spec': patch
7+
'@objectstack/service-settings': patch
8+
---
9+
10+
**BREAKING (behaviour):** a static `readonly` field is now stripped from a **non-system caller's INSERT payload inside `engine.insert`**, exactly as it already was on `engine.update`. A non-system create that used to write a read-only column now has that column dropped, reported through `onFieldsDropped` / `droppedFields`, logged at `warn`, and refused outright under `strictReadonlyWrites`. Seeding a read-only column at create time is a **system** act — use `context.isSystem`, a flow's `runAs: 'system'`, a system hook or a seed.
11+
12+
Until now the create-side strip lived only at the DataProtocol ingress (`stripReadonlyForInsert` in `@objectstack/metadata-protocol`), so `readonly` meant one thing on insert and another on update: every external REST/GraphQL/MCP create was stripped, while a caller reaching `engine.insert` directly — the automation engine's `create_record` among them — wrote the column with no refusal, no `WARN` and no dropped-field event.
13+
14+
- `stripReadonlyForInsert` and its five call sites in `@objectstack/metadata-protocol` are **deleted**, not kept as a second implementation; every create face — `createData`, `cloneData`, `createManyData`, `insertManyData`, and `batchData`'s `create` rows and both arms of `upsert` that create — now hands the caller's payload to the engine whole, and every face whose response carries `droppedFields` (`createData`, `createManyData`, `insertManyData`, every `batchData` row that created) reports the engine's own verdict there, so `droppedFields` says the same thing at each of those seams. `cloneData` forwards whole but reports nothing on the wire: its response contract (`CloneDataResponseSchema`, declared as produced) has no `droppedFields` member, so a clone that carried or overrode a read-only column is stripped and logged at `warn` but not reported in the 201 body — adding that key is a spec change, not part of this one.
15+
- `create_record` (`@objectstack/service-automation`) starts receiving readonly drops on the `onFieldsDropped` channel it has been wired for since #3407 — a flow without `runAs: 'system'` that seeds a read-only column now reports a node warning and `output.droppedFields` instead of a clean success. That package's own code changes only in prose; the traffic is new, the surface is not.
16+
- Unchanged, deliberately: `isSystem` is still the exemption; `preserveAudit` is still an UPDATE-path exemption and a create that asks for it is told so out loud; runtime-owned types (`autonumber`) keep their own pass and their own wider whitelist; platform objects (`managedBy`, the `sys_` namespace) are still left to their own field-write guards; `readonlyWhen` still has no create-side strip. A stripped key's `defaultValue` is re-derived, so a forged `approval_status` becomes `draft` rather than NULL.
17+
- `@objectstack/service-settings` is `patch`: prose only — the `upsertRow` docblock, which ships in the package's `.d.ts`, no longer states the superseded INSERT exemption; it names the platform-object carve-out that actually keeps a `sys_setting` insert outside the strip.
18+
- `@objectstack/lint` and `@objectstack/spec` are `patch`: both change prose only. All three lint rules — `validate-readonly-action-writes`, `validate-readonly-flow-writes`, `validate-readonly-hook-writes` — drop the superseded "INSERT is exempt" premise from their docblocks and from the justification of their green control cases; the two non-elevated rules now name their `insert`/`create` silence as a scan gap rather than an exemption (the action rule additionally records its now-reasoned refusal as a module-local constant that its `index` does not re-export, so no public surface widens). The spec change is prose only: one docblock sentence that named the deleted function, the `strictReadonlyWrites` contract docblock (which now states what strict refuses on insert), and the `readonly` liveness-ledger verdict, whose evidence pointer named the deleted ingress strip.
19+
20+
<!-- adr-0087: not-required (no-migration-prescription) The BREAKING here is a WRITE-PATH BEHAVIOUR change, not a retirement of an authorable or published surface, so there is nothing for the ledger to carry to `objectstack migrate meta`, `spec-changes.json` or the upgrade guide: no spec property, metadata key, accepted value or exported symbol disappears, and this body prescribes no FROM/TO migration. The remedy for an affected caller is to declare the write trusted (`context.isSystem` / `runAs: 'system'`), which is application code, not a metadata migration. The obvious retirement candidate is a non-question in the same direction: `stripReadonlyForInsert` was a bare module-private `function` in `packages/metadata-protocol/src/protocol.ts`, absent from that package's `index.ts` (the only path its `exports` map offers), so no consumer could name it. -->
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
Flow templates: `{TODAY() + n}` and `{TODAY() - n}` now do their day arithmetic on the same calendar they render on (UTC), so the resolved date no longer lands a day off across a DST transition.
6+
7+
The offset branch of the template resolver shifted the day on the **local** calendar (`getDate` / `setDate`) and then rendered the result on the **UTC** one (`toISOString`). `setDate` preserves wall-clock time, so a local day shift moves the underlying instant by exactly n x 24 hours only while every local day in the window is 24 hours long. Across a spring-forward the window is 23 hours and across a fall-back 25, and when that one hour of slack crosses a UTC midnight the rendered date comes out a day early (spring-forward) or a day late (fall-back).
8+
9+
The window is narrow — roughly one hour per DST-observing zone, twice a year — but the values written through it persist: a quote expiration, a follow-up date, a close date. Measured across 34 zones at every 30 minutes of 2026 for offsets `+1` and `-1` (1,191,360 instant-offset pairs), the old spelling disagreed with the UTC day in 190 of them, spread over 24 DST-observing zones; the new spelling disagrees in none.
10+
11+
The same branch serves `{NOW() + n}`, which likewise now moves the instant by exactly n x 24 hours instead of preserving a wall-clock time across the transition.
12+
13+
Nothing else moves. The bare `{TODAY()}` and `{NOW()}` forms never entered this branch and are byte-for-byte unchanged — they already resolved on UTC, and the offset forms now agree with them. This is not a timezone feature: these tokens remain timezone-unaware by design, and whether they should be is a separate question.

.claude/skills/dogfood-verification/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,9 @@ dev 工作树、dev-server 端口、preview 浏览器全是**共享的**:并行
132132
`git -C /tmp/pr apply /tmp/fix.patch` → 确认 `git -C /tmp/pr status` **列你的文件。
133133
- [ ]**changeset**(`.changeset/<slug>.md`,`"@objectstack/<pkg>": patch`)——
134134
发布包的改动过不了 CI 的「Check Changeset」门。
135-
- [ ] 提交(信息末尾带 `Co-Authored-By:` trailer)、推送、`gh pr create`,然后
136-
`gh pr merge --squash --auto --delete-branch`(先移除 worktree,本地分支才不被
137-
锁:`git worktree remove /tmp/pr --force`)
135+
- [ ] 提交(信息末尾带 `Co-Authored-By:` trailer)、推送、**draft** PR(dev 容器走 REST 端点;
136+
人手上 `gh pr create --draft`),PR 报给派发你的席位;ready / 入队 / 人工合并归该席位。
137+
- [ ] `git worktree remove /tmp/pr`:⛔ 不带 `--force`,被拒说明有东西没提交,先读 `git status`
138138

139139
## 6. Shell 卫生
140140

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
104104

105105
| issue 上的信号 | 含义 |
106106
|---|---|
107-
| open + 队列标签 + 无 assignee | 可派发(同卡带 `pm:retriage` 的除外) |
108-
| assignee 已设 | 已认领/在飞,不是你的就永不碰 |
109-
| `pm:dispatched` | 已派发(派发评论记轮次);与摘 `pm:queue` 同一次标签写入成对落地 |
107+
| open + 队列标签 + 无 assignee | 可派发(同卡带 `pm:retriage` 的除外);`pm:queue` 卡恒无 assignee,有即半态 |
108+
| assignee 已设 | 已认领/在飞,不是你的就永不碰;离手恒走释放 —— 四因 = 改路由、前提证伪、弃飞无接管、跨车道移交,去向 = 新标签态或车道;⛔ 不静默摘 assignee |
109+
| `pm:dispatched` | 已派发(派发评论记轮次),恒带 assignee;与摘 `pm:queue` 同一次标签写入成对落地 |
110110
| `needs-user-decision` | 决定待做:永不派发、除代裁通道外永不代答;维护者的收件箱 |
111111
| `pm:on-hold` | 决定已做且答案是暂不做:不派发不催;仅当带机器可读 `Restart-when:` 行才合法 |
112112
| `pm:blocked` + 正文行 `Blocked-by: #N` | 等上游:选择期跳过,#N 关闭时由解锁扫描放回;工已完、PR 被外部门禁卡住的同用本态 |
@@ -462,8 +462,8 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
462462

463463
- 同账号多会话共享 GitHub 身份:assignee 只回答有 agent 认领了,认领评论承载身份。
464464
- assignee 字段归 PM:原子对 step 1 设,dev 席恒不写它;跨账号 assignee 不是你 ⇒ 永不碰。
465-
- 选中的单派发前按序执行一个原子对
466-
- ① Assign @me,并把 `pm:dispatched` 与摘 `pm:queue` 放进同一次标签写入。
465+
- 释放是显式动作:让卡离手者同笔清 assignee + `Release:` 行(会话/因/去向);下一任重新认领
466+
- 派发前按序执行原子对:① Assign @me,并把 `pm:dispatched` 与摘 `pm:queue` 放进同一次标签写入。
467467
- step ① 之后获得 assignee 的直接弃出本批。
468468
- ② 认领评论(英文),固定形状见 模板与表 节;首行以字面 `Claim:` 开头是机器判据。
469469
- 巡查谓词只认 `Claim:` 这一个拼写且保持严格,修法是全舰队向它收敛,⛔ 不放宽谓词。
@@ -490,7 +490,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
490490
- 过栏后,派发 worktree 的未提交改动先 WIP commit 到派发分支并 push,sha 记进回收评论。
491491
- WIP commit 标 INCOMPLETE AND UNREVIEWED;续派者 diff 它,⛔ 不无审续建。
492492
- WIP 信息只写观察到的(脏路径/行数/sha),⛔ 不写席位行为的现在时断言。
493-
- 再评论询问,静默一窗后摘 assignee(注明原因)回队;有带提交活分支的认领永不回收。
493+
- 再评论询问,静默一窗后释放回队(`Release:` 行载因);有带提交活分支的认领永不回收。
494494
- 误伤活席位 ⇒ 令其追加式更正,落 PR 正文不落分支历史。
495495

496496
### 派发

.claude/skills/pm-dispatch/references/state-machine.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
## `pm:awaiting-maintainer``pm:retriage`
3636

3737
- awaiting 无机器出口:维护者完成该操作后,由项目总监席核验摘标 + 证据评论同笔。
38-
- awaiting 卡在在飞视图里计停放库存。
39-
- retriage 挂/摘分属两方:原定级在改判前仍是权威。
38+
- retriage 原定级在改判前仍是权威;awaiting 卡在在飞视图里计停放库存。
4039
- 挂标者 = 提出异议的席位,挂标与异议评论(证据 + 所求的答案)同笔;⛔ 裸挂标不合法。
4140
- 摘标者 = 分诊 Routine,每 fire 先答异议评论所求、答后同笔摘标;老化兜底归巡查 H18。
41+
- 释放者 = 让卡离手的席位:行首 `Release:`(会话/因/去向)与清 assignee 同笔;下一任重新认领。
4242
- 标签须五仓存在,首次应用时创建。

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Dependencies
22
node_modules/
3+
# also without the slash: in a linked worktree node_modules can be a symlink, which is not a directory to git and so escapes the line above; the repo tracks node_modules in no shape (#15763)
4+
node_modules
35
package-lock.json
46

57
# Build output

0 commit comments

Comments
 (0)