Repository navigation
Commit cfb602c
fix(lint)!: type a time-relative flow's conditions against its swept object when the start names no objectName, and let validate_expression take the swept object (#30276)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg1bQRcYtBcNuyy3fjtaaNaz)_
domain:server#2 · os-bill · session_01Utp6CaUg3ibF939L2RFB5D
Fixes #30026
Clause-②: yes (narrowing) — the author-time gate (`os build`, `os
validate`, the package publish and the flow publish door) refuses, in
the start, edge and `decision` conditions of a time-relative flow whose
start node names no `objectName`, a `type-mismatch`
(`type-mismatch-run-fails`), a `record.FIELD` read of a field the swept
object does not declare (`unknown field`), and date arithmetic
(`date-arithmetic-run-fails` / `date-text-arithmetic-run-fails`), which
it passed with no verdict at all before. Added beside it (the widening
half): the optional `sweepObject` input of the `validate_expression` MCP
tool.
Metadata-protocol: narrowed
Skills-docs: updated here — the MCP skill text in
`packages/mcp/src/skill-md.ts` (the new `sweepObject` input);
`skills/**` teaches no time-relative flow condition with a
text-vs-number comparison, so no skill or eval moves.
## 维护者速读
- 改了什么:时间相对(定时扫描)流程的开始节点不写 `objectName`、只在 `timeRelative.object`
写了被扫描的对象时,条件现在按被扫描对象的字段做类型检查,文本与数字比较会被发布拦下(以前完全不检查);开始节点写的对象与扫描对象不一致时,仍只给警告,但会多一条说明告诉作者为什么、怎么改;MCP
的 `validate_expression` 新增可选输入 `sweepObject`,能给出与发布门一致的判决。
- 为什么改:这类流程每次运行拿到的 `record` 就是被扫描对象的一行,条件里文本与数字比较每次都会让运行失败,但发布时一声不吭;另外
#30015 的注释和测试描述了一个根本绑不上的形状(不写 `timeRelative.object`)。
- 风险与回滚:只收窄一种形状的发布(BREAKING 已在 changeset 写明 FROM → TO),已登记的 ADR-0087
语义条目覆盖同一修法;其余流程判决不变;回滚即撤 PR。
- 先看:`packages/lint/src/validate-expressions.ts` 流程遍历开头的 `objectName =
startObject ?? sweepObject` 与
`sweepsTypedObject`,同文件流程末尾的说明警告;`packages/mcp/src/mcp-http-tools.ts` 的
`sweepObject` 输入与 `flow_condition` 判决。
- 两分钟验证:`pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2
src/validate-expressions.test.ts -t "time-relative"`,预期 3 个用例通过,其中
`tr_sweep_only` 三处条件均为 error。
- 需要你做的:无,席位落地(收窄面由席位安排 CONTRACT_REVIEW_TIER 复审)。
## What changes
- **Lint — the typed object of a flow** (`validateStackExpressions`, the
flow walk). The object a flow's conditions are typed against is the
start node's `objectName`, else, on a time-relative flow,
`timeRelative.object`. `TimeRelativeTriggerSchema` requires that key,
and `time-relative-trigger.ts` launches every run with a row of that
object as `record` (`engine.find(desc.object, …)`, then `{ record: row
}`), whatever the start node names. So a sweep whose start names no
`objectName` is now typed against the swept object at every flow site
that reads the typed object: start, edge and `decision` conditions
(field existence, the type check with the `run-fails` outcome,
`record.FIELD` reads), the shadowed-variable warning, and the root
check's did-you-mean order. The null-guard twin is unchanged
(record-triggered flows only).
- **Lint — `sweepsTypedObject`** is now `timeRelative.object === typed
object` and nothing else. The old "else the start node's" arm graded a
descriptor with no `object` as a sweep of the start object; that
descriptor never binds (`flow-time-relative-descriptor-invalid`, error),
so no run of it binds a row and it now gets no declared outcome (the
warning). The ledger comment says so.
- **Lint — why a sweep of another object only warns.** When the start
node's `objectName` is not `timeRelative.object`, the
`type-mismatch-run-may-fail` warning stays, and the flow gets one more
warning at `flows[i].nodes[j].config.objectName` saying why: the
conditions are checked against the start object's fields while each
run's record is a row of the swept one; set `objectName` to the swept
object or remove it. It is pushed once per flow, and only when the flow
was served a `type-mismatch-run-may-fail` warning (counted in `check()`
by that one `code`). The date verdicts' `-run-may-fail` arm (#30016) is
not counted: it is an error whatever the outcome, so the sweep softens
nothing there and there is nothing to explain; a pin says so.
- **MCP — `validate_expression` takes `sweepObject`** (optional; site
`flow_condition` with `triggerKind: 'time_relative'` only, a tool error
anywhere else, the same refusal shape as `severity` / `triggerKind`). It
is the flow's `timeRelative.object`; another object than `objectName`
gives the gate's warning instead of the run-fails error. The
`flow_condition` outcome reads a third input, `sweepsTypedObject`, the
lint ledger's own third fact. The tool description and the MCP skill
text (`skill-md.ts`) name the input.
- **What else the typed object refuses.** Typed against the swept
object, a sweep-only flow also gets the two other verdicts a sweep of
the start object gets, both new refusals there: a `record.FIELD` read of
a field the swept object does not declare (`unknown field FIELD on
OBJECT`), and arithmetic on a date field (`date-arithmetic-run-fails`).
Both are pinned, and both are in the changeset's Clause-② sentence and
FROM → TO.
- **Tests.** The `tr_default` pin (a descriptor with no `object`, a
shape that cannot bind) is re-purposed to `tr_sweep_only`: a start with
no `objectName` sweeping `crm_opportunity`, refused at start, decision
and edge. New pins: the note on `tr_other` (once, its `where`, `path`
and `source`) and its absence on a clean `tr_other`, on `tr_same` and on
`tr_sweep_only`; a well-typed `tr_sweep_only` flow is clean. The MCP
per-trigger-kind table gains three rows (`sweepObject` = `objectName`,
start with no `objectName`, `sweepObject` another object), each asserted
equal to the gate's own verdict on the same flow, and the wrong-site
refusal list gains four `sweepObject` calls. The key-read meta-test now
lists `id` on `startNode` (the note names the start node).
- **Changeset** `.changeset/30026-time-relative-sweep-typing.md`,
`minor` on `@objectstack/lint` and `@objectstack/mcp`, BREAKING with
FROM → TO, ADR-0087 `already-registered
flow-record-read-and-decision-type-mismatch-refused`, and a correction
note for the released #30015 changeset text ("or is omitted"), which is
left as is.
## Acceptance notes
- **ADR-0087 disposition.** `already-registered
flow-record-read-and-decision-type-mismatch-refused`: the entry
prescribes this exact rewrite for a time-relative flow's conditions
(same code, same replacement, same reason). Its `surface` text says
"sweeping the start node's objectName", which this change makes narrower
than the gate; editing it is `packages/spec/**`, outside this card's
surface. The seat files it for `domain:spec`.
- **A sweep of another object is still typed against the start object.**
Field reads are judged against the start object too, so a `record.FIELD`
read of a field only the swept object declares is still refused as
unknown there, while the run would hold it. That is pre-existing and not
made worse here; the new note tells the author to align the two objects,
which removes it.
- **The engine's binding fallback.** `deriveTriggerBinding`
(`plugin-extension-api.ts`) still names `config.objectName` as the
binding's `object` when `timeRelative.object` is absent. The sweep
itself reads only the parsed descriptor, so no run depends on it; noted,
not touched.
## Verification
All readings on head `52faf73c` (merged with `main` at `959c26be`; the
merge resolves the `validate-expressions.test.ts` conflict with #30016
by keeping both sides' tests) unless named.
<details><summary>Build, typecheck, tests</summary>
- Build: `pnpm --filter '@objectstack/lint^...' --filter
'@objectstack/mcp^...' build`, VERDICT command-exit 0; full `turbo run
build --filter='!@objectstack/docs'`, 119/119 tasks, VERDICT
command-exit 0.
- `@objectstack/lint`: `pnpm test` 8006/8006 (193 files); `pnpm
typecheck` 0 (with `check:test-typecheck`).
- `@objectstack/mcp`: `pnpm test` 678/678 (56 files); `pnpm typecheck` 0
(with `check:test-typecheck`).
- `eslint --no-inline-config` over the five changed source and test
files: 0. The repo-wide `pnpm lint` is CI's.
</details>
<details><summary>Ablations (each through scripts/ablation-replace.mjs;
restored blob == HEAD, `git diff HEAD` empty)</summary>
- A1, lint source (the lint suite imports `src`): `objectName =
startObject ?? sweepObject` → `startObject`. 2 red on `52faf73c`:
`tr_sweep_only: expected [] to deeply equal [ …3 ]`, the pre-fix answer
(no verdict at all), and the unknown-field / date-arithmetic pin. (On
`bc376239`, before that pin existed: 1 red.)
- A2 (on `bc376239`; the MCP sources are unchanged since), lint `dist/`
(the MCP suite reads `@objectstack/lint` through its exports): the same
mutation with a string marker, lint rebuilt, `ablation-dist-preflight`
present in 4 built files; 1 red: `time_relative, start names no
objectName: expected [] to have a length of 1`. Restore leg rebuilt,
preflight `--absent` over all 20 built files, suite 38/38.
- A3 (on `bc376239`), MCP source: `sweepsTypedObject` forced `true`. 1
red: `sweepObject another object: expected [ [ 'error', … ] ] to deeply
equal [ [ 'warning', … ] ]`.
</details>
<details><summary>Derived gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `52faf73c`: 59 families; all 59 run, each
exit 0 (exit code captured per line before any pipe).
- `--ran` reconciliation: "59 derived famil(ies) accounted for — 59 run,
0 NOT-MEASURED".
</details>
<details><summary>Dogfood (the narrowing arm's battery, on the final
head)</summary>
- On `52faf73c`, after a full `turbo run build
--filter='!@objectstack/docs'` (119/119): `pnpm --filter
@objectstack/dogfood test` under the lock, no shard, no path: Test Files
418/422 passed (3 skipped, 1 failed), Tests 3491 passed, 36 skipped, 1
failed; VERDICT command-exit 1.
- The one red is `permission-set-clone-door.dogfood.test.ts` › "a
platform set … copies into its shipping package's pair": cloning
`member_default` answers 422, `objects.sys_comment` /
`objects.sys_activity` not defined in the target package. It is not this
change: with `validate-expressions.ts`, `mcp-http-tools.ts` and
`skill-md.ts` set back to `origin/main` and lint and mcp rebuilt, the
same file is red the same way (1 failed / 10 passed; control taken on
`bc376239`). The seat tracks it as #30255, with its revert in the merge
queue. The grants arrived on `main` with #30222 (`490d136f`, "every
member reads and posts comments, and reads the activity history"),
inside this branch's base.
</details>
CI on `52faf73c` at the time of writing: 63 check-runs, 54 success, 9
skipped, 0 failed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6CaUg3ibF939L2RFB5D
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CaUg3ibF939L2RFB5D)_
<!-- signed: domain:server#2 · os-bill ·
session_01Utp6Cbvcmv8YWFhFJwzjKL -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent bfca0ef commit cfb602c
6 files changed
Lines changed: 269 additions & 44 deletions
File tree
- .changeset
- packages
- lint/src
- mcp/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1107 | 1107 | | |
1108 | 1108 | | |
1109 | 1109 | | |
1110 | | - | |
1111 | | - | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
| 1113 | + | |
| 1114 | + | |
| 1115 | + | |
1112 | 1116 | | |
1113 | 1117 | | |
1114 | 1118 | | |
1115 | 1119 | | |
1116 | 1120 | | |
1117 | | - | |
1118 | | - | |
| 1121 | + | |
| 1122 | + | |
1119 | 1123 | | |
1120 | 1124 | | |
1121 | 1125 | | |
| |||
1130 | 1134 | | |
1131 | 1135 | | |
1132 | 1136 | | |
1133 | | - | |
1134 | | - | |
1135 | | - | |
1136 | | - | |
| 1137 | + | |
| 1138 | + | |
| 1139 | + | |
| 1140 | + | |
1137 | 1141 | | |
1138 | | - | |
| 1142 | + | |
1139 | 1143 | | |
1140 | 1144 | | |
1141 | 1145 | | |
| |||
1149 | 1153 | | |
1150 | 1154 | | |
1151 | 1155 | | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
| 1180 | + | |
| 1181 | + | |
| 1182 | + | |
| 1183 | + | |
| 1184 | + | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
| 1192 | + | |
| 1193 | + | |
| 1194 | + | |
| 1195 | + | |
| 1196 | + | |
| 1197 | + | |
| 1198 | + | |
| 1199 | + | |
| 1200 | + | |
| 1201 | + | |
| 1202 | + | |
| 1203 | + | |
| 1204 | + | |
| 1205 | + | |
| 1206 | + | |
| 1207 | + | |
| 1208 | + | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
| 1215 | + | |
| 1216 | + | |
| 1217 | + | |
| 1218 | + | |
| 1219 | + | |
| 1220 | + | |
| 1221 | + | |
| 1222 | + | |
| 1223 | + | |
| 1224 | + | |
| 1225 | + | |
| 1226 | + | |
| 1227 | + | |
| 1228 | + | |
| 1229 | + | |
| 1230 | + | |
| 1231 | + | |
| 1232 | + | |
| 1233 | + | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
| 1239 | + | |
| 1240 | + | |
| 1241 | + | |
| 1242 | + | |
| 1243 | + | |
| 1244 | + | |
| 1245 | + | |
| 1246 | + | |
| 1247 | + | |
| 1248 | + | |
| 1249 | + | |
| 1250 | + | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
1152 | 1263 | | |
1153 | 1264 | | |
1154 | 1265 | | |
| |||
4034 | 4145 | | |
4035 | 4146 | | |
4036 | 4147 | | |
4037 | | - | |
| 4148 | + | |
| 4149 | + | |
4038 | 4150 | | |
4039 | 4151 | | |
4040 | 4152 | | |
| |||
0 commit comments