Skip to content

fix(service-automation,mcp): type a time-relative flow's conditions against the object its sweep scans - #30453

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#2 · os-bill · session_01Utp6CbRQNg8jRbhpBF2rFY

Fixes #30396

Clause-②: no — a logged registration advisory (never thrown, never a refusal) now types a time-relative flow's conditions against the object the run binds, and the MCP validate_expression prose stops presenting a start the publish door refuses as a supported shape; the tool's input schema and verdicts are unchanged.
Metadata-protocol: none

维护者速读

  • 改了什么:时间相对(定时扫描)流程注册时的字段提示,现在按真正被扫描的对象(timeRelative.object)检查条件,不再按开始节点的 objectName;MCP validate_expression 的说明和 SKILL.md 改为「把被扫描对象作为 objectName 传入,开始节点写别的对象会在发布时被拦(flow-time-relative-object-mismatch)」。
  • 为什么改:实测旧行为两头都错:发布规则上线前存下的两对象流程,注册日志对被扫描对象的正确字段报 unknown field,对运行时必失败的字段一声不吭;只写 timeRelative.object 不写 objectName 的流程完全不做字段检查。MCP 文案还在教 agent 写一种现在发布会被拒的流程。
  • 风险与回滚:注册这一步只是日志提示,从不抛错、不拦注册;对象取自触发绑定本身(deriveTriggerBinding),与运行时同一答案;非时间相对流程不变;MCP 只改文字,输入 schema 与判决不变。回滚即撤 PR。
  • 先看:packages/services/service-automation/src/flow-registration-checks.ts 的 validateFlowExpressions 取对象那几行;engine.ts 宿主里新增的一行 deriveTriggerBinding;packages/mcp/src/mcp-http-tools.ts 的 sweepObject 说明。
  • 两分钟验证:pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/flow-registration-checks.test.ts,预期 4 个用例通过。
  • 需要你做的:无,席位落地。

What changes

  • Registration advisory (flow-registration-checks.ts, validateFlowExpressions). The schema-aware pass (Formula guardrail: cel-js arithmetic silently returns null (double × int + bare identifiers) #1928: logged unknown field / typo / type-mismatch warnings, never thrown) took its object from the start node's config.objectName. It now takes it from the flow's trigger binding when that binding is time_relative: binding.object, which deriveTriggerBinding resolves as timeRelative.object when it is a string, config.objectName only otherwise — the object whose row the sweep hands each run. The rule is reused, not restated: the pass calls the engine's own deriveTriggerBinding (the same method validateApiTriggerSecret already calls at registration). Every other flow keeps config.objectName. The binding is derived only on the advisory pass with a resolver wired; the stored pass (advisory: false) never reads the hint.
  • Engine host (engine.ts). One line: the validateFlowExpressions host now passes deriveTriggerBinding, plus a sentence in that method's docblock. Outside the claim's expected file list (not a banned path); see Acceptance notes.
  • MCP text (mcp-http-tools.ts, skill-md.ts). validate_expression's description, its sweepObject input text and the SKILL.md line no longer advise passing a sweepObject other than objectName. They say a time-relative flow's conditions read rows of the swept object, so pass it as objectName, and that a start whose objectName names another object is refused at publish (flow-time-relative-object-mismatch). The sweepObject text keeps what the tool answers when the two differ (the gate's advisory type-mismatch warning). No schema change: the other-object arm still mirrors what @objectstack/lint grades for such a flow's expressions, so retiring it would move the tool off the gate.
  • Tests (flow-registration-checks.test.ts, new). On a stored two-object flow (start objectName: crm_account, sweep crm_contract): a field of the swept object passes; a field only crm_account has warns unknown field. A one-object flow is unchanged (clean, and a typo still warns). A sweep-only flow (no objectName) is typed against the swept object.
  • Changeset .changeset/30396-time-relative-condition-object.md: patch on @objectstack/service-automation and @objectstack/mcp.

Acceptance notes

  • Measurement (on 09720e2c, before the fix). The four tests above against the old code: 3 red. The two-object flow logged [flow 'renewal_alert'] node 'check' (decision) condition: unknown field \renewal_status` on `crm_account`for the swept object's real field, and nothing forrecord.industry(a field that fails every run); the sweep-only flow logged nothing forrecord.industry` because no object was typed at all. The one-object case was already green.
  • Behaviour widening, advisory only. A sweep-only time-relative flow (start names no objectName) now gets the schema advisory where it got none. It is a log line at registration; nothing is refused.
  • File surface. engine.ts is outside the claim's expected list: validateFlowExpressions receives its host members from the engine (RegistrationCheckHost Pick), so reusing the binding means one wiring line there. The alternatives were a third copy of the binding rule beside deriveTriggerBinding and @objectstack/lint's, which the order rules out, or calling deriveTriggerBinding with a stand-in host.
  • Text pins. The package pins no tool-description text (skill-md-surface-guard.test.ts checks tool names only), and hint text is not pinned by convention; no text pin added.
  • Restatements elsewhere. git grep sweepObject over skills/, content/, docs/, apps/, ui/: no hit; no skill page restates the MCP advice.

Verification

Readings on head 9fe18676 (fa901c10 the change, 35374475 merged with main at fdd06415, 9fe18676 a comment-only edit) unless named.

Build, typecheck, tests
  • Build: pnpm --filter '@objectstack/service-automation^...' --filter '@objectstack/mcp^...' build under the lock, VERDICT command-exit 0; later pnpm --filter '@objectstack/dogfood^...' build on 35374475, exit 0.
  • @objectstack/service-automation: pnpm test Test Files 264/264, Tests 3418/3418 (on fa901c10); pnpm typecheck 0.
  • @objectstack/mcp: pnpm test Test Files 56/56, Tests 678/678 (on fa901c10); pnpm typecheck 0.
  • pnpm lint (eslint . --no-inline-config) on 9fe18676: exit 0.
Ablation (scripts/ablation-replace.mjs; the suite imports `src`, no dist leg)
  • The binding read gated off, so the pass types against config.objectName again (const objectName = 'ABLATION_30396' && bound?.triggerType === 'never'): mutation landed (anchor 1→0, blob c53973cf → 488218fa); 3 red, among them a stored two-object flow: a field of the swept object passes with + "[flow 'renewal_alert'] node 'check' (decision) condition: unknown field \renewal_status` on `crm_account`"; restored, blob == HEAD, git diff HEAD` empty.
Derived gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 9fe18676: 62 families (the same list as on 35374475), all 62 exit 0 (exit code captured per line before any pipe).
  • One was not measured on the first pass and is recorded at its re-run: check:dual-build-cjs-loads (PREREQUISITE NOT MET: seven packages outside the dogfood closure had no dist/; built under the lock, then exit 0). check-plugin-teardown-shape --self-test's pinned fixture commit was fetched into this shallow clone before the run.
  • --ran reconciliation: "62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero …)".
Dogfood (full battery, run as the order asks; no narrowing arm)
  • pnpm --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test under the lock, started on 35374475 (no shard, no path): Test Files 419 passed, 3 skipped (422); Tests 3492 passed, 36 skipped (3528); VERDICT command-exit 0. 9fe18676 changes only a code comment in flow-registration-checks.ts, written while that build was running.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6CbRQNg8jRbhpBF2rFY

…gainst the swept object

The registration advisory read the start node's objectName; the run binds
timeRelative.object. Read it off deriveTriggerBinding instead, and reword the
validate_expression sweepObject advice to name the publish refusal.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbRQNg8jRbhpBF2rFY
@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 2 package(s): @objectstack/mcp, @objectstack/service-automation, touching 7 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/mcp/src/skill-md.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 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 validateFlowExpressions))
  • content/docs/permissions/capabilities.mdx (via time_relative (literal, a string literal in validateFlowExpressions))

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

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class), time_relative (literal, a string literal in validateFlowExpressions))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via time_relative (literal, a string literal in validateFlowExpressions))
  • content/docs/releases/v17/17-6.mdx (via AutomationEngine (symbol, a top-level class))

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/mcp/src/skill-md.ts) — pages documenting those are invisible to this run
  • 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 — 19 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 df4c06e6b165bb0a30eb86e4ace6258932f69d8a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1769ef19ed0cecfd179beb01bab0cb6f742be532 — the merge of head 9fe18676371e491a5b842d595750eee0a645cf53 into base df4c06e6b165bb0a30eb86e4ace6258932f69d8a, 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 1769ef19ed0cecfd179beb01bab0cb6f742be532 && git checkout 1769ef19ed0cecfd179beb01bab0cb6f742be532
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df4c06e6b165bb0a30eb86e4ace6258932f69d8a 9fe18676371e491a5b842d595750eee0a645cf53 && git checkout -B drift-repro df4c06e6b165bb0a30eb86e4ace6258932f69d8a && git merge --no-ff 9fe18676371e491a5b842d595750eee0a645cf53

node scripts/docs-audit/affected-docs.mjs --json df4c06e6b165bb0a30eb86e4ace6258932f69d8a

⚠️ 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 df4c06e6b165bb0a30eb86e4ace6258932f69d8a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

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

  • 形态: draft, author objectstack-fleet[bot] (relay pr_create), no assignee, no review request; 6 files, +131 −16 against merge base fdd06415; check-governed-merges --pr 30453: NOT governed (0 of 6 paths), 147 lines ≤ 3000. Title has no !.
  • Review face: TIER_DEFAULT (declaration face: Clause-②: no, Metadata-protocol: none; no schema, no governed path, no narrowing), written here by the seat.
  • Seat read (diff against fdd06415): validateFlowExpressions now reads the bound object off deriveTriggerBinding — binding.object when triggerType === 'time_relative', the start node's config.objectName otherwise — the same answer the run and activateFlowTrigger use, not a second reading of the start node. Derived only when advisory && objectSchemaResolver; the stored pass (advisory: false) never reads the hint, so it is untouched. deriveTriggerBinding already runs on the same registration path (validateApiTriggerSecret), so no new throw path enters registration; the warnings stay logged, never thrown, and the fatal set is unchanged. Every non-time-relative flow is typed exactly as before.
  • Deviation accepted: engine.ts +4 (one host wiring line deriveTriggerBinding in the validateFlowExpressions host, three docblock lines). Needed because that host is built in engine.ts; outside the ordered file list and outside the forbidden set. Ordering with seat 3's engine.ts move (refactor: sweep the #4918 as any sites in engine.ts's six erasure-pinned methods so the codemod can move them (about 550 lines) #30055) agreed: this PR lands first; refactor: sweep the #4918 as any sites in engine.ts's six erasure-pinned methods so the codemod can move them (about 550 lines) #30055 moves six other methods and does not touch validateFlowExpressions.
  • MCP text: validate_expression description, the sweepObject input text and skill-md.ts no longer present "sweepObject other than objectName" as a supported shape; they name the publish refusal flow-time-relative-object-mismatch (PR fix(lint)!: refuse a time-relative start whose objectName is not the object it sweeps #30393). Inputs and verdicts unchanged (schema text only). True against the head.
  • Pins: four cases on the real AutomationEngine.registerFlow — two-object stored flow (swept-object field passes; start-object-only field warns), one-object flow unchanged (typo still warns), sweep-only flow now typed. Dev ablation: 3 red, restored.
  • Declarations: Clause-②: no, Metadata-protocol: none line-initial in PR body and changeset; patch ×2 (@objectstack/service-automation, @objectstack/mcp).
  • Dev readings (9fe18676): service-automation 3418/3418, mcp 678/678, typecheck and pnpm lint green, derived gates 62/62, full dogfood 419 files / 3492 passed. os-dev-report 6657502054.
  • 门禁 check-runs: in progress at ACCEPT; auto-merge waits on the required contexts. main moved 19 commits past the merge base, none under packages/services/service-automation or packages/mcp; the merge queue tests the merged result.
  • 落地条款: Fixes Time-relative flows: the registration warning and the MCP sweepObject advice still treat the start node's objectName as the condition object #30396. Ordinary queue landing by this seat: pr_ready + automerge_enable on this head.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 12:06
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 12:06
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 6c4c43a Nov 5, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-18o18j branch November 5, 2026 12:46
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.

Time-relative flows: the registration warning and the MCP sweepObject advice still treat the start node's objectName as the condition object

1 participant