Skip to content

fix(lint)!: refuse a time-relative start whose objectName is not the object it sweeps - #30393

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-kpp4uc
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-kpp4uc

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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, graded error: the author-time gate (os build, os validate, the package publish and the flow publish door) refuses a time-relative start whose config.objectName is set and is not config.timeRelative.object, which it passed with no readiness verdict before.
Metadata-protocol: narrowed
Skills-docs: none — skills/** and content/docs/** teach the time-relative sweep with timeRelative.object alone and no objectName on its start node, which this rule accepts; the MCP skill text keeps its sweepObject advice for validate_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 失败;反过来正确读被扫描对象的字段,却被当成未知字段拒掉。
  • 风险与回滚:只收窄这一种自相矛盾的写法(BREAKING 与作者该怎么改已写进 changeset);删掉 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。
  • 需要你做的:无,席位落地(收窄面由席位安排 CONTRACT_REVIEW_TIER 复审)。

What changes

  • Lint — new readiness rule flow-time-relative-object-mismatch (validateFlowTriggerReadiness, block 1b-iii). On a time-relative start whose config.objectName is a string and is not config.timeRelative.object (a string), one error at flows[i].nodes[j].config.objectName: the verdict names both objects ("each run's record is a 'B' row, and 'A' is never bound"), the fix: line says remove objectName or 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's deriveTriggerBinding pre-check routes that shape to the record-change trigger, which does bind objectName). A record-* token outranks the descriptor in the resolver, so that flow is not judged either. A descriptor whose object is not a string is left to flow-time-relative-descriptor-invalid; a bad window beside a string object reports both, at two keys.
  • Severity: 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.object first), so nothing installed changes the answer. The file header, the severity section and the authoring-rules.ts entry 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).
  • Barrel: FLOW_TIME_RELATIVE_OBJECT_MISMATCH is exported from @objectstack/lint (rule-id-barrel-exports.test.ts holds it).
  • validate-expressions.ts — the config.objectName warning on a sweep of another object (from lint/mcp: a time-relative flow condition is typed only against the start objectName — a flow naming only timeRelative.object gets 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.
  • Tests (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 no objectName; 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, lint answer exactly this one error); the runtime publish gate refusing the write on the flow alone; the os explain facts. validate-expressions.test.ts: the note names the rule.
  • Changeset .changeset/30283-time-relative-object-mismatch.md, minor on @objectstack/lint, BREAKING with what an author does, ADR-0087 not-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

  • Measurement that decided error (on 9afcd6a6). Objects crm_opportunity { amount } and crm_task { priority, close_date }, a start with objectName: 'crm_opportunity' sweeping crm_task. Author time (lint validateStackExpressions + readiness): record.amount > 10 and amount > 10 → no finding at all; record.priority > 1 → unknown field priority on crm_opportunity (error); the same two conditions with no objectName → unknown field amount on crm_task and clean, respectively. Run time (AutomationEngine.execute with the context time-relative-trigger.ts builds: a crm_task row as record, object: 'crm_task'): record.amount > 10 → run fails No such key: amount; amount > 10 → Unknown variable: amount; record.priority > 1 → run succeeds, while the engine's registration pass logs unknown 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) takes timeRelative.object before objectName, and expandDeclaredLookups reads the run's object.
  • Corpus. git grep over examples/, skills/, content/docs/ (outside the generated references) and the MCP skill text: no time-relative start carries an objectName (the showcase showcase_task_due_reminder sweep names none). The dogfood battery below is the executed check.
  • MCP validate_expression's sweepObject advice (packages/mcp/src/skill-md.ts, mcp-http-tools.ts) still says a mismatch of sweepObject and objectName makes 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.
  • The engine's registration advisory pass (flow-registration-checks.ts) still types a time-relative flow's conditions against objectName, logging the misleading unknown field warn 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
  • Build: pnpm --filter '@objectstack/dogfood^...' build (lint included), VERDICT command-exit 0; the seven packages check:dual-build-cjs-loads reads that lie outside that closure, VERDICT command-exit 0.
  • @objectstack/lint on d3580892: pnpm test 8014/8014 (193 files); pnpm typecheck 0 (with check:test-typecheck).
  • pnpm lint (eslint . --no-inline-config) on d3580892: exit 0.
Ablations (each through scripts/ablation-replace.mjs; restored blob == HEAD, `git diff HEAD` empty)
  • A1, the verdict (lint suite imports src): the mismatch condition gated off ('ABLATE-30283' === 'on' &&). 6 red, among them closes 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 row flow-time-relative-object-mismatch is error.
  • A2, the routing guard: resolveFlowTriggerKind(flow) === 'time_relative' && !isArrayRecordTriggered replaced 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/objectstack on d3580892: 57 families (the same list as on b4c229b6), all 57 exit 0 (exit code captured per line before any pipe).
  • Three were red on the first pass and are recorded at their re-run: check-adr-0087-registration (the changeset carried a FROM/TO rewrite beside no-migration-prescription; fixed in d3580892, 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 no dist/; built, then exit 0).
  • --ran reconciliation: "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 test under the lock on b4c229b6 (no shard, no path; d3580892 changes 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

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 7 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via time_relative (literal, a string literal in validateFlowTriggerReadiness))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
  • content/docs/permissions/capabilities.mdx (via time_relative (literal, a string literal in validateFlowTriggerReadiness))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via time_relative (literal, a string literal in validateFlowTriggerReadiness))
  • content/docs/releases/v17/17-5.mdx (via time_relative (literal, a string literal in validateFlowTriggerReadiness))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 38717d34a4d16b44e7b22cb4bccf745ae47086d6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a97bbfd248d1955afd2a1e1732306ce6023386e9 — the merge of head d3580892c48144db564df73810a605a88ed19bb0 into base 38717d34a4d16b44e7b22cb4bccf745ae47086d6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 38717d34a4d16b44e7b22cb4bccf745ae47086d6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d3580892c48144db564df73810a605a88ed19bb0
Local-runs: probe — node scripts/check-adr-0087-registration.mjs --base 8589756a --head HEAD on a detached worktree of the head, which refused to grade for want of an installed tsx; no reading was taken from it, and the gate's verdict in ② is CI's
Scope: the net diff main@8589756a...d3580892 (8 files, +272/−2; the branch's three commits are the change fb38ffb0, the merge b4c229b6, and d3580892, which moves one line of the changeset and nothing else), read whole: packages/lint/src/validate-flow-trigger-readiness.ts (header 4b, the severity paragraph, the FLOW_TIME_RELATIVE_OBJECT_MISMATCH constant, block 1b-iii), its test (the severity-table row and the [#30283] block of seven cases), rule-explanations.ts, authoring-rules.ts, index.ts, validate-expressions.ts and its test, .changeset/30283-time-relative-object-mismatch.md. Against the live tree at the head: packages/spec/src/automation/flow-trigger-kind.ts (resolveFlowTriggerKind: a string record-* token first, then timeRelative != null && typeof === 'object'), packages/services/service-automation/src/plugin-extension-api.ts (deriveTriggerBinding: the array-form record-* pre-check routes to record_change with object: config.objectName; the time_relative arm binds tr.object when it is a string, config.objectName only otherwise), packages/triggers/trigger-schedule/src/time-relative-trigger.ts (engine.find(desc.object, …) L598, { record: row } L657, run context object: desc.object L716; no read of the start node's objectName), packages/services/service-automation/src/flow-registration-checks.ts L455–L462 (types conditions against config.objectName), packages/mcp/src/mcp-http-tools.ts and skill-md.ts (the sweepObject text), the lint helpers isArrayRecordTriggerType and startNodeOf, and the corpus (examples/, skills/, content/docs/ outside references/ and releases/, packages/qa/dogfood/test/fixtures). Card #30283 read whole: body, triage 6650080613, claim 6652306077, os-dev-report 6654371968; dispatch order server-seat2/dispatch-30283.md; PR #30276's record 6649920800 (should-fix 3 is this card). The PR body was read live and is byte-identical to the dev's copy; its three declaration lines are byte-identical to the changeset's. Check-runs on the head, see ③.

① Derived judgments

  1. The predicate is the mismatch and nothing broader. Block 1b-iii fires only when all five hold: typeof tr.object === 'string', typeof config.objectName === 'string', the two differ, resolveFlowTriggerKind(flow) === 'time_relative', and !isArrayRecordTriggered. It sits inside the existing isTimeRelative && start guard, which is the resolver's own timeRelative test character for character, so the resolver condition adds exactly one exclusion: a string record-* token, which the engine binds to the record-change trigger (with objectName). isArrayRecordTriggerType is the engine's pre-check verbatim (Array.isArray and some(startsWith('record-'))), which outranks the resolver in deriveTriggerBinding and also binds objectName. So the rule speaks for exactly the flows the engine hands to the time-relative trigger, and in every one of them the run binds tr.object and never objectName (trigger L598/L657/L716; engine arm reads tr.object first). RIGHT. A descriptor whose object is not a string is left to flow-time-relative-descriptor-invalid (the schema requires object), and that is the one case where the engine arm would fall back to objectName, so no verdict here contradicts a binding. Both startNodeOf and the resolver take the first start node. Nothing new is read from the stack, so an object declared by another package changes nothing.
  2. The measured hole is real. With objectName set, validate-expressions.ts types the flow's conditions against startObject ?? sweepObject (PR 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, record 6649920800 ① item 1), i.e. against objectName, while the run's record is a row of tr.object. A read of a field only objectName declares therefore gets no author-time verdict and faults at run time; a correct read of the swept object's field is refused as unknown. The dev's numbers (No such key: amount, Unknown variable: amount, unknown field priority on crm_opportunity) follow from those two reads of the code and are pinned at author time by closes the measured hole across AUTHORING_COMMANDS and by the runtime publish gate case. The run-time half was a scratch probe the dev deleted; I accept it on the source reads above, which leave no other outcome.
  3. Severity error is the file's own criterion, applied honestly. The header's rule (lint never-fire 族 severity 复审:两条 warning 实感更接近 error(#5496/#5647 dev 报告建议)—— 同批升、单独 PR,不逐条搭车 #5762) is "is THIS STACK enough to know": the two names are on one start node and the binding precedence is a hardcoded read in the engine, so no install changes the answer. The dev does not claim never-fire membership (header 4b and the severity paragraph say so; the id is not in any never-fire list, and none exists outside the header prose), and the fix is lossless (dropping objectName changes no run). This is also Prime Directive Add comprehensive test suite for Zod schema validation #12's shape: refuse at authoring/publish, no consumer shim. RIGHT.
  4. Registration is complete for this family. On main the sibling id flow-time-relative-descriptor-unroutable appears outside its own rule file only in index.ts, rule-explanations.ts and the release-owned CHANGELOG.md; the new id is in the first two, plus authoring-rules.ts (comment) and the validate-expressions.ts note. rule-id-barrel-exports.test.ts reads the barrel from index.ts, so no second ledger. No content/docs page or skills/** file enumerates readiness rule ids (validating-metadata.mdx names flow-trigger-unknown-object for the closure case only), so there is no generated or hand-written mirror to drift.
  5. No false positive in the shipped corpus. Every time-relative start in the tree names no objectName: examples/app-showcase (showcase_task_due_reminder), skills/objectstack-automation/references/examples-flows.md, content/docs/automation/flows.mdx, and packages/qa/dogfood/test/fixtures/schedule-organization-fixture.ts; examples/app-todo/src/flows/task.flow.ts only mentions timeRelative in a comment. The Dogfood Regression Gate check-run on the head is success, and the dev's battery under the lock on b4c229b6 (which differs from the head by one changeset line) was 419 files / 3492 tests green.
  6. The validate-expressions.ts note change is additive and consistent. It names the readiness rule inside the existing sentence; the verdict itself stays in one place. Pinned by one assertion.
  7. Ablations are plausible. A1 (verdict gated off) would red exactly the six cases that expect the finding: the severity row, refuses the start, the positive leg of is silent where the engine binds another trigger, the co-firing half of the descriptor split, closes the measured hole, and the runtime gate case; the two silence cases and the os explain case stay green. A2 (routing guard forced true) reds exactly the record-after-update / array-form case. 6 and 1 match the report.
  8. Nothing in packages/spec, no governed path, no ui/ consumer, no model identifier in the title, body, changeset or code comments (grep over the diff). The PR is a draft on main, head repo is the base repo, no assignee, no requested reviewer.

② Semver level

  • Changeset: '@objectstack/lint': minor. The diff moves exactly that one published package (no private flag in its manifest), the kind is narrowed, so at least minor on it per the Post-Task Checklist level rule. CORRECT.
  • Declarations: line-initial Clause-②: yes (narrowing) — …, Metadata-protocol: narrowed, Skills-docs: none — …, byte-identical in the PR body and the changeset; (narrowing) carries the BREAKING banner. The Skills-docs: line is TRUE on the reads in ① item 5: skills/** and content/docs/** teach the sweep with timeRelative.object alone, which this rule accepts, and no page enumerates the rule family. CORRECT.
  • ADR-0087 disposition: exactly one marker, adr-0087: not-required (no-migration-prescription) <why>, in the gate's not-required (<category>) <why> grammar with a category in CATEGORIES. The "What an author does" paragraph is prose (no arrow, no table, no FROM/TO label), which is the form the gate's R2 arm demands beside this category; the first push's rewrite form was refused by the dev's own local run and corrected in d3580892. The category is honest: no schema key moves, the narrowing is the accept set of one combination of two existing keys, and no rewrite preserves intent because the author meant one of two objects. The Lint · docs & code guards and Lint · CI, release & engine gates check-runs that carry the registration gate are success on the head. CORRECT.
  • Footer: the PR body carries the session-URL form.

③ Boundary flags

From os-dev-report 6654371968:

  • open_questions: [].
  • Deviation 1 (changeset prose rewrite): the gate's R2 arm, handled; see ②. CLOSED.
  • Deviation 2 (scratch probes deleted, readings in Acceptance notes): the author-time half is pinned in the suite; the run-time half rests on the source reads in ① item 2. ACCEPTED.
  • Deviation 3 (assign/review step skipped, model-free trailers): reporting notes.
  • Out-of-scope 1 (MCP validate_expression text): see ④ should-fix 1, for the seat to file.
  • Out-of-scope 2 (engine registration advisory pass): see ④ should-fix 2, for the seat to file or record.
  • Security findings: none; none found (the rule compares two strings for equality).

Flags of this review's own:

④ Design judgment

Does the narrowing take a declared capability away, or make the spec's own contract real? It makes it real. The flow schema admits objectName beside a timeRelative descriptor, and the engine, trigger and spec resolver all agree that a time-relative run binds timeRelative.object and nothing else; objectName on such a start is a declaration no part of the run honours while the author-time checks type against it. Refusing the contradiction at the one key that makes it so, with both names and a lossless fix in the verdict, is the contract-first answer the previous record asked for. Attempts to break it:

  • objectName equals timeRelative.object, or is absent: silent; pinned. Holds.
  • String record-* token beside a descriptor: the resolver answers record_change; rule silent; the engine binds objectName and never sweeps. Pinned (record-after-update). Holds.
  • Array-form record-* token beside a descriptor: the engine's pre-check routes it to record-change; the rule is silent through !isArrayRecordTriggered, and flow-trigger-unknown-event already refuses the shape. Pinned. Holds.
  • timeRelative.object not a string: sweptObject undefined, rule silent; flow-time-relative-descriptor-invalid fires and the bind throws. Pinned (object: 7). Holds.
  • Bad window beside a string object: both rules, two keys. Pinned. Holds.
  • triggerType: 'schedule' string beside a descriptor: resolver answers time_relative; fires. Pinned. Holds.
  • Swept object declared by another package: the rule reads no object set, so unchanged; flow-trigger-unknown-object keeps its hedge. Holds.
  • Runtime publish gate (per-write snapshot): the rule reads only the written flow; pinned as error on the flow alone, silent without objectName. A stored flow that names two objects keeps being served and is refused only on its own next publish, as the explanation says. Holds.

Findings:

  • should-fix 1 (follow-up card, packages/mcp, not this PR). validate_expression's tool description, its sweepObject input text (mcp-http-tools.ts L1104–L1151) and the skill text (skill-md.ts L278–L279) still advise passing sweepObject "when it sweeps another object than objectName (then a mismatch warns)". That stays true of the expression verdict, but after this PR the only flows that take that branch are ones the publish door refuses, so the text steers an agent toward tuning a condition for a start it cannot publish. One sentence naming the readiness refusal, or retiring the other-object arm of the ledger's third input, is the seat's call; outside this card's surface.
  • should-fix 2 (record or card). flow-registration-checks.ts L455–L462 still types a time-relative flow's conditions against config.objectName, so a flow stored before this rule keeps getting the misleading unknown field warn at registration. Reachable only for stored rows now; aligning the advisory pass with the binding's object is a small server card.
  • No must-fix. The predicate is the mismatch exactly, with the engine's two routing exclusions; the severity follows the file's criterion; the declarations, level and ADR-0087 marker are well-formed and true; the corpus carries no refused shape; each pin has the ablation that would catch its loss.

Implemented-by: session_01Utp6CXGZhZVasbhiGEdpZk
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

席位核对 — ACCEPT · domain:server#2 · head d3580892c48144db564df73810a605a88ed19bb0

  • 形态: draft, author objectstack-fleet[bot] (relay pr_create), no assignee, no review request; 8 files, +272 −2; check-governed-merges --pr 30393: NOT governed (0 of 8 paths), 274 lines ≤ 3000.
  • 契约复审: CONTRACT_REVIEW_TIER, narrowing face, PASS 6654534464 on this head. The predicate is exactly the mismatch on flows the engine binds to the time-relative trigger (both record-* routings excluded); severity error per the file's own criterion; declarations line-initial and byte-identical in body and changeset.
  • Declarations: Clause-②: yes (narrowing), Metadata-protocol: narrowed, Skills-docs: none — … (true: no skill or doc enumerates readiness rule ids; no corpus start carries a second object); minor for @objectstack/lint; BREAKING banner, one ADR-0087 not-required (no-migration-prescription) marker.
  • Dev readings: lint 8014/8014, typecheck 0, derived gates 57/57 (--ran 0 NOT-MEASURED), pnpm lint 0, full dogfood 419 files / 3492 passed on b4c229b6 (head differs by one changeset line); ablations A1 (6 red) and A2 (1 red) restored.
  • 门禁 check-runs: lint, changeset, PR-body, queue-guard and dogfood rows green; build, type-check and test shards in progress at ACCEPT; auto-merge waits on the required contexts.
  • 范围外去向: Time-relative flows: the registration warning and the MCP sweepObject advice still treat the start node's objectName as the condition object #30396 (registration advisory and MCP sweepObject text still treat objectName as the condition object). Cosmetic doubled backtick in the body's Verification line: not fixed.
  • 落地条款: Fixes flow readiness: a time-relative start whose objectName differs from timeRelative.object declares an object its run never binds #30283. Ordinary queue landing by this seat: pr_ready + automerge_enable on this head.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

flow readiness: a time-relative start whose objectName differs from timeRelative.object declares an object its run never binds

1 participant