Skip to content

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

Draft
objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/project-thread-orkuas
Draft

objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/project-thread-orkuas

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#2 · os-bill · session_01Utp6CaUg3ibF939L2RFB5D

Fixes #30026

Clause-②: yes (narrowing) — the author-time gate (os build, os validate, the package publish and the flow publish door) refuses a type-mismatch in the start, edge and decision conditions of a time-relative flow whose start node names no objectName, which it passed with no type verdict at all before. Added beside it (the widening half): the optional sweepObject input of the validate_expression MCP tool.
Metadata-protocol: narrowed
Skills-docs: updated here — the MCP skill text in packages/mcp/src/skill-md.ts (the new sweepObject input); skills/** teaches no time-relative flow condition with a text-vs-number comparison, so no skill or eval moves.

维护者速读

  • 改了什么:时间相对(定时扫描)流程的开始节点不写 objectName、只在 timeRelative.object 写了被扫描的对象时,条件现在按被扫描对象的字段做类型检查,文本与数字比较会被发布拦下(以前完全不检查);开始节点写的对象与扫描对象不一致时,仍只给警告,但会多一条说明告诉作者为什么、怎么改;MCP 的 validate_expression 新增可选输入 sweepObject,能给出与发布门一致的判决。
  • 为什么改:这类流程每次运行拿到的 record 就是被扫描对象的一行,条件里文本与数字比较每次都会让运行失败,但发布时一声不吭;另外 fix(lint)!: a flow condition's text-vs-number comparison gets a true verdict on every flow, record.x read and decision branch #30015 的注释和测试描述了一个根本绑不上的形状(不写 timeRelative.object)。
  • 风险与回滚:只收窄一种形状的发布(BREAKING 已在 changeset 写明 FROM → TO),已登记的 ADR-0087 语义条目覆盖同一修法;其余流程判决不变;回滚即撤 PR。
  • 先看:packages/lint/src/validate-expressions.ts 流程遍历开头的 objectName = startObject ?? sweepObject 与 sweepsTypedObject,同文件流程末尾的说明警告;packages/mcp/src/mcp-http-tools.ts 的 sweepObject 输入与 flow_condition 判决。
  • 两分钟验证:pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-expressions.test.ts -t "time-relative",预期 3 个用例通过,其中 tr_sweep_only 三处条件均为 error。
  • 需要你做的:无,席位落地(收窄面由席位安排 CONTRACT_REVIEW_TIER 复审)。

What changes

  • Lint — the typed object of a flow (validateStackExpressions, the flow walk). The object a flow's conditions are typed against is the start node's objectName, else, on a time-relative flow, timeRelative.object. TimeRelativeTriggerSchema requires that key, and time-relative-trigger.ts launches every run with a row of that object as record (engine.find(desc.object, …), then { record: row }), whatever the start node names. So a sweep whose start names no objectName is now typed against the swept object at every flow site that reads the typed object: start, edge and decision conditions (field existence, the type check with the run-fails outcome, record.FIELD reads), the shadowed-variable warning, and the root check's did-you-mean order. The null-guard twin is unchanged (record-triggered flows only).
  • Lint — sweepsTypedObject is now timeRelative.object === typed object and nothing else. The old "else the start node's" arm graded a descriptor with no object as a sweep of the start object; that descriptor never binds (flow-time-relative-descriptor-invalid, error), so no run of it binds a row and it now gets no declared outcome (the warning). The ledger comment says so.
  • Lint — why a sweep of another object only warns. When the start node's objectName is not timeRelative.object, the type-mismatch-run-may-fail warning stays, and the flow gets one more warning at flows[i].nodes[j].config.objectName saying why: the conditions are checked against the start object's fields while each run's record is a row of the swept one; set objectName to the swept object or remove it. It is pushed once per flow, and only when the flow was served a -run-may-fail warning (counted in check() by the warning's code, so the date verdicts' -run-may-fail arm from expression gate: a date-arithmetic verdict in a flow condition still says the value "silently evaluates to null", though the run fails #30016 is counted too once it lands).
  • MCP — validate_expression takes sweepObject (optional; site flow_condition with triggerKind: 'time_relative' only, a tool error anywhere else, the same refusal shape as severity / triggerKind). It is the flow's timeRelative.object; another object than objectName gives the gate's warning instead of the run-fails error. The flow_condition outcome reads a third input, sweepsTypedObject, the lint ledger's own third fact. The tool description and the MCP skill text (skill-md.ts) name the input.
  • Tests. The tr_default pin (a descriptor with no object, a shape that cannot bind) is re-purposed to tr_sweep_only: a start with no objectName sweeping crm_opportunity, refused at start, decision and edge. New pins: the note on tr_other (once, its where, path and source) and its absence on a clean tr_other, on tr_same and on tr_sweep_only; a well-typed tr_sweep_only flow is clean. The MCP per-trigger-kind table gains three rows (sweepObject = objectName, start with no objectName, sweepObject another object), each asserted equal to the gate's own verdict on the same flow, and the wrong-site refusal list gains four sweepObject calls. The key-read meta-test now lists id on startNode (the note names the start node).
  • Changeset .changeset/30026-time-relative-sweep-typing.md, minor on @objectstack/lint and @objectstack/mcp, BREAKING with FROM → TO, ADR-0087 already-registered flow-record-read-and-decision-type-mismatch-refused, and a correction note for the released fix(lint)!: a flow condition's text-vs-number comparison gets a true verdict on every flow, record.x read and decision branch #30015 changeset text ("or is omitted"), which is left as is.

Acceptance notes

  • ADR-0087 disposition. already-registered flow-record-read-and-decision-type-mismatch-refused: the entry prescribes this exact rewrite for a time-relative flow's conditions (same code, same replacement, same reason). Its surface text says "sweeping the start node's objectName", which this change makes narrower than the gate; editing it is packages/spec/**, outside this card's surface. The seat files it for domain:spec.
  • A sweep of another object is still typed against the start object. Field reads are judged against the start object too, so a record.FIELD read of a field only the swept object declares is still refused as unknown there, while the run would hold it. That is pre-existing and not made worse here; the new note tells the author to align the two objects, which removes it.
  • The engine's binding fallback. deriveTriggerBinding (plugin-extension-api.ts) still names config.objectName as the binding's object when timeRelative.object is absent. The sweep itself reads only the parsed descriptor, so no run depends on it; noted, not touched.

Verification

All readings on head bc376239 (the two commits merged with main at 797f9257) unless named.

Build, typecheck, tests
  • Build: pnpm --filter '@objectstack/lint^...' --filter '@objectstack/mcp^...' build, VERDICT command-exit 0; full turbo run build --filter='!@objectstack/docs', 119/119 tasks, VERDICT command-exit 0.
  • @objectstack/lint: pnpm test 7996/7996 (193 files); pnpm typecheck 0 (with check:test-typecheck).
  • @objectstack/mcp: pnpm test 678/678 (56 files); pnpm typecheck 0 (with check:test-typecheck).
  • eslint --no-inline-config over the five changed source and test files: 0. The repo-wide pnpm lint is CI's.
Ablations (each through scripts/ablation-replace.mjs; restored blob == HEAD, `git diff HEAD` empty)
  • A1, lint source (the lint suite imports src): objectName = startObject ?? sweepObject → startObject. 1 red: tr_sweep_only: expected [] to deeply equal [ …3 ], the pre-fix answer (no verdict at all).
  • A2, lint dist/ (the MCP suite reads @objectstack/lint through its exports): the same mutation with a string marker, lint rebuilt, ablation-dist-preflight present in 4 built files; 1 red: time_relative, start names no objectName: expected [] to have a length of 1. Restore leg rebuilt, preflight --absent over all 20 built files, suite 38/38.
  • A3, MCP source: sweepsTypedObject forced true. 1 red: sweepObject another object: expected [ [ 'error', … ] ] to deeply equal [ [ 'warning', … ] ].
Derived gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on bc376239: 59 families; all 59 run, each exit 0 (exit code captured per line before any pipe).
  • --ran reconciliation: "59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED".
Dogfood (the narrowing arm's battery, on the final head)
  • pnpm --filter @objectstack/dogfood test under the lock, no shard, no path: Test Files 418/422 passed (3 skipped, 1 failed), Tests 3491 passed, 36 skipped, 1 failed; VERDICT command-exit 1.
  • The one red is permission-set-clone-door.dogfood.test.ts › "a platform set … copies into its shipping package's pair": cloning member_default answers 422, objects.sys_comment / objects.sys_activity not defined in the target package. It is not this change: with validate-expressions.ts, mcp-http-tools.ts and skill-md.ts set back to origin/main and lint and mcp rebuilt, the same file is red the same way (1 failed / 10 passed). The grants arrived on main with fix(plugin-security): every member reads and posts comments, and reads the activity history, of the records they can read #30222 (490d136f, "every member reads and posts comments, and reads the activity history"), inside this branch's base.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6CaUg3ibF939L2RFB5D


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bc376239f50fec951f23a0ec20f9be535dbca117
Local-runs: none
Scope: packages/lint/src/validate-expressions.ts, packages/lint/src/validate-expressions.test.ts, packages/mcp/src/mcp-http-tools.ts, packages/mcp/src/mcp-validate-expression.test.ts, packages/mcp/src/skill-md.ts + .changeset/30026-time-relative-sweep-typing.md (for ②), packages/spec/src/automation/time-relative-trigger.zod.ts (TimeRelativeTriggerSchema, object required, objectName an alias the strict shape refuses), packages/spec/src/automation/flow-trigger-kind.ts (resolveFlowTriggerKind), packages/triggers/trigger-schedule/src/time-relative-trigger.ts (L335 safeParse at bind, L598 engine.find(desc.object, …), L657 { record: row }, the run context's record and object: desc.object), packages/lint/src/validate-flow-trigger-readiness.ts (block 1b, flow-time-relative-descriptor-invalid as error, flow-trigger-unknown-object as warning), packages/lint/src/flow-cel-root-scope.ts (flowCelRootScope: every object's fields seed bound, the trigger object leads did-you-mean only; flowCelEntrances hands record to a time-relative flow), packages/formula/src/validate.ts (no field set ⇒ no existence or type verdict; type-mismatch-run-may-fail and the two date -run-may-fail codes in expression-refusal.ts), packages/services/service-automation/src/plugin-extension-api.ts (deriveTriggerBinding, time_relative arm), packages/spec/src/migrations/entries/semantic/18.flow-record-read-and-decision-type-mismatch-refused.ts, and PR #30015's record 6625738206. Card #30026 read whole: body, triage 6626758212, claim 6648775252, correction 6648841838, os-dev-report 6649735495. The net diff origin/main...bc376239 (6 files, +226/−44, merge base 797f9257). The head's check-runs, see ③.

① Derived judgments

Accept-set and public-surface changes the diff implies, each judged against the live tree on main:

  1. Lint, the typed object of a flow becomes startObject ?? sweepObject (sweepObject read only when resolveFlowTriggerKind answers time_relative). RIGHT. The sweep binds TimeRelativeTriggerSchema at start, finds rows of desc.object and launches each run with that row as record and object: desc.object; the start node's objectName is read by no line of the sweep. So a start naming no objectName has its record typed by the swept object, which is the only object its run ever binds.
  2. New refusal arm A, declared: type-mismatch-run-fails at start, edge and decision conditions of a sweep-only flow. RIGHT and correctly directed. Before, check() ran with no objectName, so validateExpression got no field set and no field types and issued no type verdict (formula validate.ts L1439–L1490); the run then faulted on every swept row (ADR-0032 section 1c, CEL has no text-vs-number ordering). The refusal makes the spec's own refusal real. tr_sweep_only pins all three sites, and ablation A1 is the pin that would catch its loss.
  3. New refusal arm B, declared in the body and changeset but not in the Clause-② sentence: field existence. A record.FIELD read of a field the swept object does not declare was accepted on a sweep-only flow (no field set ⇒ no existence check; the root check binds record because the sweep hands one) and is now refused as unknown field … on OBJECT with the validator's own did-you-mean. RIGHT: the run faults on no such key. See ④ should-fix 1 for the declaration.
  4. Bare names. No accept-set change. The root check already bound every object's fields (flowCelRootScope L329 adds all of fieldIndex), so a bare field of the swept object was never refused there; the typed object only leads the did-you-mean order, exactly as the PR body says. A bare name outside the swept object's fields now gets the flattened-scope typo warning. RIGHT.
  5. sweepsTypedObject becomes sweepObject !== undefined && sweepObject === objectName. RIGHT. The old arm typeof sweep.object !== 'string' graded a descriptor the schema refuses (object is required; objectName inside the descriptor is a strict-shape alias, refused with a rename) as a sweep of the start object; readiness refuses that flow as error and the trigger throws at bind, so no run of it binds a row. The expression gate now warns where it erred on a flow that is refused one rule over; nothing an author can publish changes. Declared in the changeset's correction bullet. The tr_default pin's re-purposing is right for the same reason.
  6. Sweep of another object (startObject !== sweepObject) keeps the -run-may-fail warning and gains one note at config.objectName. RIGHT as an advisory, and the gating is sound: counted on w.code.endsWith('-run-may-fail') inside check(), which every start, edge and decision verdict passes through (checkDeclaredPredicate calls check() when branchCondition is given), delta taken per flow, so it fires once and only where a warning it explains was served. Pins: tr_other once with where, path, source; absent on tr_other_clean, tr_same, tr_sweep_only. See ④ should-fix 2 and 3.
  7. MCP validate_expression gains the optional sweepObject input (public tool surface, widening) and flow_condition's outcome reads a third fact (narrowing of the tool's verdict: time_relative with another sweepObject now answers the gate's warning instead of run-fails). RIGHT: the verdict is the ledger's own, faultOutcomeOf's third input, and each new table row is asserted equal to the gate's verdict on the same flow. The wrong-site refusal (site !== 'flow_condition' || triggerKind !== 'time_relative') is the same shape as severity / triggerKind, pinned on four calls. sweepObject never reaches the bridge, so the missing guard() on it is harmless.
  8. Skill text names the input and when to pass it. RIGHT, consistent with the tool description and the lint rule.
  9. Nothing in packages/spec, no governed path, no ui/ consumer of the changed symbols. deriveTriggerBinding's config.objectName fallback is correctly left alone: the sweep reads the parsed descriptor only.

② Semver level

  • Changeset .changeset/30026-time-relative-sweep-typing.md: '@objectstack/lint': minor, '@objectstack/mcp': minor. The diff moves exactly those two published packages; the kind is narrowed (a condition an author could publish is refused), so at least minor on each per the Post-Task Checklist level rule. CORRECT.
  • Declarations, line-initial in both the PR body and the changeset and byte-identical: Clause-②: yes (narrowing) — … with the widening half named (the optional sweepObject input), and Metadata-protocol: narrowed. Well-formed; (narrowing) carries the BREAKING banner and the FROM → TO block in the changeset. CORRECT.
  • ADR-0087 disposition: exactly one marker, adr-0087: not-required (already-registered flow-record-read-and-decision-type-mismatch-refused) …, which parses under the gate's not-required (CATEGORY ids) why grammar with already-registered in the closed vocabulary. The id resolves on main at packages/spec/src/migrations/entries/semantic/18.flow-record-read-and-decision-type-mismatch-refused.ts, pre-existing and untouched by this diff, so it is the honest category (not the R5 shape). Its replacement, reason ("a time-relative sweep launches each run with the swept row as its record") and acceptanceCriteria ("a record-triggered or time-relative flow's condition") already cover the new shape; only its surface clause says "sweeping the start node's objectName". Disposition PRESENT and POINTING AT A REAL ENTRY; the stale surface clause is ④ should-fix 4.
  • Skills-docs: updated here — packages/mcp/src/skill-md.ts: the named file moves in the diff.
  • No model identifier in the title, body, changeset or code comments. The PR body footer is the session-URL form.

③ Boundary flags

From os-dev-report 6649735495:

  • open_questions: [] — none to answer.
  • Deviation 1 (declarations measured yes (narrowing) / narrowed against the claim's no / none): correctly raised before the PR and corrected by claim 6648841838; the body and changeset carry the corrected lines. CLOSED.
  • Deviation 2 (packages/mcp/src/skill-md.ts outside the expected landing list): not on the claim's forbidden list, the same file fix(lint)!: a flow condition's text-vs-number comparison gets a true verdict on every flow, record.x read and decision branch #30015 moved for the same reason, and the Skills-docs: line declares it. ACCEPTED.
  • Deviation 3 (assign/review step and PR subscription skipped per the dispatch order): a reporting note, nothing to judge.
  • Out-of-scope finding 1 (permission-set-clone-door.dogfood.test.ts red on main at 797f9257, control leg red the same way): not this change; the seat files it as its own card (class a, dedupe words given). ESCALATED to the seat.
  • Out-of-scope finding 2 (ADR-0087 entry surface text): the seat files it to domain:spec, see ④ should-fix 4. ESCALATED.
  • Security findings: none; none found reading the diff (the tool's new input is compared for equality only).

Flags of this review's own:

④ Design judgment

The narrowing against the published spec on main: does it take a declared capability away at the consumer, or make the spec's own refusal real? It makes the refusal real. TimeRelativeTriggerSchema's own doc declares that a sweep launches the flow once per matching record "with that record in the automation context (so the start-node condition gate works exactly as for record-change flows)"; object is required; the trigger finds rows of desc.object and binds each as record. A text-vs-number comparison in such a flow's start, edge or decision condition faults on every row, so the refusal is ADR-0032 section 1c arriving at author time, the same verdict the entry already prescribes for a sweep of the start object. No consumer shim, no alias, no fallback: lint reads the same key the schema requires and the trigger reads, and the MCP tool reads the same ledger. Nothing declared is taken away. Attempts to break it:

  • Start names objectName A and timeRelative.object B, A ≠ B. Typed against A, warning kept, note served once naming B, the path and the fix. Holds, within the card's own ruling that the warning is correct. The deeper fact stands in the acceptance notes: the run binds a B row only, so typing against A can miss a real fault (A's title a number, B's a text) and refuse a field only B declares; pre-existing since fix(lint)!: a flow condition's text-vs-number comparison gets a true verdict on every flow, record.x read and decision branch #30015, not made worse, and the note now tells the author to align the two. See should-fix 3.
  • Decision node in a sweep-only flow: typed against the swept object through checkDeclaredPredicate → check(); pinned as error on tr_sweep_only. Holds.
  • No timeRelative.object at all: readiness error (flow-time-relative-descriptor-invalid), bind throws; the expression gate's downgrade to the warning changes no publishable flow. Holds.
  • timeRelative.objectName (the alias): the strict shape refuses it with the rename; lint sees no sweepObject and falls back to startObject; the flow is refused by readiness anyway. Holds.
  • Swept object declared by another package (not in the stack): fieldIndex.get is undefined, so no field set and no field types reach the validator, no new refusal; readiness already warns flow-trigger-unknown-object with "ignore this if another installed package declares it". Holds, same treatment a record-change flow with a foreign objectName gets.
  • Start carrying both a record-* triggerType and a descriptor: resolveFlowTriggerKind answers record_change, so sweepObject is not read and the record-change arm applies, which is how the engine routes it. Holds.
  • MCP sweepObject with record_change, schedule, api, none, with site omitted, or with site: validation: tool error, four shapes pinned. With sweepObject === objectName or omitted: run-fails, equal to the gate. With another object: warning, equal to the gate (tr_other). Holds. An empty string sweepObject answers the warning, which is harmless and silly rather than wrong.
  • Runtime publish gate (per-write snapshot): the typed object and the field types do not depend on the entrance map, so the new refusal reaches the flow publish door as the body claims; the note's path uses flowIndex 0 there. Holds.

Findings:

  • should-fix 1 (declaration completeness). The Clause-② sentence in the PR body and changeset names the type-mismatch arm only, while the diff also newly refuses a record.FIELD read of a field the swept object does not declare on a sweep-only flow (① item 3). The changeset's "Now" bullet does say "field reads … are judged against the same object", and the refusal carries its own did-you-mean, so the migration is present; the Clause-② sentence and the FROM → TO line should name the arm too, in the follow-up that merges main.
  • should-fix 2 (note wording). The note says "a type mismatch in its conditions is a warning here", but its counter fires on every -run-may-fail code, which after the merge with main includes the two date-arithmetic arms from fix(formula,lint): a date-arithmetic verdict in a flow condition says the run fails, not that the value reads null #30240. Say "a type or date verdict" (or name the counted code) so the note never explains a verdict the flow did not get.
  • should-fix 3 (follow-up card, not this PR). A start whose objectName differs from timeRelative.object declares an object its run never binds. The card ruled the warning correct and the note is the right first step; the contract-first answer is a readiness verdict on the contradiction itself (the start object is inert on a time-relative flow) so the gate can type every sweep against the swept object alone. File it as its own card against validate-flow-trigger-readiness.ts.
  • should-fix 4 (domain:spec card, as the claim already commits to). The ADR-0087 entry's surface clause "sweeping the start node's objectName" is now narrower than the gate; its reason and acceptanceCriteria already cover a sweep whose start names no objectName. Amend the surface text in packages/spec.
  • No must-fix. The narrowing holds against the schema, the trigger, the readiness rule, the root check and the registered entry; the tests pin each arm and the ablations name the pin that would catch each loss.

Implemented-by: session_01Utp6CaUg3ibF939L2RFB5D
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: PASS

…-orkuas

# Conflicts:
#	packages/lint/src/validate-expressions.test.ts
…y the warning the time-relative note explains

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CaUg3ibF939L2RFB5D
@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/lint, @objectstack/mcp, 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.

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

  • content/docs/api/declarative-endpoints.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/api/plugin-endpoints.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/automation/approvals.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/automation/flows.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line), time_relative (literal, a string literal in registerObjectTools; a string literal in runStackExpressionPasses; a string literal on a changed line))
  • content/docs/automation/hooks.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/automation/workflows.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/concepts/architecture.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/getting-started/common-patterns.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/kernel/services-checklist.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/permissions/capabilities.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line), time_relative (literal, a string literal in registerObjectTools; a string literal in runStackExpressionPasses; a string literal on a changed line))
  • content/docs/permissions/system-context.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/protocol/objectql/schema.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/ui/actions.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))

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

  • content/docs/releases/v12.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/releases/v16.mdx (via time_relative (literal, a string literal in registerObjectTools; a string literal in runStackExpressionPasses; a string literal on a changed line))
  • content/docs/releases/v17/17-0.mdx (via record_change (literal, a string literal in registerObjectTools; a string literal on a changed line))
  • content/docs/releases/v17/17-5.mdx (via time_relative (literal, a string literal in registerObjectTools; a string literal in runStackExpressionPasses; a string literal on a changed line))

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 — 15 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 959c26be1f4179202b0ed42f58076190d4d590d5 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 959c26be1f4179202b0ed42f58076190d4d590d5

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

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

1 participant