Skip to content

fix(lint): a flow condition's bare-name type verdict no longer refuses a name a screen field holds, and warns after a node that can bind any name - #30067

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-c4k10t
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-c4k10t

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #30025

维护者速读

Clause-②: yes (widening) — the author-time gate (os build, os validate, the package publish and the flow publish door) no longer refuses, on a record-triggered flow (or a time-relative sweep of its own object), a type-mismatch-run-fails on a BARE condition name that a screen node's declared field holds (no verdict now), nor on a bare name in a flow with a node that can bind a name no reader sees (script, connector_action, wait, subflow, map, a screen with no input contract), which is now the type-mismatch-run-may-fail warning. A record.<field> / previous.<field> read keeps its refusal, and nothing new is refused. The null-guard verdict's bare names also leave a screen's field names out. Added beside them: the optional bareNamesUnproven key of @objectstack/formula's ExprSchemaHint, and collectScreenFieldNames in @objectstack/lint's flow-variable-scope module.
Metadata-protocol: none
Skills-docs: updated here — content/docs/data-modeling/formulas.mdx (the type-soundness row names the screen field and the open binder); skills/** and the MCP skill text teach no bare flow-condition name a screen or an open binder holds, so no skill or eval moves.

What changes

Measured on main at c23501b2. Two kinds of name a bare flow-condition read can hold instead of the trigger record's field were still typed as the field, so a record-triggered flow was refused type-mismatch-run-fails on a condition that runs:

bare name what the run binds (measured) before now
a screen node's declared field name applyResumeSignal (service-automation/src/engine.ts) lands the resume bag's variables under their plain names, over the flattened field typed as the field: error type-mismatch-run-fails not typed as the field (joins ExprSchemaHint.flowVariables); no verdict
any bare name, in a flow with a node that can bind a name no reader sees (script, connector_action, wait, subflow, map, a screen with no input contract; flowCelRootScope's CLOSED_BINDER_NODE_TYPES) the node can write the live variable map or fold any resume bag error type-mismatch-run-fails warning type-mismatch-run-may-fail
record.<field> / previous.<field> in either flow the field error error (unchanged)

Screen fields (@objectstack/lint). New collectScreenFieldNames(graphs) in flow-variable-scope.ts reads every screen node's config.fields[].name across every graph. The flow walk unions it into the names it hands the type check (flowVariables), and subtracts it from the null-guard twin's nullGuardBare. collectFlowVariableNames is unchanged, so the #14089 shadowing warning and its other consumers are not moved.

Open binders (@objectstack/formula + lint). New optional ExprSchemaHint.bareNamesUnproven (flattened scope only). In typeSoundnessIssue, a type-mismatch on a bare name (no record/previous root) under a declared outcome takes no outcome when the key is set, so it is the existing run-may-fail warning. If a record.<field> mismatch sits in the same source behind the bare one (name > 5 && record.title > 5), firstTypeMismatch is asked again with every bare name left dyn, and that record-root mismatch keeps the error. Date verdicts are untouched.

Which predicate (a measured departure from the order's literal wording, agreed with the seat). The order said to use rootScope.provable, the null-guard twin's predicate. Measured: at the runtime publish gate rootScope is built with perWriteSnapshotEntrance(), which opens EVERY flow write, so the literal reading would have dropped every bare-name refusal on every record-triggered flow at the Studio/package publish door to a warning; publish-type-mismatch-fail-closed.test.ts goes red on the #29926 start pin and the #29959 decision pin (ablation A5 below). The flow walk now asks the same judge, flowCelRootScope(flow, graphs, objectName, fieldIndex), with no entrance: only a NODE that can bind a name stands the bare verdict down. An entrance (and the per-write snapshot, which sees none) changes which record both the bare name and record.<field> read, not whether a node rebinds the bare name, so it moves neither. The null-guard twin keeps standing its bare names down on the whole rootScope, as before. A code comment beside the call says so.

Ledger. faultOutcomeOf's ledger docblock gains the screen field in the variable row and a row for the open-binder stand-down; validate-null-guards.ts point 2 names the screen fields and the shared judge.

Not done (optional item). The engine's registration advisory pass (flow-registration-checks.ts) still passes no flowVariables: @objectstack/service-automation does not depend on @objectstack/lint (where collectFlowVariableNames / collectScreenFieldNames live), and adding that dependency is outside this card. Its line is a log, never a refusal.

Tests

Ablations (each restored, tree clean after):

put back red
A1 screen fields out of flowBare.variables lint "does not type a bare name a screen field holds…"
A2 unproven: false lint "warns, does not refuse, a bare-name mismatch after a node that can bind any name"
A3 formula: no record-root re-ask (outcome = undefined always) formula "drops a bare-name mismatch…" (name > 5 && record.title > 5)
A4 screen fields back in nullGuardBare lint "leaves the null-guard twin silent…"
A5 literal unproven: !rootScope.provable objectql #29926 "refuses the comparison in a record-triggered flow condition…" and #29959 "refuses a name > 5 decision branch…"
A6 unproven: false at the publish door objectql "publishes a bare name > 5 after a script node with the run-may-fail warning"

Verification

All at head 71da71df, under os-verify-lock (shared box). Base c23501b2; origin/main is 3 commits ahead, none on the touched files, and git merge-tree reports no conflict.

build   pnpm --filter '@objectstack/lint^...' --filter @objectstack/lint build     exit 0
build   pnpm --filter '@objectstack/objectql^...' build                            exit 0
typecheck  formula, lint, objectql                                               exit 0
formula test   Test Files 64 passed (64)    Tests 1639 passed (1639)
lint test      Test Files 193 passed (193)  Tests 7977 passed (7977)
objectql test  Test Files 547 passed (547)  Tests 10836 passed (10836)
mcp src/mcp-validate-expression.test.ts     Tests 35 passed (35)   (formula hint parity)
pnpm lint                                                                         exit 0

node scripts/pm/dispatch-gates.mjs --commands: 89 derived families, each run, CMD :: exit N recorded, reconciled with --ran: 89 derived, 88 run, 1 NOT-MEASURED, 0 UNRUN. 88 exit 0 (three of them, check-plugin-teardown-shape --self-test, check:skill-examples and check:lean-entry-closure, first exited 3 on a missing prerequisite and pass after git fetch --unshallow and building objectql / client-react). NOT MEASURED: check:dual-build-cjs-loads, reason: it reads every package's dist (70 missing) and needs a full pnpm build; CI's run is the measurement.

Acceptance notes

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzicfrt7Yr8FaVdDsXD

…e a screen field or an open binder can hold the name

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzicfrt7Yr8FaVdDsXD
… the publish door; changeset and docs row

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzicfrt7Yr8FaVdDsXD
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

Which tree this was computed on

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

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

⚠️ 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 d79eecfe5b78598f9117726b542615ca2332d58f → 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#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

席位核对

Verdict: ACCEPT at head 71da71dfbe8d9f3d4e5175b3d45d0b8a4ae09f28 · 2026-11-04T12:24Z

  • Shape:
    • A screen node's declared field names join the flowVariables hint, so a bare read of one is no longer typed as the object field. They also leave the null-guard twin's nullGuardBare.
    • In a flow with a node that can bind a name no reader sees, a bare-name mismatch has no declared outcome and serves the type-mismatch-run-may-fail warning. That means flowCelRootScope read with no entrance finds a script, connector_action, wait, subflow or map node, or a screen with no input contract. It is carried by the new optional ExprSchemaHint.bareNamesUnproven.
    • record. / previous. reads keep run-fails.
  • Seat review: the diff was read (formula validate.ts typeSoundnessIssue, lint validate-expressions.ts FlowBareNames, flow-variable-scope.ts, validate-null-guards.ts, the changeset).
    • The no-entrance reading is the seat's ruling (11:49Z). The publish door's per-write snapshot entrance changes which record both reads see, not whether a node rebinds the bare name. The literal rootScope.provable would have widened every record-triggered refusal at the publish door, and ablation A5 shows exactly that.
    • When a record-root mismatch sits beside a bare one in the same source, the record-root mismatch wins, so it keeps its refusal.
  • Premise measured: the fix(lint)!: a flow condition's text-vs-number comparison gets a true verdict on every flow, record.x read and decision branch #30015 delta review's finding 1, reproduced on main. The expression gate: a type mismatch in a record-triggered flow condition (and in an advisory validation rule) is served a verdict whose text is false for that role #29926 and expression gate: flow-condition type-mismatch verdicts still miss or misdescribe three sites after #29926 (non-record-triggered flows, record.x, decision branches) and MCP validate_expression cannot carry severity or trigger kind #29959 publish-door refusals still hold, pinned in publish-type-mismatch-fail-closed.test.ts. Ablations A1–A6 each went red and were restored. formula 1639, lint 7977, objectql 10836, MCP parity 35; typecheck and pnpm lint 0. Gates: 88/89 exit 0, plus 1 NOT MEASURED locally (check:dual-build-cjs-loads, measured by CI).
  • Declaration: Clause-②: yes (widening), Metadata-protocol: none, patch on @objectstack/formula and @objectstack/lint, and Skills-docs: updated here. Reviewed as correct: it refuses nothing new, and the new hint key and collectScreenFieldNames are named in the widening.
  • Accepted deviations: the harness-pinned branch claude/project-thread-c4k10t. packages/formula/src/validate.ts joined the file surface, as the order allows. The optional engine log-pass item was not done, because service-automation has no lint dependency.
  • Landing: on all-green at this head, the seat sends pr_ready + automerge_enable through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 12:26
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 4, 2026 12:26
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 29cbfcc Nov 4, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-c4k10t branch November 4, 2026 13:11
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