Repository navigation
fix(spec)!: refuse a start-node filter beside timeRelative and a schedule the readers cannot use - #31376
Conversation
…dule the readers cannot use A start node's config is an open record, so the flow parse never judged two shapes that then do nothing: a `filter` beside `timeRelative` (the sweep reads `timeRelative.filter`, so the flow launches for every record in the window), and a `schedule` normalizeSchedule reads nothing from (a schedule flow stays unbound, a sweep falls back to daily 08:00 UTC). FlowSchema now refuses both, through flowStartConfigRefusals: a typed schedule object is judged against the published ScheduleSchema and then for being readable; any other value only when the reader reads nothing, so the bare cron string and the untyped aliases that bind today stay accepted. flowStartScheduleReadsNothing mirrors normalizeSchedule branch for branch, and trigger-schedule pins the two equal. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 171 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 5e54edfbd3f293be0157d78ff5df48634185032b && git checkout 5e54edfbd3f293be0157d78ff5df48634185032b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eff90af6a2a68202ef167b8ed91aadecfaed90b7 bdc95ace88d92c914d25a04dba2cd2725ac1baab && git checkout -B drift-repro eff90af6a2a68202ef167b8ed91aadecfaed90b7 && git merge --no-ff bdc95ace88d92c914d25a04dba2cd2725ac1baab
node scripts/docs-audit/affected-docs.mjs --json eff90af6a2a68202ef167b8ed91aadecfaed90b7
|
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract reviewServed-tier: Inputs read: card #31224 body and every comment (triage 6711648543, claim 6712039823, plan 6712129038, seat rulings 6712144718 and 6714195854, dev reports 6713825906, 6716296533 and the B1 report 6717473839); PR #31376 body, file list (9 paths) and ① Derived judgmentsParity with The typed arm is NOT parity, and the ruling says so. A typed object is judged by the published
Top-level start nodes only — right. Every reader picks Doors, and which keep. The rule is in Message, location and cure — right, two nits. Path Census — nothing shipped, example, dogfood or hotcrm is refused. Tests. The 10 spec cases pin the filter refusal and both acceptances, each accepted schedule form (bare string, three typed, five untyped aliases), the IANA refusal at the parse with its sub-path, four ② Semver level
③ Boundary flags
④ Design judgmentAgainst the published spec on Altitude: parse, by ruling. The card's Expected names "the checks in Duplication: the spec now carries a copy of the runtime's Implemented-by: VERDICT: FAIL |
…ases, restate the ADR-0087 reason
The filter refusal now fires only when `config.timeRelative` is a non-null
object, the test the trigger-kind resolver routes a sweep by. A typed
`{ type: 'cron', cron }` or `{ type: 'interval', every }` schedule is refused
with the declared spelling named (`expression`, `intervalMs`). The changeset's
ADR-0087 reason says what is true of each arm, and the release note no longer
carries a FROM/TO rewrite: the parse refusal names where the key belongs.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract-review-claim: session_01FEkQ5mw7HFYn9o4LEGMQdK · bdc95ac · 2026-11-08T05:26Z Delta review of the B1 fix round, answering FAIL 6717630512 under seat ruling 6717636410. |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract reviewServed-tier: Delta record on the FAIL of record 6717630512 (head ① Derived judgments(a) The commit, hunk by hunk. Three hunks in The guard is exactly the resolver's test. The typed-alias hint — right, and only where it applies. For (d) Nothing new turned false. Messages: the filter message is unchanged and now reaches only start nodes the engine routes as sweeps, so its "a time-relative sweep takes its row filter from ② Semver level
③ Boundary flags
④ Design judgmentThe design judged on Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK 维护者速读
席位核对Verdict: ACCEPT · head
Ruling-ref: the maintainer, 2026-11-01T12:11Z: 「定级你可以自己决定」; 2026-11-07T16:00Z: 「你有 fable ,车道契约复审可以自己做」; 2026-11-07T16:59Z: 「你应该使用 pm-dispatch 技能审核和落地pr」; standing landing authorisation 2026-10-25T11:35Z: 「按照 pm-dispatch 规则复核过的都授权」. |
Requested by Leehom · project thread
Refs #31224
Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: none —
skills/**andcontent/docs/**teachtimeRelative.filterinside the descriptor and typed{ type: 'cron', expression }schedules, which this accepts. The generatedskills/objectstack-automation/references/_index.mdgains the source files the flow closure now reaches (system/job.zod.tsand its imports), regenerated bycheck:generated --fix.维护者速读
FlowSchema新增两类开始节点config拒绝(新文件flow-start-config-refusals.ts):①config.timeRelative为对象时,与之并列的config.filter:没有任何读取方,时间相对扫描只读timeRelative.filter(timeRelative: null不判定);②config.schedule读不出调度:带type的对象按已发布的ScheduleSchema判定(因此{ type: 'cron', cron }、{ type: 'interval', every }两个带类型别名也被拒绝,提示改用expression/intervalMs),其余值只在normalizeSchedule什么都读不出时拒绝。裸 cron 字符串和运行时接受的无类型别名({ cron }/{ expression }/{ every }/{ intervalMs }/{ at })照旧接受。新增三个导出、spec minor changeset。defineStack、os validate/build、保存门含草稿、发布、引擎注册)拒绝两种此前接受的写法。普查:filter与timeRelative并列 0 处;对象形式的开始节点调度全部带type且合法(showcase、cli 夹具、dogfood 夹具);两个带类型别名 0 处;9 处裸字符串 cron(app-todo 2、hotcrm 7)照旧接受。回滚即 revert。packages/spec/src/automation/flow-start-config-refusals.ts的模块头和flowStartScheduleReadsNothing;以及packages/triggers/trigger-schedule/src/schedule-reader-spec-parity.test.ts的 34 条对齐钉。pnpm --filter @objectstack/spec exec vitest run src/automation/flow-start-config-refusals.test.ts(12 条)与pnpm --filter @objectstack/trigger-schedule exec vitest run src/schedule-reader-spec-parity.test.ts(34 条),预期全部通过。What changes
packages/spec/src/automation/flow-start-config-refusals.ts(new) exportsflowStartConfigRefusals(flow),flowStartScheduleReadsNothing(raw)and theFlowStartConfigRefusaltype. The rules cover top-levelstartnodes only, atnodes[i].config.*:config.filterbesideconfig.timeRelative. Judged only whenconfig.timeRelativeis a non-null object, the test the trigger-kind resolver (flow-trigger-kind.ts) routes a sweep by, sotimeRelative: nullis not judged. Refused atnodes[i].config.filter: "nothing reads it … launches for every record in the date window. Move it intoconfig.timeRelative.filter." The sweep readstimeRelative.filter(time-relative-trigger.ts).config.schedule. Both readers (ScheduleTrigger, andTimeRelativeTriggerfor a sweep's cadence) pass it throughnormalizeSchedule. When that yieldsnull, a schedule flow is "not bound" (one warn), and a sweep falls back toTIME_RELATIVE_DEFAULT_CRON. The verdict, atnodes[i].config.schedule, by form:typekey: judged against the publishedScheduleSchema, so the discriminant, an IANAtimezone, a positive integerintervalMsand an ISOatmust all hold; the first issue is quoted, with its sub-path. An object that passes is then also judged for readability, so a typed cron whoseexpressionis a{ dialect, source }envelope is refused, because the reader takes only a string. The schema does not declarecronoreveryon a typed object, so{ type: 'cron', cron }and{ type: 'interval', every }are refused although the reader binds them today (seat rulings 6714195854 (a), confirmed in 6717636410). When the refusal sees one of those keys it names the declared spelling (expression,intervalMs). Census for both: 0 in objectstack and hotcrm;flowStartScheduleReadsNothing(an empty object, an unknown key, a number, an array, a non-positive interval).nullor absent is not judged.packages/spec/src/automation/flow.zod.ts:FlowSchema.superRefinecallsflowStartConfigRefusalsbeside the api-trigger refusals.packages/spec/src/automation/index.ts: re-exports the module.check:generated --fix:api-surface/automation.jsonandexport-origins/automation.json, each +3 names;skills/objectstack-automation/references/_index.md, becausejob.zod.tsand its imports now reach the flow closure through theScheduleSchemaimport..changeset/31224-flow-start-config-refusals.md:'@objectstack/spec': minor, BREAKING banner, ADR-0087not-required (no-migration-prescription), per seat ruling 6717636410 §1. The reason says what each arm refuses:config, which the published descriptor declares only insidetimeRelative(the 3b — wire the flow executors toparse()their config, and tighten the undeclared-key warning into an error #4277 class);ScheduleSchemarefuses, or that the reader reads nothing from.The release note carries no FROM/TO rewrite: the parse refusal names where the key belongs.
One reading, pinned.
flowStartScheduleReadsNothingmirrorsnormalizeSchedule(@objectstack/trigger-schedule) branch for branch, including its truthiness and its "first candidate present decides" precedence.packages/triggers/trigger-schedule/src/schedule-reader-spec-parity.test.tspinsflowStartScheduleReadsNothing(x) === (normalizeSchedule(x) === null)over 33 shapes, plus the absent-schedule case. If the two drift, the parse would either refuse a schedule that binds or accept one that never does. The pin is test-only, so@objectstack/trigger-scheduletakes no changeset.Tests
packages/spec/src/automation/flow-start-config-refusals.test.ts, 12 cases:timeRelative, and on a start node withouttimeRelative; not judged besidetimeRelative: null, both through the module and at the parse.timezone: 'UTC+8'(path and sub-path pinned).ScheduleSchema: an unknowntype,intervalMs: 0, a cron with noexpression, a non-ISOat.{ type: 'cron', cron }namesexpressionand{ type: 'interval', every }namesintervalMs(path, sub-path and hint pinned); a typed cron with no alias key carries no alias hint.{ cadence },{},[],42,true, a blank string,{ every: -5 }, and a typed cron with an envelope expression.nullschedule, and a non-start node.packages/triggers/trigger-schedule/src/schedule-reader-spec-parity.test.ts: the 34-case parity pin above.filterbesidetimeRelative: 0 in objectstack examples, fixtures and non-test packages, and 0 in hotcrm.examples/app-showcase({ type: 'interval', intervalMs: 60000 }), the clioption-b-collection-zoofixture and the dogfoodschedule-organization-fixture(typed crons). All typed and all valid.{ type: 'cron', cron }and{ type: 'interval', every }: 0 in objectstack and hotcrm.examples/app-todoand 7 in hotcrm, still accepted.Acceptance notes
ScheduleSchemaon every object form, was declined, because it would refuse untyped forms that run today. C landed as fix(lint)!: refuse a flow node no edge path from the start node reaches #31287 and D as fix(lint)!: judge a roll-up's summaryOperations.filter keys against its child object #31337; A is not built. The seat closes the card, soRefs.yes (narrowing)for the three new exports,narrowed, spec minor.Verification
Head
bdc95ace, two commits on the branch restarted fromorigin/mainc2bc76e3, which includes #31337:532b289fand the review-round fix. These are the fast checks the seat asked for, onbdc95ace:pnpm --filter @objectstack/spec typecheckexits 0, includingcheck:test-typecheck. That needsNODE_OPTIONS=--max-old-space-size=8192locally; at the default heap it runs out of memory.pnpm --filter @objectstack/spec check:generatedexits 0 after a spec build, at the same heap. It coverscheck:api-surface,check:export-originsand the skills refs.check:docsexits 0 (243 generated files in sync).flow-start-config-refusals.test.ts(12),flow.test.tsandscripts/export-origins.test.ts.schedule-reader-spec-parity.test.ts34/34.check-adr-0087-registrationexits 0, reading the new reason.check-changeset-no-majorwith apull_requestevent carrying this body exits 0.Earlier, on the pre-restart local build of the same change, full suites passed:
@objectstack/trigger-schedule: 12/12 files;@objectstack/service-automation: 264/264 files.CI runs the full set.
Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV