Skip to content

fix(spec): name timeRelative.object in the time-relative arm of the flow type-mismatch upgrade entry - #30637

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-30279-time-relative-entry-surface
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-30279-time-relative-entry-surface

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Fixes #30279

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:只改升级指南条目 flow-record-read-and-decision-type-mismatch-refused 的两段文字(surface 与 reason 中对应的一句),写明时间相对流程的条件按被扫描对象 config.timeRelative.object 做类型检查:开始节点不写 objectName 时按它,开始节点的 objectName 就是被扫描对象时按 objectName。
  • 为什么改:发布门(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 之后)已经这样检查并拒收,条目原文只说“扫描开始节点的 objectName”,比实际拒收范围窄,os migrate meta 与升级指南会把作者引向不完整的说明。
  • 风险与回滚:纯文字更正,条目 id、kind、replacement、acceptanceCriteria 与任何判决都不变;回滚即撤 PR。
  • 先看:packages/spec/src/migrations/entries/semantic/18.flow-record-read-and-decision-type-mismatch-refused.ts(surface 末段与 reason 中 time-relative 一句);packages/spec/src/migrations/migrations.test.ts 末尾新增的断言。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/migrations/migrations.test.ts -t "names the object a time-relative flow",预期 1 个用例通过。
  • 需要你做的:无,席位落地。

What changes

The entry's surface and one reason sentence, written from the gate code (packages/lint/src/validate-expressions.ts, the flow walk: objectName = startObject ?? sweepObject, where sweepObject is config.timeRelative.object on a time-relative flow; the run-fails outcome is declared when sweepsTypedObject: sweepObject !== undefined && sweepObject === objectName). Every other field (id, kind, replacement, acceptanceCriteria) is byte-identical.

surface, the time-relative tail

  • FROM: …; and the same conditions, bare name included, of a time-relative flow (start config.timeRelative) sweeping the start node's objectName
  • TO: …; and the same conditions, bare name included, of a time-relative flow (start config.timeRelative) sweeping config.timeRelative.object, typed against that object when the start node names no objectName and against the start node's objectName when that is the swept object

reason, the time-relative sentence

  • FROM: A time-relative sweep launches each run with the swept row as its record, the same total row of the same object, so its conditions get the same refusal where 18.3.0 warned.
  • TO: A time-relative sweep launches each run with a row of the object it sweeps (config.timeRelative.object) as its record, the same total row of one object, so its conditions are typed against that object when the start node names no objectName, and against the start node's objectName when that is the swept object, and get the same refusal: where 18.3.0 warned on a sweep of the start node's objectName, and where a start that names no objectName got no verdict at all.

Test. No test pinned the entry's text; a new top-level describe in packages/spec/src/migrations/migrations.test.ts asserts the surface matches sweeping config.timeRelative.object.

Changeset. .changeset/30279-time-relative-entry-surface.md, @objectstack/spec patch, with both declaration lines.

Verification

All readings on head bc0dd484 unless named.

Build, tests, typecheck
  • pnpm --filter @objectstack/spec build (under the verify lock): VERDICT command-exit 0.
  • pnpm --filter @objectstack/spec check:generated: exit 0, "All 14 generated artifacts are up to date"; no artifact carries the entry text, nothing regenerated.
  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/migrations/: 5 files, 198/198.
  • The whole @objectstack/spec pnpm test: NOT MEASURED locally, reason: the shared verify lock answered queue-timeout (exit 99) twice behind a sibling's dogfood run (held 1094 s at the second try). Declared narrowing: the migrations directory above is the suite that reads the entry; CI Test Core runs the whole package.
  • pnpm --filter @objectstack/spec typecheck (under the lock): VERDICT command-exit 0.
  • eslint --no-inline-config over the entry file and the test file: exit 0.
Ablation (scripts/ablation-replace.mjs; restored blob == HEAD, git diff HEAD empty)
  • Mutation: the surface's new three-line tail set back to the FROM text (sweeping the start node's objectName). On disk: FROM phrase count 0 → 1, sweeping config.timeRelative.object 1 → 0, blob f0920514 → 01fd0df2; gen:migration-registry rerun, FROM phrase present in the generated registry.ts (1).
  • Result: 1 red, the surface names config.timeRelative.object as the swept object: AssertionError: expected 'automation.Flow of a record-triggered…' to match /sweeping config\.timeRelative\.object/ (Tests 1 failed, 157 skipped).
  • Restore: blob after restore == HEAD (f0920514), git diff HEAD empty; registry regenerated; migrations.test.ts 158/158.
Derived gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on bc0dd484: 80 families; all 80 run, exit codes captured per command before any pipe; 77 exit 0 on the first pass. Three answered exit 3 (PREREQUISITE NOT MET: an unbuilt @objectstack/formula, unbuilt workspace dist/, unbuilt objectql dist/) on the first pass: check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure; re-run after the closure build check:type-check-debt performed, each exit 0.
  • --ran reconciliation with coded exit lines: "80 derived famil(ies) accounted for — 80 run, 0 NOT-MEASURED (a DERIVED zero — all 80 recorded an exit code and none of them is 3)".
  • Named in the dispatch and inside the derived set: pnpm check:adr-0087-registration exit 0 ("this PR adds no declared-breaking changeset"), pnpm check:doc-authoring exit 0.

Acceptance notes

  • The code agrees with the card. The gate types the conditions against config.timeRelative.object when the start names no objectName, and declares the run-fails outcome only when timeRelative.object equals the typed object, so a start naming another object keeps the warning; the TO sentences say exactly that.
  • A start that names another object. Since the flow-time-relative-object-mismatch readiness rule landed, a time-relative start whose objectName is not timeRelative.object is refused by that rule; this entry's reason still says "Any other flow keeps a warning", which stays true of the expression verdict itself. Noted, not changed.
  • No tracker id in a runtime string: the entry's surface and reason carry none; the test comment carries none either.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P

…low type-mismatch upgrade entry

The ADR-0087 semantic entry flow-record-read-and-decision-type-mismatch-refused
said its refusal covers a time-relative flow sweeping the start node's
objectName. The author-time gate types a time-relative flow's conditions
against config.timeRelative.object when the start names no objectName, and
against the start's objectName when that is the swept object; the surface and
the matching reason sentence now say so. A migrations pin asserts the surface
names config.timeRelative.object.

Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P
@github-actions github-actions Bot added size/s 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

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 170 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 fabd25958f9908c51db18057ac3c4e73c4994edf → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bc0dd4845624790a495f8d8c7fd917bc67401ebb
Local-runs: none
Scope: packages/spec/src/migrations/entries/semantic/18.flow-record-read-and-decision-type-mismatch-refused.ts + packages/lint/src/validate-expressions.ts (the flow walk, objectName = startObject ?? sweepObject, sweepsTypedObject, faultOutcomeOf), packages/lint/src/validate-flow-trigger-readiness.ts (flow-time-relative-object-mismatch), packages/spec/src/automation/time-relative-trigger.zod.ts (object required), packages/spec/src/automation/flow-trigger-kind.ts (resolveFlowTriggerKind), packages/triggers/trigger-schedule/src/time-relative-trigger.ts ({ record: row }), packages/spec/src/migrations/types.ts (SemanticMigration), docs/adr/0087-metadata-protocol-upgrade-contract.md (D3), packages/spec/scripts/build-schemas.ts (check b, exact-key registration), PR #30276's diff at cfb602c4 and .changeset/30026-time-relative-sweep-typing.md, .changeset/30283-time-relative-object-mismatch.md at 54b491ad

Inputs: card #30279 (body; comments 6650100066 triage, 6665805154 claim, 6667452302 os-dev-report), PR #30637 (body, 3-file list, net diff origin/main...bc0dd484, merge-base 25fea77a), the head's check-runs. The rest of the diff (.changeset/30279-time-relative-entry-surface.md, packages/spec/src/migrations/migrations.test.ts) is the lane seat's ACCEPT at TIER_DEFAULT; it is read here as evidence for ② and ④, not reviewed.

① Derived judgments

  • No accept set moves — right. The diff touches two string literals of one shipped SemanticMigration (surface, one reason sentence); id, replacement and acceptanceCriteria are byte-identical on the diff (hunks at lines 10-15 and 25-33 only), the entry declares no conversionIds or relevance before or after, and it stays a semantic entry of major 18 (the entries/semantic/18. path the generator classifies by; SemanticMigration itself has no kind field, so the PR body's "kind" can only mean that). No gate reads the entry's prose as a machine fact: build-schemas.ts check (b) registers a retirement by EXACT key in RETIRED_KEYS_BY_MAJOR (the leaf-vs-surface match was retired at build-schemas.ts 检查 (b) 用叶名匹配 conversion surface —— 无关簇的 .type 就能让一个 tombstone 冒充「已登记迁移」 #4659), and SemanticRelevance is "never a match against the prose of surface" (types.ts). So no author-time verdict, publish-door verdict or chain result changes.
  • Public surface — right. @objectstack/spec publishes the entry through the generated registry, os migrate meta, spec-changes.json and the upgrade guide; what every one of them shows moves by exactly these two strings. The registry (packages/spec/src/migrations/registry.ts) is gitignored and rebuilt on install and build, so no tracked generated artifact carries the text; git grep of the FROM phrase on origin/main hits only the entry itself and the 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 changeset's prose. The dev's check:generated reading (14 artifacts, nothing regenerated) agrees with the tree.
  • The text now describes the gate — right. validate-expressions.ts 2736-2740: objectName = startObject ?? sweepObject, with sweepObject read from config.timeRelative.object on a time_relative flow; 2857-2861: sweepsTypedObject: sweepObject !== undefined && sweepObject === objectName; faultOutcomeOf 1353-1355 declares run-fails for time_relative only under sweepsTypedObject. That is exactly "typed against that object when the start node names no objectName and against the start node's objectName when that is the swept object": the two cases where sweepObject === objectName are startObject === undefined and startObject === sweepObject. The object key is required in TimeRelativeTriggerSchema (.string().regex(MACHINE_NAME), no .optional()), so "sweeping config.timeRelative.object" names the one object every sweep has. time-relative-trigger.ts:657 launches each run with { record: row }, the swept row, as the reason says.
  • Tracker ids in runtime strings — none. The new surface and reason text carries no #NNNN; the only ids in the PR are in the changeset body and the PR body, which do not ship as runtime strings.

② Semver level

  • .changeset/30279-time-relative-entry-surface.md: '@objectstack/spec': patch, with Clause-②: no and Metadata-protocol: none as line-initial declarations in the body; the PR body carries the same two lines. AGENTS.md Post-Task Checklist step 3: a bug fix in a released package takes patch, never none; Metadata-protocol: none is right (no key, accepted value or meaning an author writes changes; what moves is explanatory text); Clause-②: no is right (no accept-set move at any gate, per ①). No ! in the PR title, no BREAKING banner, no ADR-0087 marker: none is owed on a non-breaking changeset, and check:adr-0087-registration ("this PR adds no declared-breaking changeset") agrees. The changeset body states the FROM phrase and that id, kind, replacement and acceptance criteria are unchanged, which is what the diff publishes. Matches.
  • Clause-② line: no. Right.

③ Boundary flags

  • Dev note / out_of_scope_findings — "Any other flow keeps a warning" left as is, now that flow-time-relative-object-mismatch refuses a time-relative start whose objectName is not timeRelative.object. Answered: the sentence stays true of the verdict this entry governs, and the entry is the right place for no more. The sentence is about the type-mismatch verdict: on a sweep of another object the expression gate serves type-mismatch-run-may-fail as a warning (faultOutcomeOf returns undefined, 1353-1356) plus the 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 explanatory warning at config.objectName (3283-3300), and nothing in the diff or on main turns that into a refusal. The refusal of such a start is a different rule with its own id, grade (severity: 'error', validate-flow-trigger-readiness.ts:741), os explain text, and a changeset whose ADR-0087 disposition (54b491ad, not-required (no-migration-prescription)) states in writing that this entry "prescribes a rewrite of a condition, not of the start node" and that no ledger entry covers the start-node combination. An entry sentence saying the flow is refused for another reason would be an erratum about a rule it does not govern. Holds; no finding.
  • open_questions: [] — nothing to answer.
  • PR Acceptance notes, "a start that names another object keeps the warning" — confirmed against faultOutcomeOf (above).
  • PR Acceptance notes, "no tracker id in a runtime string" — confirmed by reading the two strings on the head.
  • Dev's whole-package pnpm test NOT MEASURED locally (verify-lock queue timeout) — declared narrowing to src/migrations/ (the suite that reads the entry); CI Test Core covers the package. The head's check-runs, read after every one completed: 64 runs, 55 success, 9 skipped (UI Published Artifacts, Dogfood Verify CLI, Build Docs, the dogfood shard matrix, Temporal Conformance, Packed-tarball smoke, the schedule gate, Lint UI, all opt-in or path-gated), 0 failure; Test Core 8/8 shards, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Lint · PM dispatch-gates and Lint · docs & code guards among the successes. Closed by the gate.

④ Design judgment

packages/spec/src/migrations/entries/semantic/18.flow-record-read-and-decision-type-mismatch-refused.ts, against ADR-0087 D3, SemanticMigration and the gate it describes:

  • Against the gate (no more, no less). Each clause of the new surface tail and reason sentence maps to one line of validate-expressions.ts: "sweeping config.timeRelative.object" is sweepObject (2739); "typed against that object when the start node names no objectName" is startObject ?? sweepObject with startObject === undefined (2740); "against the start node's objectName when that is the swept object" is sweepObject === objectName with startObject set (2860); "get the same refusal" is run-fails (1353-1355) under the -run-fails code (1334). The reason's history clause is right on both arms: "where 18.3.0 warned on a sweep of the start node's objectName" is the shipped text's own claim, and "where a start that names no objectName got no verdict at all" is what the 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 changeset's Before paragraph records (no type verdict at all; 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 made sweepsTypedObject require sweepObject !== undefined instead of typeof sweep.object !== 'string'). Nothing in the text claims more than the gate does: it does not say a sweep of another object is refused by this verdict, and it does not say such a flow publishes. Holds.
  • A start that names no objectName: replacement and acceptanceCriteria still hold. The replacement (the text's length via size(record.name), a number field for a value, double(...) on numeric text, record.active == true for a boolean) rewrites the condition, which is the same condition whichever object it is typed against. The acceptance criteria name "a record-triggered or time-relative flow's condition, decision branch or record.FIELD read" and the type-mismatch-run-fails code: a flow whose start names no objectName is a time-relative flow, gets that code (1334), and publishes through publish-drafts once the condition compares like with like; the readiness rule does not fire on it (namedObject !== undefined is false, validate-flow-trigger-readiness.ts:735). The "create on POST /api/v1/data/OBJECT that should start the flow" sentence is the record-triggered arm's and was already there. Holds.
  • Against ADR-0087 D3 and SemanticMigration. D3's structured TODO is "(surface, reason, acceptance criteria)", the reason being the one load-bearing prose field; the entry keeps all four fields and the stable kebab-case id. The surface head (automation.Flow of ...) is unchanged, so the ' / '-split clause vocabulary the check-mode fixture reads (minRegisteredMajorForLeaf) sees the same clauses. The entry stays on major 18 in the semantic registry. Holds.
  • Trigger-kind edge. A start carrying a record-* triggerType and a timeRelative descriptor resolves to record_change first (flow-trigger-kind.ts), so it is covered by the entry's record-triggered arm and never by the time-relative one; the readiness rule's !isArrayRecordTriggered exemption agrees. The text needs no clause for it.
  • should-fix — packages/spec/src/migrations/migrations.test.ts:814. The new pin matches only /sweeping config\.timeRelative\.object/. The correction's substance is the two typing clauses that follow ("typed against that object when the start node names no objectName and against the start node's objectName when that is the swept object"); a regression that keeps the first six words and drops the clauses passes this pin, and the ablation the dev ran mutates the whole tail at once, so it does not show the gap. One more toMatch on names no objectName (or on the reason sentence) closes it. Not a FAIL: the entry text on the head is right, the pin is a guard on a future edit.

No must-fix.

Implemented-by: claude/issue-30279-time-relative-entry-surface
Reviewed-by: session_01L6bhgVugtCCwBs98FeR27P

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

维护者速读

  • 改了什么:升级指南条目 flow-record-read-and-decision-type-mismatch-refused 的两句说明改准了。原来说定时(time-relative)流程的条件按开始节点的 objectName 判类型;现在写明按被扫描的 config.timeRelative.object 判,开始节点没写 objectName 时也一样。只改文字,判定不变。
  • 为什么改: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 之后,门禁在开始节点不写 objectName 时也按扫描对象判类型、照样拒绝,但条目还只提开始节点,看 os migrate meta 输出的人会以为这种流程不受影响。
  • 风险与回滚:只改条目里 surface 和 reason 两个字符串,id、替换写法和验收标准逐字不变,没有任何接受或拒绝的变化。回滚即 revert 本 PR。
  • 先看:packages/spec/src/migrations/entries/semantic/18.flow-record-read-and-decision-type-mismatch-refused.ts → packages/spec/src/migrations/migrations.test.ts 末尾新增的 describe。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/migrations/migrations.test.ts,预期全过;把 surface 改回原来那句后 1 条转红。
  • 需要你做的:无,席位落地。
  • 席位意见:接受。契约面达档复核 PASS(comment 6667973654,Head-sha bc0dd484),没有 must-fix;一条 should-fix(pin 只钉了前半句)不采纳,理由见第 5 项。

席位核对

ACCEPT · head bc0dd4845624790a495f8d8c7fd917bc67401ebb · 2026-11-05T20:06Z

  1. 形态:draft,base main,正文 Fixes #30279,全文只有这一个闭合关键词。3 个文件(条目、migrations.test.ts、changeset)都在认领文件面内,门禁本身(packages/lint/**、packages/services/**)、skills/**、docs/adr/**、.claude/**、AGENTS.md 都没碰,没有 content/docs/releases/。
  2. 前提实测:在 origin/main 25fea77a 上,条目的 surface 以 “sweeping the start node's objectName” 结尾,reason 那句也只说开始节点;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 已合并。dev 读了 validate-expressions.ts 的流程遍历(objectName = startObject ?? sweepObject),复核在 L2736-2740、L2857-2861 核到同一结论。前提成立。
  3. 门禁 check-runs:
    • head bc0dd484 上 55 success、9 skipped、0 failure,全部跑完,Test Core 在其中(dev 本地整包 pnpm test 因验证锁超时未测,由 CI 覆盖)。
    • 契约复核 6667973654 在 Served-tier: CONTRACT_REVIEW_TIER 下 PASS,Scope 覆盖 hits(条目文件)以及门禁和定时触发器的所引代码。
    • dev 本地 80 条派生门禁 --ran 0 NOT-MEASURED,只证本地子集;三条首轮 exit 3 是前置包没构建,构建后重跑都是 0。
    • 消融:surface 改回原句后 pin 1 条转红,还原后 blob 等于 HEAD,迁移测试 158/158 绿。
  4. 席位自读(TIER_DEFAULT,hits 之外的部分):
    • 测试:新 describe 从 MIGRATIONS_BY_MAJOR[18] 取条目,断言它存在、surface 含 sweeping config.timeRelative.object。
    • changeset 逐句核过:@objectstack/spec patch,Clause-②: no / Metadata-protocol: none,与认领 6665805154 一致;“no verdict moves” 与 diff 只改两个字符串一致。句中 “kind” 指条目所在的 semantic/18. 分类,SemanticMigration 没有这个字段,复核已记,不算问题。
  5. 接受的偏差:
    • 复核 should-fix:pin 只钉了 “sweeping config.timeRelative.object”,没钉后面两个 “typed against …” 分句。不采纳:本卡要防的回退是条目重新只提开始节点的 objectName,这条 pin 能抓到;逐句钉散文措辞,只会让以后的文字修订多一步。为一条测试改动挪 head 还要再复核一次,不划算。
    • 有一次消融的 vitest 跑在验证锁之外,结果与锁内的迁移测试一致。
    • PR 没设 assignee 和 reviewer,Tier S 不交人。
  6. 范围外去向和 Skills-docs::
  7. 落地条款:check-governed-merges.mjs --pr 30637 已在本 head 读过:3 个路径 0 个命中 governed 登记,36 行(+33 / -3)≤ 3000,属于 Tier S;落地前再读一次。经 relay pr_ready + automerge_enable,跟到 MERGED,同一动作里摘 pm:dispatched。

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 20:07
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 20:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 2d6402c Nov 5, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-30279-time-relative-entry-surface branch November 5, 2026 20:37
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/s tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant