Repository navigation
fix(lint)!: refuse a time-relative start whose objectName is not the object it sweeps - #30393
Conversation
…swept object A time-relative run binds a row of config.timeRelative.object and nothing else, while the expression checks type the flow's conditions against the start node's config.objectName. A start naming two objects is now refused with flow-time-relative-object-mismatch, at config.objectName, naming both objects and the fix. The explanatory warning on a sweep of another object names the new rule. Claude-Session: https://claude.ai/code/session_01Utp6CXGZhZVasbhiGEdpZk Co-authored-by: Claude <noreply@anthropic.com>
…e prescription The ADR-0087 disposition is no-migration-prescription: which object a start naming two meant is a judgement, so the body carries no FROM/TO rewrite. Claude-Session: https://claude.ai/code/session_01Utp6CXGZhZVasbhiGEdpZk Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a97bbfd248d1955afd2a1e1732306ce6023386e9 && git checkout a97bbfd248d1955afd2a1e1732306ce6023386e9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 38717d34a4d16b44e7b22cb4bccf745ae47086d6 d3580892c48144db564df73810a605a88ed19bb0 && git checkout -B drift-repro 38717d34a4d16b44e7b22cb4bccf745ae47086d6 && git merge --no-ff d3580892c48144db564df73810a605a88ed19bb0
node scripts/docs-audit/affected-docs.mjs --json 38717d34a4d16b44e7b22cb4bccf745ae47086d6
|
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flagsFrom os-dev-report 6654371968:
Flags of this review's own:
④ Design judgmentDoes the narrowing take a declared capability away, or make the spec's own contract real? It makes it real. The flow schema admits
Findings:
Implemented-by: VERDICT: PASS |
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL 维护者速读
席位核对 — ACCEPT · domain:server#2 · head
|
Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6CXGZhZVasbhiGEdpZk
Fixes #30283
Clause-②: yes (narrowing) — a new published readiness rule id,
flow-time-relative-object-mismatch, gradederror: the author-time gate (os build,os validate, the package publish and the flow publish door) refuses a time-relative start whoseconfig.objectNameis set and is notconfig.timeRelative.object, which it passed with no readiness verdict before.Metadata-protocol: narrowed
Skills-docs: none —
skills/**andcontent/docs/**teach the time-relative sweep withtimeRelative.objectalone and noobjectNameon its start node, which this rule accepts; the MCP skill text keeps itssweepObjectadvice forvalidate_expression, whose expression verdict this change does not move.维护者速读
objectName写的对象和timeRelative.object(真正被扫描的对象)不一样,发布和os validate现在直接拦下(新规则flow-time-relative-object-mismatch,error),提示里写明两个对象和改法:删掉objectName或改成被扫描对象。record只是被扫描对象的一行,objectName从不被读,但条件检查却按objectName的字段来做。实测两个方向都错:只有objectName对象才有的字段写进条件,所有检查全过、每次运行都报No such key失败;反过来正确读被扫描对象的字段,却被当成未知字段拒掉。objectName不改变任何运行行为;仓里的示例、文档、技能都没有这种写法;已存的此类流程照常运行,只是下次发布会被拦;回滚即撤 PR。packages/lint/src/validate-flow-trigger-readiness.ts的 1b-iii 判决块(只判引擎真正绑到时间相对触发器的流程);同文件FLOW_TIME_RELATIVE_OBJECT_MISMATCH的说明与严重度段;packages/lint/src/rule-explanations.ts新条目。pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-flow-trigger-readiness.test.ts -t "30283",预期 7 个用例通过,其中 "closes the measured hole" 在三个命令下都只报这一条 error。What changes
flow-time-relative-object-mismatch(validateFlowTriggerReadiness, block 1b-iii). On a time-relative start whoseconfig.objectNameis a string and is notconfig.timeRelative.object(a string), oneerroratflows[i].nodes[j].config.objectName: the verdict names both objects ("each run's record is a 'B' row, and 'A' is never bound"), thefix:line says removeobjectNameor set it to the swept object. Judged only where the engine binds the time-relative trigger:resolveFlowTriggerKind(flow) === 'time_relative'and no array-form record token (the engine'sderiveTriggerBindingpre-check routes that shape to the record-change trigger, which does bindobjectName). Arecord-*token outranks the descriptor in the resolver, so that flow is not judged either. A descriptor whoseobjectis not a string is left toflow-time-relative-descriptor-invalid; a bad window beside a string object reports both, at two keys.error, on the file's own criterion (is this stack enough to know?). Both names sit on the one start node, and which one the run binds is a hardcoded read in the engine's binding resolver (timeRelative.objectfirst), so nothing installed changes the answer. The file header, the severity section and theauthoring-rules.tsentry comment say so. Not a never-fire verdict: the sweep binds and runs; the rule is not added to the never-fire id list.os explain flow-time-relative-object-mismatch(rule-explanations.ts): what the run binds, what the checks type against, the measured outcome both ways, the fix, what is not reported, and the gate reach (the runtime publish gate judges only the written flow, so a stored flow keeps being served and its own next publish is refused).FLOW_TIME_RELATIVE_OBJECT_MISMATCHis exported from@objectstack/lint(rule-id-barrel-exports.test.tsholds it).validate-expressions.ts— theconfig.objectNamewarning on a sweep of another object (from lint/mcp: a time-relative flow condition is typed only against the startobjectName— a flow naming onlytimeRelative.objectgets no type verdict; MCP assumes the sweep object; #30015 wording on an omitted object is false #30026) keeps its explanation and now names the readiness rule as the verdict asking for the same fix ("as flow-time-relative-object-mismatch asks of this start"); it does not repeat the verdict.validate-flow-trigger-readiness.test.ts): a severity-table row; a[#30283]block with seven cases: the refusal (rule, severity, where, path, both names, one-line message and fix of at most 200 characters); silence on the swept object and on noobjectName; silence where the engine binds another trigger (record-after-update, array form), paired with the shape that fires; the descriptor-rule split and co-firing; the measured hole through every authoring command (validate,build,lintanswer exactly this one error); the runtime publish gate refusing the write on the flow alone; theos explainfacts.validate-expressions.test.ts: the note names the rule..changeset/30283-time-relative-object-mismatch.md,minoron@objectstack/lint, BREAKING with what an author does, ADR-0087not-required (no-migration-prescription): no rewrite keeps intent, because whether the author meant the swept object or the named one is a judgement.Acceptance notes
error(on9afcd6a6). Objectscrm_opportunity { amount }andcrm_task { priority, close_date }, a start withobjectName: 'crm_opportunity'sweepingcrm_task. Author time (lintvalidateStackExpressions+ readiness):record.amount > 10andamount > 10→ no finding at all;record.priority > 1→unknown field priority on crm_opportunity(error); the same two conditions with noobjectName→unknown field amount on crm_taskand clean, respectively. Run time (AutomationEngine.executewith the contexttime-relative-trigger.tsbuilds: acrm_taskrow asrecord,object: 'crm_task'):record.amount > 10→ run failsNo such key: amount;amount > 10→Unknown variable: amount;record.priority > 1→ run succeeds, while the engine's registration pass logsunknown field priority on crm_opportunity. So the contradiction hides a run-failing condition from every gate and refuses a correct one. The binding itself was read from source, not run:deriveTriggerBinding(plugin-extension-api.ts) takestimeRelative.objectbeforeobjectName, andexpandDeclaredLookupsreads the run'sobject.git grepoverexamples/,skills/,content/docs/(outside the generated references) and the MCP skill text: no time-relative start carries anobjectName(the showcaseshowcase_task_due_remindersweep names none). The dogfood battery below is the executed check.validate_expression'ssweepObjectadvice (packages/mcp/src/skill-md.ts,mcp-http-tools.ts) still says a mismatch ofsweepObjectandobjectNamemakes the expression verdict a warning. That stays true of the expression verdict; the publish gate now also refuses the start. Outside this card's surface; noted, not changed.flow-registration-checks.ts) still types a time-relative flow's conditions againstobjectName, logging the misleadingunknown fieldwarn measured above. After this rule such a flow cannot be published, so the warn is reachable only for a flow stored before; noted, not touched.Verification
All readings on head ``d3580892
(fb38ffb` the change, `b4c229b6` merged with `main` at `8589756a`, `d3580892` the changeset's author paragraph)` (this branch merged with `main` at `8589756a`) unless named.Build, typecheck, tests
pnpm --filter '@objectstack/dogfood^...' build(lint included), VERDICT command-exit 0; the seven packagescheck:dual-build-cjs-loadsreads that lie outside that closure, VERDICT command-exit 0.@objectstack/lintond3580892:pnpm test8014/8014 (193 files);pnpm typecheck0 (withcheck:test-typecheck).pnpm lint(eslint . --no-inline-config) ond3580892: exit 0.Ablations (each through scripts/ablation-replace.mjs; restored blob == HEAD, `git diff HEAD` empty)
src): the mismatch condition gated off ('ABLATE-30283' === 'on' &&). 6 red, among themcloses the measured hole …: validate: expected [] to deeply equal [ [ 'flow-time-relative-object-mismatch', 'flows[0].nodes[0].config.objectName' ] ](the pre-fix answer: no error from any rule) and the severity rowflow-time-relative-object-mismatch is error.resolveFlowTriggerKind(flow) === 'time_relative' && !isArrayRecordTriggeredreplaced by a constant true. 1 red:is silent where the engine binds another trigger, which reads objectName and never sweeps.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackond3580892: 57 families (the same list as onb4c229b6), all 57 exit 0 (exit code captured per line before any pipe).check-adr-0087-registration(the changeset carried a FROM/TO rewrite besideno-migration-prescription; fixed ind3580892, the author paragraph is now prose),check-plugin-teardown-shape --self-test(its pinned fixture commit was outside this shallow clone; that commit fetched, 48 cases pass),check:dual-build-cjs-loads(PREREQUISITE NOT MET: seven unrelated packages had nodist/; built, then exit 0).--ranreconciliation: "57 derived famil(ies) accounted for — 57 run, 0 NOT-MEASURED".Dogfood (the narrowing arm's battery, on the final head)
pnpm --filter @objectstack/dogfood testunder the lock onb4c229b6(no shard, no path;d3580892changes only the changeset text): Test Files 419 passed, 3 skipped (422); Tests 3492 passed, 36 skipped (3528); VERDICT command-exit 0. No fixture or example flow carries the refused shape.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CXGZhZVasbhiGEdpZk
Generated by Claude Code