Skip to content

Commit edf64d5

Browse files
committed
Merge origin/main (00d5494) into claude/issue-16077-default-value-literal-rename-preference
2 parents 414cc30 + 00d5494 commit edf64d5

30 files changed

Lines changed: 2597 additions & 64 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/service-automation": minor
3+
---
4+
5+
`evaluateCondition` now refuses a malformed condition shape with the same `STRUCTURAL_CONDITION_SHAPE_REFUSAL` registration already raises — evaluation and registration share one refusal, so a shape that slipped past registration can never surface as a raw `TypeError` or as a silent `false`.
6+
7+
#15662 closed the reject set at the producer: `registerFlow` refuses a structural condition (`config.condition` on a node, `edge.condition`) that is neither CEL text nor an expression envelope. The evaluator was left saying the opposite thing in a different vocabulary, and that half matters because `evaluateCondition` is a **public method on an exported class** — a plugin reaches it directly regardless of what `registerFlow` admits, and a flow stored before that gate landed replays through it.
8+
9+
The unguarded read had three arms, all of them now refused by the shared `structuralConditionRefusal` — the same call `registerFlow` makes, not a second hand-written envelope that could drift from it:
10+
11+
- an envelope whose `source` is present and **not a string** (`{ source: 1 }`, `{ dialect: 'cel', source: 1 }`) reached `.trim()` and threw `TypeError: exprStr.trim is not a function`, naming no flow, no node and no expression;
12+
- a value that is neither text nor envelope-shaped (`42`, `true`, `['a']`, `{}`, `{ dialect: 'cel' }`) was read as an **empty condition** and answered `false` — the "an unauthored branch must not open" rule applied to a value that was very much authored, on the same key a start node's **trigger gate** is read from;
13+
- a malformed envelope carrying a non-predicate dialect (`{ dialect: 'cron', source: 1 }`) answered `false` one statement earlier still, at the dialect check, never reaching the source derivation at all.
14+
15+
**What still evaluates is unchanged, and is pinned as controls.** Bare CEL text and both envelope spellings evaluate exactly as before; an `ast`-only envelope still answers `false`; a well-formed non-predicate dialect (`{ dialect: 'cron', source: '0 0 * * *' }`) still answers `false` rather than being refused; absent, `null`, empty and whitespace-only conditions are still "not authored", not malformed. A malformed **string** still earns its own verdict — the brace trap or the ADR-0032 §1c CEL fault — never the shape refusal.
16+
17+
An app whose stored flow carries one of the refused shapes in a node or edge `condition` now fails that run loudly with a message carrying the rule, instead of skipping a branch in silence or faulting unattributed; the fix is to write the condition as bare CEL text (`record.rating >= 4`) or as an expression envelope.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`nav-object-ungranted`'s hint no longer tells you to gate the nav entry with `requiredPermissions`/`visible` — that never cleared the finding, because the rule never reads either key. Gating restricts who can see the entry; it doesn't grant the object read, so a holder who clears the gate could still hit permission-denied, and the warning kept firing anyway. The hint (and the module doc-block) now name the two remedies that actually clear it: grant read on the object in a permission set (`allowRead: true` or `viewAllRecords`), or drop the nav entry. No behavior change — the rule fires and stays silent on exactly the same inputs as before; only the wording of the hint moved.

.changeset/olive-donkeys-repeat.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
feat(cli): point `@objectstack/cli/console` at a public barrel with a name-and-shape pin
6+
7+
**BREAKING**: `@objectstack/cli/console` publishes three names instead of thirteen. Ten names it used to resolve no longer resolve through that subpath.
8+
9+
The subpath pointed straight at `dist/utils/console.js` — an internal module — and carried no surface pin of any kind, neither names nor shapes. Two assertions did exist and neither is one: `./console` was held among the declared `exports` keys, and the specifier was held to resolving from the packed tarball. Both answer *is the door open*; neither can answer *what is behind it*. So all thirteen of that module's top-level exports were public API, and every export it gained afterwards became a permanent public contract the moment it landed, silently.
10+
11+
The subpath stays open and now points at a dedicated barrel, `dist/console.js`, which re-exports by name (no star) exactly the three helpers the one ledgered out-of-repo consumer uses to mount the Console SPA:
12+
13+
- `resolveConsolePath`
14+
- `hasConsoleDist`
15+
- `createConsoleStaticPlugin`
16+
17+
Those three keep their existing shapes exactly, so a consumer importing only them compiles unchanged.
18+
19+
These ten are no longer reachable through `@objectstack/cli/console`:
20+
21+
- `CONSOLE_PATH`
22+
- `ConsoleShaDrift`
23+
- `DRIFT_OVERRIDE_ENV`
24+
- `ResolveConsoleOptions`
25+
- `createRuntimeAssetsPlugin`
26+
- `decideConsoleMount`
27+
- `detectConsoleShaDrift`
28+
- `formatConsoleShaDriftRefusal`
29+
- `formatConsoleShaDriftWarning`
30+
- `isConsoleVersionCompatible`
31+
32+
Nothing was deleted. `utils/console.ts` still exports all thirteen and every in-package caller still imports it directly; what these ten lost is only the ability to be named through a published specifier. `ResolveConsoleOptions` in particular is still `resolveConsolePath`'s parameter type, so the options object a caller passes keeps working structurally — only the type's name is no longer importable from this subpath.
33+
34+
`decideConsoleMount` and `createRuntimeAssetsPlugin` were retired on a measurement rather than by default: every reference to either name in this repo is inside `packages/cli`, the consumer-specifier ledger names neither, and `decideConsoleMount`'s own docblock scopes it to `isDev` and states that no published install can reach the refusal it exists to produce.
35+
36+
`packages/cli/test/published-subpath-console.pin.test.ts` now holds the packed `.d.ts` to exactly the three names and their shapes, compiled by a real consumer outside the workspace, with a control per retired name. Re-admitting any of the ten is a deliberate, reviewed, `minor`-bumped edit to that barrel and that pin.
37+
38+
<!-- adr-0087: not-required (no-migration-prescription) The ADR-0087 ledger serves metadata upgraders: its entries are the data source for `objectstack migrate meta`, `spec-changes.json` and the generated upgrade guide. All ten names are ordinary TypeScript values and types on a published subpath with no metadata surface whatsoever — no Zod schema, no `packages/spec` declaration, no stored representation — so `objectstack migrate meta` has nothing to reach and no ledger entry could carry anything. There is also nothing to prescribe, and the consumer reading behind that is stated here at exactly the strength it was measured. In this repo: no importer of any of the ten outside `packages/cli` itself. In `objectui`: a real zero, re-derived at the pinned `.objectui-sha` — the specifier `cli/console` does not occur, and none of the ten occurs as an identifier except `CONSOLE_PATH`, twice, both inside comment prose and neither an import — against a positive control of 545 lines that do import from the `@objectstack/` scope, so the corpus is live and the zero is a reading rather than a silence. In `cloud`: NOT MEASURED, which is not the same thing as zero — the code-search index does not cover that repository from this seat, answering 0 hits with `incomplete_results: true`, and no checkout of it is reachable either. Every channel tried refused, and an unreachable repository never reads as "no consumers"; a refusal is not an absence. So for `cloud` the evidence stays second-hand by construction: the consumer-specifier ledger, which names exactly the three kept helpers, and the ruling behind this card, which reads it the same way. The channel that would actually reach a surprised consumer is the compiler (TS2305 naming the retired symbol at the import site), which is more precise than a ledger line. -->
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/trigger-record-change": patch
3+
"@objectstack/trigger-schedule": patch
4+
---
5+
6+
`@objectstack/trigger-record-change` and `@objectstack/trigger-schedule` now declare a `repository.directory` that resolves to the directory they actually live in.
7+
8+
Both manifests declared a path under `packages/plugins/` that no longer exists in the repository:
9+
10+
| package | declared | actual |
11+
|---|---|---|
12+
| `@objectstack/trigger-record-change` | `packages/plugins/plugin-trigger-record-change` | `packages/triggers/trigger-record-change` |
13+
| `@objectstack/trigger-schedule` | `packages/plugins/plugin-trigger-schedule` | `packages/triggers/trigger-schedule` |
14+
15+
`repository.directory` is what npm uses to build the **Repository** deep link on a package page, and what tooling uses to locate a monorepo package's source from its tarball. Pointing it at a path that does not exist sends a reader to a 404 instead of to the source — on packages published today at `17.3.0`. The value ships inside the tarball, so this correction only reaches npm by being published; that is why it carries a changeset rather than `skip-changeset`.
16+
17+
The residue came from a three-commit sequence on 2026-06-12, and only one of those commits was a pure rename. `f15d6f6f6` **copied** the two packages to `packages/plugins/trigger-*` (26 files, +2222/-19, with all four directories briefly coexisting) and edited exactly one line of each copied manifest — its `name` — leaving `directory` pointing at the path it was copied from; `290c62514` deleted the originals five minutes later; and `ea4941ad8` then promoted `packages/plugins/trigger-*` to a first-class `packages/triggers/` directory as a pure 16-file rename with zero content changes, which made the declared value wrong in a second segment. Six weeks after that, `9a43e042f` (#3380) rewrote `repository.url` and `bugs` in both of these manifests, with the stale `directory` line sitting as unchanged context one line below the edited `url`. So the field was not merely never in anyone's way: one commit edited its immediate neighbour inside the same object, and a later reviewed hunk had the wrong line on screen. Nothing caught it because nothing reads it.
18+
19+
Scope of this change, stated as a measured set rather than a general claim: over all **81** tracked `package.json` files in the repository, **57** declare `repository.directory`; before this change **55** resolved to the manifest's own directory and **2** did not — the two above. After it, **57 of 57** resolve. No other manifest field is edited, and no package's code, exports or behaviour is touched. The remaining **24** manifests declare no `repository.directory` at all; that population is deliberately left alone here and is reported separately, because whether declaring the field is mandatory is a policy question rather than a correction.

.claude/agents/os-dev.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ model: opus
3838
- PR 上不是你设置的状态属于另一个 actor:⛔ 永不去纠正;疑问进报告,挡住报 blocked。
3939
- 共享身份让所有人的写入都像你写的;被改写的 body 只是关于 body 的证据,不证明别的。
4040
- 回退他人的操作(尤其 ready 翻转)永不轮到你;把意外写进 `summary`
41-
3. **范围 = 这张 issue,别无其它。** 顺路撞见的无关缺陷立成新的无 assignee issue。
42-
- 列进 `out_of_scope_findings`;⛔ 永不在本 PR 里修它。
41+
3. **范围 = 这张 issue,别无其它。** 顺路发现 ⛔ 不在本 PR 修,只有三类立卡且不打标签:
42+
- (a) 可复现缺陷(复现或失败探针具名);(b) 违背已声明契约(引契约原文);
43+
- (c) 让 AI 写出运行时拒收或静默丢弃的元数据的陷阱;三类内 ⛔ 不因看着小揣着不报。
44+
- 其余 ⛔ 不立卡:观察、死代码、未演练漂移、抛光、风格、文档 nit、命名。
45+
- 它们进 PR `## 验收备注`,报告 `out_of_scope_findings``noted, not filed: …`,席位 ACCEPT 时读。
4346
- 先搜再立:关键词 + 文件路径扫 open issues;并行 dev 同一小时立的卡只有这一搜能看见。
4447
- 通道先探后选:同容器先测一条 repo-scoped REST 读;通 ⇒ 走 REST 列表端点 + 本地 grep。
4548
- 通道对照表见 `.claude/skills/pm-dispatch/references/rest-channel.md`,其 ✓ 按座位实测。
@@ -49,13 +52,10 @@ model: opus
4952
- 大宗读走零配额档:公开仓单卡网页内嵌 JSON payload 载原始 body + 全评论。
5053
- 其拼写与边界住 platform-readings;它只覆盖单卡读,⛔ 不拿它做 search。
5154
- 卡与评论先走 git 与 payload 档,MCP 留给写 + 那一次查重;报告记 MCP 调用计数(`mcp_calls`)。
52-
- 立不成 ⇒ 发现连同缘由写进报告交 PM 代立。
53-
- ⛔ 不查重硬立与静默弃报同为禁形:发现永不因通道断而消失。
55+
- 立不成 ⇒ 发现连同缘由写进报告交 PM 代立;⛔ 不查重硬立与静默弃报同为禁形。
5456
- PM 的去重读数随派发词下发,当既有事实用,只复核其后增量,⛔ 不重跑。
5557
- 归挂不散落:落在已排队 issue 完成范围内的发现,立成它的 sub-issue(自动进派发池)。
5658
- 只是依赖它的,独立立单带一行 `Blocked-by:`;立在修复落地的仓,带回链。
57-
- 观察类发现(死代码、未演练漂移、外观抛光)打 `finding` 标签,⛔ 不打 `pm:queue`
58-
- 具体缺陷不打标签,留给 PM 分诊;⛔ 不因看着小揣着不报,平实立单,分诊轮定级。
5959
- 有界就地修豁免,四条全立才就地修:① 与本卡同一缺陷类;② 机械修且形态已被钉死。
6060
- ③ 该文件无其他认领持有;④ 同一批门禁族,不新增验证面。
6161
- 就地修欠两样:认领申报的文件面同轮增补;PR 正文点名该修复并附证据。
@@ -73,10 +73,10 @@ model: opus
7373

7474
1. **重活串行,共享验证锁只有一个入口。** 每次 build/test 都从这里走。
7575
- `bash scripts/pm/os-verify-lock.sh -c '<command>'`(或 `-- <argv>`);⛔ 永不手搓 `flock`/lockfile。
76-
- 入口点保证:等待预算钉死在一次前台调用内;按到达序授予;99 专指没排到;报持锁时长
76+
- 一次前台调用领全部等待预算,阻塞到拿锁或 99;⛔ 不轮询重试,恒设 `OS_VERIFY_LOCK_SLOT`
7777
- 它不保证机器空闲:`check:*` 门禁、install、dev server 不走它,与持锁同核并跑。
7878
- 锁下墙钟绝对值是共享盒读数;只包命令本身,不包你的阅读与判断。
79-
- 结论读它印的 `VERDICT command-exit` 行,⛔ 不读裸 `$?`;排队是常态,不是挂死。
79+
- 结论读它印的 `VERDICT command-exit` 行,⛔ 不读裸 `$?`;按到达序授予,排队是常态,不是挂死。
8080
2. **压住堆**:重命令前缀 `NODE_OPTIONS=--max-old-space-size=4096`,要抬需给理由。
8181
3. **定向,不扫全**:只 build/test 受影响的包,turbo 用 `--concurrency=2`
8282
- 单文件跑法 `pnpm --filter <pkg> exec vitest run --maxWorkers=2 <file>`
@@ -92,8 +92,8 @@ model: opus
9292
- 消融/变异脚本自带还原 trap(硬线在标准条款节的 ablation 条)。
9393
7. **排队不是停摆,在轮内主动等。** 持锁的是你不拥有的进程,它的完成不会唤醒你。
9494
- ⛔ 永不为等锁结束一轮。
95-
- 循环:拿到 99 就把间隔花在无锁工作上(测试、changeset、PR 正文、包内 `typecheck`)。
96-
- 然后带同名再跑一次:`OS_VERIFY_LOCK_SLOT=<稳定名>` 在第一次尝试之前就设好
95+
- 99 专指没排到,读作 NOT MEASURED;把间隔花在无锁工作上(写测试、changeset、PR 正文)。
96+
- 再取以同名续位,⛔ 不从队尾重排:`OS_VERIFY_LOCK_SLOT=<稳定名>` 在第一次尝试前就设好
9797
- 没排到的调用把排位寄存,同名再来续原到达戳;不设它,每次离开都从队尾重排。
9898
- 排队约 20 分钟无进展 ⇒ 先看这次检查能否收窄到不必持锁(收窄要申报,见干净收尾)。
9999
- 收窄不了就停下报 `blocked` 并点名持锁者:`os-verify-lock.sh --status` 打印持锁者与队列。
@@ -371,7 +371,7 @@ model: opus
371371
"open_questions": [
372372
{ "question": "", "options": ["A …", "B …"], "recommendation": "A, because …" }
373373
],
374-
"out_of_scope_findings": ["filed as #<n>: one-line description"]
374+
"out_of_scope_findings": ["filed as #<n>: one-line description", "noted, not filed: one-line observation"]
375375
}
376376
```
377377

.claude/skills/pm-dispatch/references/platform-readings.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
- `assignees` 实测为陈旧空值而权威读非空;`labels`/`state` 由此存疑、未实测,⛔ 不写成已测。
145145
- ⇒ ⛔ 认领承载字段(`assignees``pm:*` 标签、`state`)永不从本档读。
146146
- ⛔ 本档的缺席不是读数:要么 `hasNextPage: false`,要么第二通道。
147+
- 两数组各比 `edges` 长度与 `totalCount`:短即最新几项缺席,认领检查走分页 `get_comments`
147148
- 自己刚写的按原写入通道回读;失效方向是空转与重发。
148149
- 成因:本档经 CDN 缓存,专对最新内容失效且绕不掉,cache-busting 查询串与 no-cache 头都无效。
149150
- 内容可滞后数分钟到数十分钟;它没有位置性对照 —— 更早内容全在,只有最新几条缺席。
@@ -250,6 +251,7 @@
250251
- 分步文件在 `{Job name}/{step number}_{step name}.txt`
251252
- completeness check 绿只断言没有 worker 静默死,不等于测试通过。
252253
- 并发输出的相邻不等于因果:先查 `turbo.json` 依赖边,⛔ 不只看 tail。
254+
- turbo 一败多包同印 `ELIFECYCLE Command failed`:权威读数是 `Failed:` 行与该包自己的 `##[error]`
253255
- 公开发出的诊断被推翻时,更正发在同样公开的位置,据它开的 PR 撤回 draft、解绑 `Fixes`
254256
- 判正文被截断必须双读取:`.body` 原文,加 `Accept: application/vnd.github.full+json``.body_html`
255257
- 两者同一处断掉才算 issue 端截断。
@@ -288,6 +290,7 @@
288290
- 第四形:裸 REST `POST /pulls` 建 PR 时,在已带 session-URL 页脚的正文后再追加一条同形页脚。
289291
- 该追加带前置横线、恰 90 字节,送出体是存储体的严格前缀。
290292
- ⇒ 追加形态随通道与动作(建 / 改)变,⛔ 不由任一条推其余;写后必回读。
293+
- 平台在尾部 `---` 前后正反两向归一空行:比对正文只按首个差异偏移,⛔ 不按长度。
291294
- 评论创建两通道都追加 58 字节 ⇒ 严格解析 `os-dev-report` 必须停在最后一个右花括号。
292295
- 并行 spec PR 同动 pin 计数断言:被踢不是事故,按 os-regen 序再解一轮。
293296
- 解冲突两侧收据都保留、按合并顺序堆叠;新计数从合并后源码重数,⛔ 不从收据做算术。

.github/workflows/ci.yml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,28 @@ jobs:
292292
# ~5× a normal sharded run (~4-6 min), with margin for a cold Turbo cache;
293293
# the old 45 left a hung job "running" for half an hour past any plausible
294294
# healthy finish.
295-
timeout-minutes: 30
295+
#
296+
# ⚠ TEMPORARY RAISE, 30 -> 45 (#16173). Shard 5/6 is being killed at the
297+
# 30-minute wall — 12+ observations at 30:16-30:21 with every other job in
298+
# every one of those runs green — so the paragraph above describes the
299+
# value this line must RETURN to, not the value it currently carries.
300+
#
301+
# Two reasons, both #16173's:
302+
# 1. Unblock. The only uncensored shard-5 readings are 25:32 and 27:36,
303+
# and shard 6/6 has been seen at 28:56 — a ~1 minute margin at the top
304+
# of the observed green band against a 30:00 wall.
305+
# 2. Un-censor. Every tail observation is a reading of THIS number, not
306+
# of the shard: the true duration is >= 30:16, unbounded above. So the
307+
# shard timings cannot be re-derived from CI history while the wall
308+
# stands here. Raising it produces the durations #16173 needs before
309+
# anyone touches `scripts/test-shard-timings.json`.
310+
#
311+
# ⛔ REVERT CONDITION, explicit: back to `30` once #16173 lands its shard
312+
# rebalance. This value is temporary and carries no other expiry — a raise
313+
# with no revert condition written beside it becomes permanent by
314+
# forgetting. The backstop stays loose only for that window; the stall
315+
# guard named above remains the primary hang detector throughout.
316+
timeout-minutes: 45
296317
permissions:
297318
contents: read
298319
strategy:

0 commit comments

Comments
 (0)