Skip to content

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

Open
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-18o18j
Open

objectstack-fleet[bot] wants to merge 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 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

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

  • 改了什么: 按时间触发的流程(time-relative),注册时的字段检查改为对照它实际扫描的对象(timeRelative.object),不再对照开始节点上的 objectName;MCP validate_expression 的说明和 SKILL.md 同步改为"把被扫描的对象作为 objectName 传入"。
  • 为什么改: 原来两对象的旧流程会对正确字段报 unknown field、对运行时必失败的字段反而不报,只写了 timeRelative.object 的流程完全不检查(Time-relative flows: the registration warning and the MCP sweepObject advice still treat the start node's objectName as the condition object #30396,PR fix(lint)!: refuse a time-relative start whose objectName is not the object it sweeps #30393 复审的跟进项)。
  • 风险与回滚: 只影响注册时记在日志里的提示(从不抛错),以及 MCP 工具的说明文字;工具的输入和判定不变。回滚即撤本 PR。
  • 先看: packages/services/service-automation/src/flow-registration-checks.ts validateFlowExpressions 里取对象的那几行。
  • 两分钟验证: pnpm --filter @objectstack/service-automation exec vitest run src/flow-registration-checks.test.ts。
  • 需要您做的: 无,席位经合并队列落地(非受管面,147 行,非 BREAKING)。

席位核对 — 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

This branch has not been deployed

No deployments
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

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