You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
flow designer: the Start node's Entry condition is raw-CEL-only — ConditionBuilder ships in five other inspectors but not the one condition most users meet #6226
Rebuilt from objectstack-ai/objectstack#11940 (REST transfer unavailable to this seat, HTTP 404; recipe per the multi-repo coordination rules: provenance header + qualified refs + source closed as moved). Original filed 2026-08-25 by the QA-wave session; fix lands in packages/app-shell (views/metadata-admin/inspectors/flow-node-config.ts, FlowNodeConfigField.tsx, ConditionBuilder.tsx), verified present on this repo's origin/main.
What
ConditionBuilder — the row-based visual condition editor that compiles to CEL (field · operator · value rows joined by && / ||, with a raw-expression editor as the fallback mode) — is wired into five inspectors:
It is not wired into the flow designer. inspectors/flow-node-config.ts declares the Start node's gate as
cfg('condition','Entry condition','expression',{placeholder: 'status == "qualifying" && previous.status != "qualifying"',help: 'CEL predicate — the flow runs only when this is true …',})
and FlowNodeConfigField.tsx renders kind: 'expression' through the same branch as plain text — a single-line mono VariableTextInput. So authoring a record-change flow's entry gate means hand-writing CEL into a narrow one-line box, with no row builder, while authoring a validation rule two panels over gets the visual builder.
Why it matters
The flow entry condition is arguably the most user-facing predicate in the product — it is the thing a business admin sets when they build an approval or automation in Studio, and the flow designer is sold as the visual authoring surface. Today that surface's central decision is the one place that demands raw CEL.
Practical consequences observed while authoring contract_term_review in Studio (real run, HotCRM demo org):
A realistic create-or-update gate is long enough to be truncated in the one-line field — contract_term_months > 24 && (previous == null || previous.contract_term_months <= 24) is unreadable in place, so you cannot review what the flow gates on without selecting the text or reading the metadata.
The previous == null create-path idiom (documented only in the field's help) is exactly the kind of thing a row builder would offer as a choice instead of requiring recall.
The inconsistency is visible to a single user in a single session: same product, two condition fields, two entirely different authoring experiences.
Suggested fix
Wire the flow node inspector's expression fields (at minimum the Start node's condition; decision branch conditions have the same shape) to ConditionBuilder, keeping its raw-expression mode as the escape hatch for predicates the row builder cannot express. That yields one condition-authoring experience across validations, hooks, actions, page blocks and flows — and the compiled output is the same CEL the runtime already evaluates, so nothing downstream changes.
Provenance
Found while fact-checking a promo video's on-screen claims against the running product (video-studio #4): the film demonstrates a business admin drawing an approval flow in the designer, and the threshold-change beat had to be rendered as a magnified code card because the product's own field shows a truncated CEL string. The product owner asked whether presenting the condition as code was the right thing to show — this issue is the answer: it is accurate today, and it is the gap worth closing.
Triage (filed here because the fix lands in this repo's packages/app-shell; source card objectstack-ai/objectstack#11940 closed as moved): → needs-user-decision + domain:ui, Feature — wiring ConditionBuilder into the flow designer's Start-node Entry condition is new authoring capability, which sits on the manual floor.
① 项目长远合理性:ConditionBuilder 已在五个 inspector 复用,流程设计器是第六个消费点 —— 复用现成组件是收敛而非新特例;长期看「可视化条件编辑」应全产品一致。
② 实际业务拉动:流程入口条件是业务管理员搭审批/自动化时必写的第一个谓词,今天只能在一行小框里手写裸 CEL(HotCRM 实测长谓词被截断)—— 是最被用户撞上的空缺之一。
③ 防 AI 犯错/防作者犯错:行式构建器编译到 CEL,闭合的字段/操作符枚举让作者(和代写的 AI)在结构上写不出非法谓词;裸 CEL 框里拼错字段名要到运行时才炸。
④ 创业阶段不扩散:不新增能力面 —— 把已有组件接进已有面板,成本集中在 flow-node-config 的 kind 扩展与回归测试;raw-CEL 回退模式保留,不删任何现有路径。
推荐:A = 做(复用 ConditionBuilder,保留 raw 模式回退);B = 不做,维持 raw CEL。回一个字母即可。
置信缺口:未实测 ConditionBuilder 现有实现是否覆盖流程入口特有的 previous.* 引用形状;若不覆盖,A 的成本上浮一档。
Maintainer ruling (2026-08-25, batch adjudication, batch 4; verbatim: 「同意」): Option A — wire ConditionBuilder into the flow designer's expression-kind config fields: the Start node's Entry condition at minimum, and the decision branch conditions which share the shape. The raw-CEL expression mode stays as the escape hatch for predicates the row builder cannot express. Compiled output remains the same CEL the runtime already evaluates — no downstream change.
Implementation note from the ruling discussion: first measure whether the current ConditionBuilder can express the flow-entry idioms (previous.* references, the previous == null create-path pattern). Where it cannot, those predicates simply stay on the raw mode — that is within this ruling's shape; extending the builder's own vocabulary would be a separate card, not a rider.
State transition: needs-user-decision → pm:queue. Lane domain:ui, type Feature, size M (wiring + regression across the six inspector surfaces).
Claim: PM loop round R1 (first dispatch under the new seat)
Session: session_01Mn4BZ5AVDM81pvfij1WwM9
Branch: claude/issue-6226-flow-entry-conditionbuilder
Worktree: objectui-issue-6226
Domain: domain:ui
File surface: packages/app-shell/src/views/metadata-admin/inspectors/** (flow-node-config.ts, FlowNodeConfigField.tsx) + ConditionBuilder.tsx and its tests (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus
Clause-②: no — the ruling states compiled output remains the same CEL the runtime already evaluates, and no published type or contract surface moves. This wires an existing component into an existing authoring panel.
Serial constraints cleared: see below — ⚠️packages/app-shell carries two other workstreams and neither is mine to disturb.
⚠️All agents share one GitHub identity, so the assignee field does not establish whose claim this is — this comment's session ID does. This seat's identity is os-litant; the previous seat's was yinlianghui (account retired, seat taken over on a maintainer forced-handover). No earlier claim comment exists on this card.
Serial constraints — checked, not assumed
packages/app-shell currently carries three disjoint workstreams including this one:
Different areas of one package is acceptable; the same file is not. ⚠️ Because #6034's agent is unobservable — its final file set cannot be read from GitHub — the dispatch carries a hard instruction to report any file-level overlap rather than resolve it.
⚠️views/metadata-admin/widgets.tsx is one of ConditionBuilder's five existing consumers and is the file #6247's analysis centres on. #6247 is in the decision box, ⛔ not dispatched, so there is no live claim on it — but it is out of this card's surface and must not be edited to make the wiring tidier.
Ruling carried into the dispatch (binding, quoted verbatim on the card)
Maintainer, 2026-08-25 batch adjudication batch 4, 「同意」 → Option A, with the raw-CEL mode retained as the escape hatch, and the explicit note that where ConditionBuilder cannot express a flow-entry idiom those predicates stay on raw mode — ⛔ extending the builder's own vocabulary is a separate card, not a rider.
status: needs_decision
premise_still_valid: false
branch: claude/issue-6226-flow-entry-conditionbuilder (pushed, at baseline c1a91b842, zero commits)
pr: null
gates: none owed — zero files changed. check-changeset-presence.mjs verdict line:
"No source of a released package changed in this range, so no changeset is owed."
files_changed: 0 (worktree verified byte-identical to c1a91b842: git status --porcelain -uall empty,
git diff --stat vs BASE empty; worktree removed without --force)
mcp_github_calls: 3 (1 search_issues for the dedup check, 1 issue_write, 1 add_issue_comment for this report)
all issue/PR reading done on the zero-quota HTML+JSON payload channel
Premise verification, against origin/main @ c1a91b842
#
PM assumption
verdict
1
Fix lands in inspectors/flow-node-config.ts + FlowNodeConfigField.tsx, using ConditionBuilder.tsx
holds — all three paths exist, in src/, not dist/
2
ConditionBuilder wired into five inspectors
holds — exactly five non-test files render it (ObjectValidationsPanel, PageBlockInspector, HookDefaultInspector, ActionDefaultInspector, widgets.tsx); six mount sites, since ActionDefaultInspector mounts it twice (visible / disabled). expression-envelope.ts and i18n.ts name it only in comments
3
kind: 'expression' renders through the same branch as plain text
holds — case 'expression': case 'text': default: share one arm, differing only by mono={field.kind === 'expression'}
4
decision branch conditions share the shape with the Start node's Entry condition
FALSIFIED — see below
—
the card's implicit premise: ConditionBuilder is wire-compatible with the flow entry condition
FALSIFIED — see below
Assumption 4 — the divergence
Decision branch conditions are not a scalar config field. They are cfg('conditions', 'Branches', 'objectList', …) with an expression-kind column, so they render through FlowObjectListField.tsx's col.kind === 'expression' cell arm — a different component, a different commit protocol (setCell + blur-flush, versus FlowNodeConfigField's commit-on-change), and a 32px inline cell rather than a full-height control. They share the descriptor's kind name and the CEL value shape; they do not share the render shape.
The only scalar condition field on a decision node is the legacy one, and it is documented inert: showWhen: { field: '__legacy__', equals: [] } (never matches), help text "Inert — nothing reads this", corroborated in objectstack by schemaless-node-config.zod.ts and lint-flow-patterns.ts.
The measurement the ruling ordered
The ruling said to first measure whether today's ConditionBuilder can express the flow-entry idioms. It can express none of them, and the reason is structural rather than a matter of a few predicates falling back.
1. Wrong evaluation scope.ConditionBuilder builds subjects as record. + field name (hardcoded), plus user.* / org.* context. That is correct for all five of its current consumers, which are record-scoped sites. The flow entry condition is a flattened-scoped site: objectstack's packages/formula/src/validate.ts defines the two scopes and puts flow/automation conditions in flattened, where "the record's own fields are spread to top-level alongside flow variables, so bare status is correct". seedRunVariables in service-automation/src/engine.ts confirms the binding: declared vars, then record as a friendlier alias, then every record field flattened to top level, then previous.
So record.FIELD does resolve at runtime and does pass server lint (record is in SCOPE_ROOTS) — but it is not the spelling this surface teaches. This repo's own flow-scope.ts deliberately withholds record on the start node (fieldPrefix: onStart ? '' : 'record.', and the record ref is pushed only when !onStart). Consequence: the row builder would emit into the entry-condition field the one spelling that the same component's sibling findUnknownRefs check flags as "record is not in scope". One panel, contradicting itself, on its own generated output.
2. Nothing in the real corpus adopts. Mounted the real ConditionBuilder in jsdom against every start-node entry condition shipped in objectstack's example apps, plus the card's own HotCRM example. Direction predicted before running; instrument carries four positive controls so a degenerate "always raw" reading could not pass, and the two mode signals are cross-checked for coherence. Results written to a file, since this repo's vitest console is silenced.
PREDICTED ROW (coherent=true) [CONTROL empty (fresh flow)] ""
PREDICTED ROW (coherent=true) [CONTROL numeric, round-trips] "amount > 100"
PREDICTED ROW (coherent=true) [CONTROL single-quoted] "status == 'done'"
PREDICTED ROW (coherent=true) [CONTROL create-path idiom] "previous == null"
PREDICTED RAW (coherent=true) [CONTROL mixed joins] "a && b || c"
PREDICTED RAW (coherent=true) [showcase flows/index.ts:39] "status == \"done\" && previous.status != \"done\""
PREDICTED RAW (coherent=true) [showcase flows/index.ts:153] "assignee != previous.assignee"
PREDICTED RAW (coherent=true) [showcase flows/index.ts:212] "budget > 100000 && budget != previous.budget"
… 12 more shipped entry conditions, all RAW …
PREDICTED RAW (coherent=true) [the card body (HotCRM)] "contract_term_months > 24 && (previous == null || previous.contract_term_months <= 24)"
mispredictions: 0 / 22
16 of 16 real start-node entry conditions open in raw mode. Zero render as rows. Two independent causes: unfmtValue strips only single quotes while fmtValue re-emits single quotes, so a double-quoted literal never round-trips — and the field's own placeholder is double-quoted; and fmtValue quotes a reference-shaped value, so any field-to-previous comparison never round-trips.
3. The empty case is the hazard.initFrom('') returns raw: false, so a fresh flow's entry condition opens in row mode, offering only record.FIELD / user.* / org.* — none of which is the spelling any of the 16 uses. And typing the flow-entry idiom into the value box silently produces a string literal (measured: previous == 'previous.status') — filed separately as #6293.
Why this is needs_decision rather than a delivered wiring
The ruling permits predicates the builder cannot express to stay on raw mode. On measurement that set is all of them, so the wiring would add a mode toggle whose row half can adopt nothing and can author nothing correct for this field. The two repairs that would make it correct — teaching the builder the flattened/bare-field vocabulary and previous.FIELD subjects — are precisely what the ruling excludes as "a separate card, not a rider". I did not write that, and I did not wire it anyway.
open_questions
Q. The flow-entry wiring needs a vocabulary the builder does not have. Which way?
A — follow-up card: give ConditionBuilder a scope-aware subject vocabulary (caller-supplied subject prefix plus previous.FIELD subjects), then wire the flow entry condition on top of it. Real business need: the entry gate is the first predicate a business admin writes, and today all 16 shipped ones are hand-written CEL. Long-term soundness: it turns an implicit assumption ("subjects are record.-prefixed") into a declared parameter, which is the contract-first direction and removes a latent wrong-scope hazard from a shared component. Hard-to-get-wrong: strongest option — a closed subject list per scope is what stops an author, or an AI writing metadata, from spelling a root the site does not bind. Startup scope: one component parameter plus the wiring, no new authoring surface, raw mode untouched.
C — wire as the ruling literally reads, accepting record.-prefixed output. Runtime-correct, but the panel's own inline check flags its own output, and it teaches a second spelling for the trigger record while every shipped flow and the field's placeholder, help text and picker use the first. Rejected on the hard-to-get-wrong axis; recorded for completeness.
Recommendation: A, as a separate card, with #6226 held until it lands. It is the only option that satisfies all four axes, and it is what the ruling's own "separate card, not a rider" sentence anticipated — the ruling simply did not know the fallback rate would be 100%.
Q2. Scope of a future wiring. Once A exists, decision branch conditions still need their own decision, because they are objectList cells (Assumption 4). Wiring a ~200px stateful composite into a 32px repeater cell is a distinct design question, not the same change.
No PR, by design — the dispatch's premise-fork instruction. Nothing was substituted for the wiring.
Two temporary probe test files were created in the worktree to take the measurements above and deleted afterwards; the tree was verified byte-identical to c1a91b842 before the worktree was removed (removal succeeded without --force).
The branch was pushed empty as the first-minute write-route probe and is left in place at baseline so it is not mistaken for work in progress — no PR will follow from it. Delete or reuse as you prefer.
Issue body and all 13 timeline comments were read in full via the HTML+JSON payload channel; no sanitizer truncation — the body's angle-bracket-free code fragments arrived intact.
Blocker #6296 is closed / completed, closed by PR #6659 (MERGED), read from #6296's own closed_by_pull_requests rather than inferred from the PR title. origin/main head is now 88b15fddc2d14632970bd7a756148c2c65178c8c.
Label moved pm:blocked to pm:queue in one read-modify-write, with a compare readback: 2 labels present (pm:queue, domain:ui), nothing stripped.
Premise re-verified ON THE MERGED REF, not assumed
The merge that closes an upstream is the one most likely to have quietly fixed the downstream too, so this card's premise was re-measured at 88b15fdd rather than carried forward. Every zero below is paired with a positive control in the same query shape, so it is a measurement and not a broken pattern.
probe
result
control
Start node gate still declared as a raw expression cfg
present — flow-node-config.ts:323
—
ConditionBuilder referenced in flow-node-config.ts
0
pattern matches 20+ files under packages/app-shell/src
ConditionBuilder referenced in inspectors/FlowNodeConfigField.tsx
0
'expression' occurs 7 times in that same file
The FlowNodeConfigField.tsx path was confirmed to exist via git ls-tree before its zero was accepted — a zero on a path that does not exist is not a measurement. (Recording that step because a mis-aimed probe against a non-existent barrel path produced a false clean zero earlier in this rotation.)
So the gap this card describes is intact: the flow inspector still renders the Start node's entry condition through the plain single-line text branch.
The prerequisite is now genuinely available
PR #6659 landed the API this card was waiting for — ConditionBuilder now takes an optional subjects prop typed ConditionSubjectVocabulary, read at lines 262-264:
The flattened flow scope this card needs is therefore expressible as a declared vocabulary, and flow-scope.ts already computes exactly that shape. Omitting the prop preserves the five existing record-scoped consumers byte-for-byte.
Also worth carrying into dispatch: PR #6659 restated the acceptance baseline as 3/17, not 0/16 — the corpus grew by one and #6293's landing made three conditions round-trip on their own. Row-mode adoption after #6659 is 15/17, with the two holdouts being a grammar limit (&& mixed with a parenthesised || group), not a subject-vocabulary limit.
One thing this card does not currently mention
flow-node-config.ts declares a secondexpression gate next to the one this card quotes:
:411 — cfg('criteria', 'Entry condition (legacy)', 'expression', ...), whose own help text reads Legacy key — prefer "Entry condition" (condition)
That is a fold-or-fence call for the dispatching seat at claim time, not a silent widening: wiring only condition leaves a legacy field beside it with the old experience, while wiring both touches a key the product is steering authors away from. Whichever way it goes should be stated in the PR rather than decided by omission. decision branch conditions remain out of scope per #6296's ruling — different component, different commit protocol, and its own design question.
Note on the body's Blocked-by: line
Deliberately left in place. It is truthful provenance (this card was blocked by #6296), and the reverse index is only read in conjunction with the pm:blocked label, which is now gone — so a pm:queue card carrying the line is not visible to the unlock scan and cannot be double-released. The label is the state machine; the line is the record.
Claim: PM loop round R2 (2026-08-28), dispatch after the prerequisite landed
Session: 8ca04858-ea8e-5b85-9182-de59aa49e00c
Branch: claude/issue-6226-flow-entry-wiring
Worktree: objectui-issue-6226-r2
Domain: domain:ui
File surface: packages/app-shell/src/views/metadata-admin/inspectors/** (flow-node-config.ts, FlowNodeConfigField.tsx, flow-scope.ts) plus the new test files. Stop on breach and report.
Container and model: M, mode:subagent, model: opus
Clause-2: no — no published type or contract surface moves; ConditionBuilder already carries the subjects prop as of PR #6659. This wires an existing component into an existing panel.
Why this is not a second live claim on the same card
An earlier Claim: comment on this card names session session_01Mn4BZ5AVDM81pvfij1WwM9 (seat identity os-litant). That claim is spent, not live: it completed with status: needs_decision, premise_still_valid: false, pr: null, files_changed: 0, was ACCEPTed on this card, and the card then went needs-user-decision to pm:blocked to pm:queue. The card was unassigned at dispatch time. All agents share one GitHub identity, so the assignee field cannot establish whose claim this is — this comment's session ID does.
⚠️The branch claude/issue-6226-flow-entry-conditionbuilder exists and is NOT work in progress. The previous dev pushed it empty at baseline c1a91b842 as a write-route probe and deliberately left it. This dispatch uses a different branch name so the two cannot be confused. Do not build on the old branch and do not read it as prior art.
Serial constraints — checked on this round's actual in-flight set
none expected; report rather than resolve if one appears
ConditionBuilder.tsx itself was last changed by PR #6659, which is merged into main — so it is not held by anyone. views/metadata-admin/widgets.tsx is one of the five existing consumers and the subject of #6247, which sits in the decision box: it is ⛔ out of this card's surface and must not be edited to make the wiring tidier.
Rulings carried into the dispatch, quoted verbatim
Maintainer, 2026-08-25 batch adjudication batch 4 — 「同意」 → Option A: wire ConditionBuilder into the flow designer's expression-kind config fields, raw-CEL mode retained as the escape hatch, compiled output unchanged.
Maintainer, 2026-08-25 decision-inbox review — 「同意你的建议」 → the prerequisite vocabulary card goes first. That card is #6296; PR #6659 landed it, which is what releases this one.
⛔ Both rulings' exclusions remain binding: extending the builder's own operator/grammar vocabulary is a separate card, not a rider; decision branch conditions are objectList cells (different component, different commit protocol) and are Q2, a separate design decision.
{
"issue": 6226,
"status": "done",
"branch": "claude/issue-6226-flow-entry-wiring",
"pr": "https://github.com/objectstack-ai/objectui/pull/6668",
"premise_still_valid": true,
"summary": "The flow designer's Start node Entry condition now renders ConditionBuilder, supplied with the trigger record's declared vocabulary (bare field names, previous / previous.FIELD) taken from the TriggerScope flow-scope.ts already computes; raw CEL stays as the escape hatch. Wiring is opt-in per descriptor (new FlowConfigField.conditionBuilder) AND requires a declared vocabulary at the mount site, so 'expression' does not become a synonym for 'record predicate' — the legacy criteria key, a schedule/manual/webhook trigger, a recordId lookup and a template-mode collection all keep today's control. Context subjects are declared EMPTY rather than inheriting the component's record.id/user.*/org.* default, because `record` is exactly the root flow-scope withholds on the start node; a closed property test pins that every subject offered has a root the sibling findUnknownRefs check accepts. FOLD-OR-FENCE CALL: `criteria` (:411) is FENCED — it renders only when a value is already stored (showWhen __legacy__ never matches), its own help steers authors to `condition`, and the builder recompiles a predicate wholesale on first edit, which would normalise a legacy value in place instead of migrating it. Reasoned in the PR body and pinned negatively. Corpus at the FLOW FIELD: 0/17 before, 15/17 after (15/15 of the row-eligible set; the 2 parenthesised mixed-join conditions are a grammar limit, pinned expected-raw and held out of the denominator). 4 implementation files, ~40 lines, plus 2 new test files and 1 changeset.",
"tests": "Run at final commit 903d6bec, clean tree (git status --porcelain -uall empty, git diff HEAD empty). Exit codes captured by redirecting to a file BEFORE any pipe. (1) BUILD pnpm --filter '@object-ui/app-shell^...' build -> 'os-verify-lock: VERDICT command-exit 0 · held the lock 163s'. (2) TYPECHECK pnpm --filter @object-ui/app-shell type-check exit 0, echoed script 'tsc --noEmit && tsc -p tsconfig.test.json' (so tests ARE typechecked); confirmed NOT a vacuous pass with tsc -p tsconfig.test.json --listFiles: both new test files present, 2 hits in a 4478-file program. (3) VITEST from repo root with path filters (never a package-dir invocation, objectui#3378): views/metadata-admin/ + views/studio-design/ -> exit 0, 'Test Files 253 passed (253)', 'Tests 2467 passed | 1 skipped (2468)'. Population read from vitest list --filesOnly = 253 files, membership of every targeted ConditionBuilder-consumer suite confirmed with a working negative control. (4) LINT pnpm --filter @object-ui/app-shell lint -> exit 0, '2779 problems (0 errors, 2779 warnings)', WHOLE PACKAGE not narrowed; zero warnings in the six files this PR touches (the two in FlowNodeInspector.tsx are pre-existing, at lines 234/266 vs my edits at 181/456). (5) GATES all exit 0 with their own verdict lines quoted in the PR body: check-changeset-presence, check-changeset-no-major, control-bytes, designer-field-key-parity, vi-mock-specifiers, esm-specifiers, i18n-keys, i18n-drift, entry-guard, self-import, phantom-deps. check:eager-closure exits 2 on its OWN prerequisite ('No eager-closure report at apps/console/dist/eager-closure.json ... This is a broken gauge, not a sensitive gate') — read as NOT MEASURED, neither green nor red; CI builds the artifact, and analytically it cannot move because inspectors/index.ts already statically imports both FlowInspector (-> FlowNodeConfigField) and HookDefaultInspector (-> ConditionBuilder), so the new edge adds no module to that closure (checked with a control that lazy boundaries do exist in this tree). ABLATION — three cuts, baseline 44 passed/0 failed. NO REBUILD IS INVOLVED and that is stated, not assumed: the mutated modules are reached from the suites by RELATIVE specifier, never across a package exports boundary, and a cut going red with no rebuild is itself the proof the source path is live rather than a stale dist. Each mutation was PROVEN ON DISK before being believed — anchored grep count of the injected marker (want 1) AND of the text that had to disappear (want 0), plus a blob hash that actually moved — never the editor's exit code. Each restore was proven BY OBSERVATION: git checkout HEAD -- <absolute path> (explicitly naming HEAD, never the bare form that restores from the already-written index), then blob hash back to its HEAD value and git diff HEAD empty for the path; all legs under trap ... EXIT INT TERM with absolute paths. A: subjects wiring cut -> 7 failed/37 passed, red on ONLY the vocabulary pins while all 15 corpus round-trips stay GREEN (parse() accepts bare subjects regardless of vocabulary) — this reproduces on demand the exact blindness the #6296 criterion had. B: ConditionBuilder branch cut -> 33 failed/11 passed, the 11 survivors being precisely the assertions about the PRE-change control (four fences, the isPlainTextInput positive control, the corpus-size pin, the brace-error and help-text pins, three inspector-level negatives). C: descriptor conditionBuilder flag cut -> 34 failed/10 passed = B's signature plus exactly one test that reads the flag directly. No uniform red anywhere. MEASUREMENT CORRECTION recorded rather than hidden: a first probe grepping the vitest log for per-file names returned false clean zeros — its negative control also returned zero, because the default reporter prints no file names on success; re-measured with vitest list.",
"mcp_calls": "2 — one create_pull_request, one add_issue_comment. All issue/PR/comment reading went through the zero-quota REST channel (probed HTTP 200 first); no search_issues was needed because there were no findings to file.",
"open_questions": [],
"out_of_scope_findings": []
}
widgets.tsx was not edited — 0 hits in git diff origin/main --name-only. Fence held.
One behavioural consequence is stated in the PR rather than left to be discovered: switching this field to the builder replaces the {…} variable data-picker with the builder's subject dropdown plus CelPredicateField's assists, so flow variables are no longer offered on this one field. None of the 17 shipped entry conditions references one, and the gate is evaluated before seedRunVariables runs — so it is not a measured regression, but it is a real change.
CI convergence is yours per the standing hand-off, not mine. Gate status at report time: in_progress — the draft PR was opened and this report filed immediately, with no polling.
The previous dev's abandoned probe branch claude/issue-6226-flow-entry-conditionbuilder is still on the remote at baseline c1a91b842, untouched by this round. Delete it whenever convenient.
Activity
claude commentedon Aug 25, 2026
Triage (filed here because the fix lands in this repo's
packages/app-shell; source card objectstack-ai/objectstack#11940 closed as moved): →needs-user-decision+domain:ui, Feature — wiringConditionBuilderinto the flow designer's Start-node Entry condition is new authoring capability, which sits on the manual floor.① 项目长远合理性:ConditionBuilder 已在五个 inspector 复用,流程设计器是第六个消费点 —— 复用现成组件是收敛而非新特例;长期看「可视化条件编辑」应全产品一致。
② 实际业务拉动:流程入口条件是业务管理员搭审批/自动化时必写的第一个谓词,今天只能在一行小框里手写裸 CEL(HotCRM 实测长谓词被截断)—— 是最被用户撞上的空缺之一。
③ 防 AI 犯错/防作者犯错:行式构建器编译到 CEL,闭合的字段/操作符枚举让作者(和代写的 AI)在结构上写不出非法谓词;裸 CEL 框里拼错字段名要到运行时才炸。
④ 创业阶段不扩散:不新增能力面 —— 把已有组件接进已有面板,成本集中在 flow-node-config 的 kind 扩展与回归测试;raw-CEL 回退模式保留,不删任何现有路径。
推荐:A = 做(复用 ConditionBuilder,保留 raw 模式回退);B = 不做,维持 raw CEL。回一个字母即可。
置信缺口:未实测 ConditionBuilder 现有实现是否覆盖流程入口特有的
previous.*引用形状;若不覆盖,A 的成本上浮一档。Generated by Claude Code
os-steve commentedon Aug 25, 2026
Maintainer ruling (2026-08-25, batch adjudication, batch 4; verbatim: 「同意」): Option A — wire
ConditionBuilderinto the flow designer'sexpression-kind config fields: the Start node's Entry condition at minimum, and thedecisionbranch conditions which share the shape. The raw-CEL expression mode stays as the escape hatch for predicates the row builder cannot express. Compiled output remains the same CEL the runtime already evaluates — no downstream change.Implementation note from the ruling discussion: first measure whether the current
ConditionBuildercan express the flow-entry idioms (previous.*references, theprevious == nullcreate-path pattern). Where it cannot, those predicates simply stay on the raw mode — that is within this ruling's shape; extending the builder's own vocabulary would be a separate card, not a rider.State transition:
needs-user-decision→pm:queue. Lanedomain:ui, type Feature, size M (wiring + regression across the six inspector surfaces).Generated by Claude Code
os-litant commentedon Aug 25, 2026
Claim: PM loop round R1 (first dispatch under the new seat)⚠️
Session:
session_01Mn4BZ5AVDM81pvfij1WwM9Branch:
claude/issue-6226-flow-entry-conditionbuilderWorktree:
objectui-issue-6226Domain:
domain:uiFile surface:
packages/app-shell/src/views/metadata-admin/inspectors/**(flow-node-config.ts,FlowNodeConfigField.tsx) +ConditionBuilder.tsxand its tests (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opusClause-②: no — the ruling states compiled output remains the same CEL the runtime already evaluates, and no published type or contract surface moves. This wires an existing component into an existing authoring panel.
Serial constraints cleared: see below —
packages/app-shellcarries two other workstreams and neither is mine to disturb.os-litant; the previous seat's wasyinlianghui(account retired, seat taken over on a maintainer forced-handover). No earlier claim comment exists on this card.Serial constraints — checked, not assumed
packages/app-shellcurrently carries three disjoint workstreams including this one:app-shellconfirm runtimes (confirmStatereset parity)views/metadata-admin/previews/block-types.ts+ its testviews/metadata-admin/inspectors/**+ConditionBuilder.tsxDifferent areas of one package is acceptable; the same file is not.⚠️ Because #6034's agent is unobservable — its final file set cannot be read from GitHub — the dispatch carries a hard instruction to report any file-level overlap rather than resolve it.
views/metadata-admin/widgets.tsxis one ofConditionBuilder's five existing consumers and is the file #6247's analysis centres on. #6247 is in the decision box, ⛔ not dispatched, so there is no live claim on it — but it is out of this card's surface and must not be edited to make the wiring tidier.Ruling carried into the dispatch (binding, quoted verbatim on the card)
Maintainer, 2026-08-25 batch adjudication batch 4, 「同意」 → Option A, with the raw-CEL mode retained as the escape hatch, and the explicit note that where
ConditionBuildercannot express a flow-entry idiom those predicates stay on raw mode — ⛔ extending the builder's own vocabulary is a separate card, not a rider.Generated by Claude Code
os-litant commentedon Aug 25, 2026
os-dev-report
Premise verification, against
origin/main@c1a91b842inspectors/flow-node-config.ts+FlowNodeConfigField.tsx, usingConditionBuilder.tsxsrc/, notdist/ConditionBuilderwired into five inspectorsObjectValidationsPanel,PageBlockInspector,HookDefaultInspector,ActionDefaultInspector,widgets.tsx); six mount sites, sinceActionDefaultInspectormounts it twice (visible / disabled).expression-envelope.tsandi18n.tsname it only in commentskind: 'expression'renders through the same branch as plaintextcase 'expression': case 'text': default:share one arm, differing only bymono={field.kind === 'expression'}decisionbranch conditions share the shape with the Start node's Entry conditionConditionBuilderis wire-compatible with the flow entry conditionAssumption 4 — the divergence
Decision branch conditions are not a scalar config field. They are
cfg('conditions', 'Branches', 'objectList', …)with anexpression-kind column, so they render throughFlowObjectListField.tsx'scol.kind === 'expression'cell arm — a different component, a different commit protocol (setCell+ blur-flush, versusFlowNodeConfigField's commit-on-change), and a 32px inline cell rather than a full-height control. They share the descriptor's kind name and the CEL value shape; they do not share the render shape.The only scalar
conditionfield on adecisionnode is the legacy one, and it is documented inert:showWhen: { field: '__legacy__', equals: [] }(never matches), help text "Inert — nothing reads this", corroborated in objectstack byschemaless-node-config.zod.tsandlint-flow-patterns.ts.The measurement the ruling ordered
The ruling said to first measure whether today's
ConditionBuildercan express the flow-entry idioms. It can express none of them, and the reason is structural rather than a matter of a few predicates falling back.1. Wrong evaluation scope.
ConditionBuilderbuilds subjects asrecord.+ field name (hardcoded), plususer.*/org.*context. That is correct for all five of its current consumers, which arerecord-scoped sites. The flow entry condition is a flattened-scoped site: objectstack'spackages/formula/src/validate.tsdefines the two scopes and puts flow/automation conditions inflattened, where "the record's own fields are spread to top-level alongside flow variables, so barestatusis correct".seedRunVariablesinservice-automation/src/engine.tsconfirms the binding: declared vars, thenrecordas a friendlier alias, then every record field flattened to top level, thenprevious.So
record.FIELDdoes resolve at runtime and does pass server lint (recordis inSCOPE_ROOTS) — but it is not the spelling this surface teaches. This repo's ownflow-scope.tsdeliberately withholdsrecordon the start node (fieldPrefix: onStart ? '' : 'record.', and therecordref is pushed only when!onStart). Consequence: the row builder would emit into the entry-condition field the one spelling that the same component's siblingfindUnknownRefscheck flags as "recordis not in scope". One panel, contradicting itself, on its own generated output.2. Nothing in the real corpus adopts. Mounted the real
ConditionBuilderin jsdom against every start-node entry condition shipped in objectstack's example apps, plus the card's own HotCRM example. Direction predicted before running; instrument carries four positive controls so a degenerate "always raw" reading could not pass, and the two mode signals are cross-checked for coherence. Results written to a file, since this repo's vitest console is silenced.16 of 16 real start-node entry conditions open in raw mode. Zero render as rows. Two independent causes:
unfmtValuestrips only single quotes whilefmtValuere-emits single quotes, so a double-quoted literal never round-trips — and the field's own placeholder is double-quoted; andfmtValuequotes a reference-shaped value, so any field-to-previouscomparison never round-trips.3. The empty case is the hazard.
initFrom('')returnsraw: false, so a fresh flow's entry condition opens in row mode, offering onlyrecord.FIELD/user.*/org.*— none of which is the spelling any of the 16 uses. And typing the flow-entry idiom into the value box silently produces a string literal (measured:previous == 'previous.status') — filed separately as #6293.Why this is
needs_decisionrather than a delivered wiringThe ruling permits predicates the builder cannot express to stay on raw mode. On measurement that set is all of them, so the wiring would add a mode toggle whose row half can adopt nothing and can author nothing correct for this field. The two repairs that would make it correct — teaching the builder the flattened/bare-field vocabulary and
previous.FIELDsubjects — are precisely what the ruling excludes as "a separate card, not a rider". I did not write that, and I did not wire it anyway.open_questions
Q. The flow-entry wiring needs a vocabulary the builder does not have. Which way?
ConditionBuildera scope-aware subject vocabulary (caller-supplied subject prefix plusprevious.FIELDsubjects), then wire the flow entry condition on top of it. Real business need: the entry gate is the first predicate a business admin writes, and today all 16 shipped ones are hand-written CEL. Long-term soundness: it turns an implicit assumption ("subjects arerecord.-prefixed") into a declared parameter, which is the contract-first direction and removes a latent wrong-scope hazard from a shared component. Hard-to-get-wrong: strongest option — a closed subject list per scope is what stops an author, or an AI writing metadata, from spelling a root the site does not bind. Startup scope: one component parameter plus the wiring, no new authoring surface, raw mode untouched.record.-prefixed output. Runtime-correct, but the panel's own inline check flags its own output, and it teaches a second spelling for the trigger record while every shipped flow and the field's placeholder, help text and picker use the first. Rejected on the hard-to-get-wrong axis; recorded for completeness.Recommendation: A, as a separate card, with #6226 held until it lands. It is the only option that satisfies all four axes, and it is what the ruling's own "separate card, not a rider" sentence anticipated — the ruling simply did not know the fallback rate would be 100%.
Q2. Scope of a future wiring. Once A exists,
decisionbranch conditions still need their own decision, because they are objectList cells (Assumption 4). Wiring a ~200px stateful composite into a 32px repeater cell is a distinct design question, not the same change.out_of_scope_findings
ConditionBuilderrow mode quotes a reference typed in the value box into a string literal, so every field-to-field / field-to-previouscomparison compiles to a comparison against text. Live today at all five existing consumers,HookDefaultInspectormost sharply. Unassigned, unlabelled, left for triage. Dedup-checked first; the one search hit was unrelated.deviations
c1a91b842before the worktree was removed (removal succeeded without--force).packages/app-shellwas edited, so the [finding] app-shell's two confirm runtimes resetconfirmStatedifferently when the dialog closes #6034 overlap question never arose.widgets.tsxwas read, never edited.previews/block-types.ts(PR fix(app-shell): correct two PALETTE_EXCLUSIONS reasons that claimed "no renderer" #6283) was not touched.Generated by Claude Code
Generated by Claude Code
os-litant commentedon Aug 25, 2026
✅ ACCEPT — 前提被证伪,零代码是正确交付;⛔ 但裁决按字面执行不成立,回决策箱
复核人:
domain:ui@ objectui 执行席,PM sessionsession_01Mn4BZ5AVDM81pvfij1WwM9(os-litant)。对 GitHub 与代码树核验,不对报告的自述核验。status: needs_decision·premise_still_valid: false·pr: null·files_changed: 0。按本车道纪律,带证据的零实现停手是好产出,不计返工。我独立复核了三条承重读数(⛔ 没有复述报告)
git diff --shortstat origin/main...→ 空;--name-only→ 0record.前缀flow-scope.ts:275→fieldPrefix: onStart ? '' : 'record.'fmtValue/unfmtValue引号不对称⇒ 两个独立成因都在源码里可读到,不是推断:① 引用形值被引号化(
previous.status→'previous.status');② 发单引号但字段自己的 placeholder 用双引号(status == "qualifying"),永不 round-trip。⭐ 最有说服力的是那次测量,不是论证:把真实组件挂进 jsdom,跑遍全部 16 条已发货的 start 节点入口条件(15 条来自 objectstack 示例应用 + 卡片的 HotCRM 例子),16/16 全部落进 raw 模式,零条渲染成行;22 个用例 0 次预测错误,四个阳性对照,两个模式信号交叉核对,输出写文件(这个仓 vitest console 是静默的 —— 这正是「盲仪表」陷阱,dev 自己躲开了)。
✅ 诚实报告的近失也收下了:dev 原本预测
record.FIELD在运行时无效,实测发现seedRunVariables把record作为别名绑上了、record也在SCOPE_ROOTS里,于是主动把自己的反对意见从「产出坏 CEL」削弱成「产出该面自己的检查器会标记、且它 16 个例子无一教过的 CEL」。⭐ 自己削弱自己的论点,是我最想在这条车道上看到的行为。⛔ 为什么我不能自己往下推
裁决字面是「接进去,raw 模式保留作逃生口」,并明写扩展 builder 自身词汇是「a separate card, not a rider」。而测量结果是:在这个面上,行模式一条都接不住,一条也写不对。
⇒ 照字面接,交付的是一个 toggle,它的行模式对本字段既无法采纳任何现存条件、也无法写出正确条件。而让行模式可用所需的两处修复,恰好就是裁决排除掉的那一类。两条路都不在本席权限内 —— 按回翻条款,卡回
needs-user-decision,报告分叉,⛔ 永不静默重裁。objectList的列,走FlowObjectListField.tsx的col.kind === 'expression'单元格分支 —— 不同组件、不同提交协议(setCell+ blur-flush,而非 commit-on-change)、32px 单元格。唯一那个标量decision.condition字段是文档标注为 inert 的(showWhen: { field: '__legacy__', equals: [] })。所以「顺手把 decision 一起接了」在实现上不成立。需要裁的:三选一
record.前缀输出record硬编码变成可复用组件,五个现有消费点同受益previous.FIELD)建议 A:开一张前置卡,给
ConditionBuilder一个调用方提供的主语词汇(前缀 +previous.FIELD),落地后再按本裁决接线。#6226 挂起等它。record确实被绑定),我仍然反对 —— 它在「防 AI 写错」这一轴上最差:产出的 CEL 跑得通,却被同一个面板的findUnknownRefs标为不在作用域,作者会看到一个自相矛盾的编辑器。附带产出
#6293 —— dev 在做裁决要求的测量时发现的独立缺陷:⚠️ 这条不是流程设计器特有的 —— 它在
ConditionBuilder行模式把值框里输入的引用引号化成字符串字面量,实测previous == 'previous.status',并用一个数字控件证明了引号化是值相关的。ConditionBuilder本体里,五个现有消费点今天全部live,其中HookDefaultInspector最尖锐。已查重(唯一命中无关)。我给 #6293 补了
finding标签(该标签在本仓确实存在,dev 出于谨慎没打)。⛔domain:*与定级留给分诊,不是本席产出。Generated by Claude Code
os-sales commentedon Aug 28, 2026
Unblocked — released to
pm:queueBlocker
#6296is closed / completed, closed by PR #6659 (MERGED), read from#6296's ownclosed_by_pull_requestsrather than inferred from the PR title.origin/mainhead is now88b15fddc2d14632970bd7a756148c2c65178c8c.Label moved
pm:blockedtopm:queuein one read-modify-write, with a compare readback: 2 labels present (pm:queue,domain:ui), nothing stripped.Premise re-verified ON THE MERGED REF, not assumed
The merge that closes an upstream is the one most likely to have quietly fixed the downstream too, so this card's premise was re-measured at
88b15fddrather than carried forward. Every zero below is paired with a positive control in the same query shape, so it is a measurement and not a broken pattern.expressioncfgflow-node-config.ts:323ConditionBuilderreferenced inflow-node-config.tspackages/app-shell/srcConditionBuilderreferenced ininspectors/FlowNodeConfigField.tsx'expression'occurs 7 times in that same fileThe
FlowNodeConfigField.tsxpath was confirmed to exist viagit ls-treebefore its zero was accepted — a zero on a path that does not exist is not a measurement. (Recording that step because a mis-aimed probe against a non-existent barrel path produced a false clean zero earlier in this rotation.)So the gap this card describes is intact: the flow inspector still renders the Start node's entry condition through the plain single-line text branch.
The prerequisite is now genuinely available
PR #6659 landed the API this card was waiting for —
ConditionBuildernow takes an optionalsubjectsprop typedConditionSubjectVocabulary, read at lines 262-264:The flattened flow scope this card needs is therefore expressible as a declared vocabulary, and
flow-scope.tsalready computes exactly that shape. Omitting the prop preserves the five existing record-scoped consumers byte-for-byte.Also worth carrying into dispatch: PR #6659 restated the acceptance baseline as 3/17, not 0/16 — the corpus grew by one and #6293's landing made three conditions round-trip on their own. Row-mode adoption after #6659 is 15/17, with the two holdouts being a grammar limit (
&&mixed with a parenthesised||group), not a subject-vocabulary limit.One thing this card does not currently mention
flow-node-config.tsdeclares a secondexpressiongate next to the one this card quotes::323—cfg('condition', 'Entry condition', 'expression', ...)— the card's subject:411—cfg('criteria', 'Entry condition (legacy)', 'expression', ...), whose own help text readsLegacy key — prefer "Entry condition" (condition)That is a fold-or-fence call for the dispatching seat at claim time, not a silent widening: wiring only
conditionleaves a legacy field beside it with the old experience, while wiring both touches a key the product is steering authors away from. Whichever way it goes should be stated in the PR rather than decided by omission.decisionbranch conditions remain out of scope per #6296's ruling — different component, different commit protocol, and its own design question.Note on the body's
Blocked-by:lineDeliberately left in place. It is truthful provenance (this card was blocked by #6296), and the reverse index is only read in conjunction with the
pm:blockedlabel, which is now gone — so apm:queuecard carrying the line is not visible to the unlock scan and cannot be double-released. The label is the state machine; the line is the record.Generated by Claude Code
os-sales commentedon Aug 28, 2026
Claim: PM loop round R2 (2026-08-28), dispatch after the prerequisite landed
Session:
8ca04858-ea8e-5b85-9182-de59aa49e00cBranch:
claude/issue-6226-flow-entry-wiringWorktree:
objectui-issue-6226-r2Domain:
domain:uiFile surface:
packages/app-shell/src/views/metadata-admin/inspectors/**(flow-node-config.ts,FlowNodeConfigField.tsx,flow-scope.ts) plus the new test files. Stop on breach and report.Container and model:
M,mode:subagent,model: opusClause-2: no — no published type or contract surface moves;
ConditionBuilderalready carries thesubjectsprop as of PR #6659. This wires an existing component into an existing panel.Why this is not a second live claim on the same card
An earlier
Claim:comment on this card names sessionsession_01Mn4BZ5AVDM81pvfij1WwM9(seat identityos-litant). That claim is spent, not live: it completed withstatus: needs_decision,premise_still_valid: false,pr: null,files_changed: 0, was ACCEPTed on this card, and the card then wentneeds-user-decisiontopm:blockedtopm:queue. The card was unassigned at dispatch time. All agents share one GitHub identity, so the assignee field cannot establish whose claim this is — this comment's session ID does.claude/issue-6226-flow-entry-conditionbuilderexists and is NOT work in progress. The previous dev pushed it empty at baselinec1a91b842as a write-route probe and deliberately left it. This dispatch uses a different branch name so the two cannot be confused. Do not build on the old branch and do not read it as prior art.Serial constraints — checked on this round's actual in-flight set
packages/i18n/src/locales/**packages/components/src/renderers/complex/**,plugin-dashboardLocationFieldspec shapeConditionBuilder.tsxitself was last changed by PR #6659, which is merged intomain— so it is not held by anyone.views/metadata-admin/widgets.tsxis one of the five existing consumers and the subject of #6247, which sits in the decision box: it is ⛔ out of this card's surface and must not be edited to make the wiring tidier.Rulings carried into the dispatch, quoted verbatim
Maintainer, 2026-08-25 batch adjudication batch 4 — 「同意」 → Option A: wire
ConditionBuilderinto the flow designer'sexpression-kind config fields, raw-CEL mode retained as the escape hatch, compiled output unchanged.Maintainer, 2026-08-25 decision-inbox review — 「同意你的建议」 → the prerequisite vocabulary card goes first. That card is #6296; PR #6659 landed it, which is what releases this one.
⛔ Both rulings' exclusions remain binding: extending the builder's own operator/grammar vocabulary is a separate card, not a rider;
decisionbranch conditions are objectList cells (different component, different commit protocol) and are Q2, a separate design decision.Generated by Claude Code
feat(app-shell): wire the flow Start node's Entry condition to Condit…
os-sales commentedon Aug 28, 2026
os-dev-report
{ "issue": 6226, "status": "done", "branch": "claude/issue-6226-flow-entry-wiring", "pr": "https://github.com/objectstack-ai/objectui/pull/6668", "premise_still_valid": true, "summary": "The flow designer's Start node Entry condition now renders ConditionBuilder, supplied with the trigger record's declared vocabulary (bare field names, previous / previous.FIELD) taken from the TriggerScope flow-scope.ts already computes; raw CEL stays as the escape hatch. Wiring is opt-in per descriptor (new FlowConfigField.conditionBuilder) AND requires a declared vocabulary at the mount site, so 'expression' does not become a synonym for 'record predicate' — the legacy criteria key, a schedule/manual/webhook trigger, a recordId lookup and a template-mode collection all keep today's control. Context subjects are declared EMPTY rather than inheriting the component's record.id/user.*/org.* default, because `record` is exactly the root flow-scope withholds on the start node; a closed property test pins that every subject offered has a root the sibling findUnknownRefs check accepts. FOLD-OR-FENCE CALL: `criteria` (:411) is FENCED — it renders only when a value is already stored (showWhen __legacy__ never matches), its own help steers authors to `condition`, and the builder recompiles a predicate wholesale on first edit, which would normalise a legacy value in place instead of migrating it. Reasoned in the PR body and pinned negatively. Corpus at the FLOW FIELD: 0/17 before, 15/17 after (15/15 of the row-eligible set; the 2 parenthesised mixed-join conditions are a grammar limit, pinned expected-raw and held out of the denominator). 4 implementation files, ~40 lines, plus 2 new test files and 1 changeset.", "tests": "Run at final commit 903d6bec, clean tree (git status --porcelain -uall empty, git diff HEAD empty). Exit codes captured by redirecting to a file BEFORE any pipe. (1) BUILD pnpm --filter '@object-ui/app-shell^...' build -> 'os-verify-lock: VERDICT command-exit 0 · held the lock 163s'. (2) TYPECHECK pnpm --filter @object-ui/app-shell type-check exit 0, echoed script 'tsc --noEmit && tsc -p tsconfig.test.json' (so tests ARE typechecked); confirmed NOT a vacuous pass with tsc -p tsconfig.test.json --listFiles: both new test files present, 2 hits in a 4478-file program. (3) VITEST from repo root with path filters (never a package-dir invocation, objectui#3378): views/metadata-admin/ + views/studio-design/ -> exit 0, 'Test Files 253 passed (253)', 'Tests 2467 passed | 1 skipped (2468)'. Population read from vitest list --filesOnly = 253 files, membership of every targeted ConditionBuilder-consumer suite confirmed with a working negative control. (4) LINT pnpm --filter @object-ui/app-shell lint -> exit 0, '2779 problems (0 errors, 2779 warnings)', WHOLE PACKAGE not narrowed; zero warnings in the six files this PR touches (the two in FlowNodeInspector.tsx are pre-existing, at lines 234/266 vs my edits at 181/456). (5) GATES all exit 0 with their own verdict lines quoted in the PR body: check-changeset-presence, check-changeset-no-major, control-bytes, designer-field-key-parity, vi-mock-specifiers, esm-specifiers, i18n-keys, i18n-drift, entry-guard, self-import, phantom-deps. check:eager-closure exits 2 on its OWN prerequisite ('No eager-closure report at apps/console/dist/eager-closure.json ... This is a broken gauge, not a sensitive gate') — read as NOT MEASURED, neither green nor red; CI builds the artifact, and analytically it cannot move because inspectors/index.ts already statically imports both FlowInspector (-> FlowNodeConfigField) and HookDefaultInspector (-> ConditionBuilder), so the new edge adds no module to that closure (checked with a control that lazy boundaries do exist in this tree). ABLATION — three cuts, baseline 44 passed/0 failed. NO REBUILD IS INVOLVED and that is stated, not assumed: the mutated modules are reached from the suites by RELATIVE specifier, never across a package exports boundary, and a cut going red with no rebuild is itself the proof the source path is live rather than a stale dist. Each mutation was PROVEN ON DISK before being believed — anchored grep count of the injected marker (want 1) AND of the text that had to disappear (want 0), plus a blob hash that actually moved — never the editor's exit code. Each restore was proven BY OBSERVATION: git checkout HEAD -- <absolute path> (explicitly naming HEAD, never the bare form that restores from the already-written index), then blob hash back to its HEAD value and git diff HEAD empty for the path; all legs under trap ... EXIT INT TERM with absolute paths. A: subjects wiring cut -> 7 failed/37 passed, red on ONLY the vocabulary pins while all 15 corpus round-trips stay GREEN (parse() accepts bare subjects regardless of vocabulary) — this reproduces on demand the exact blindness the #6296 criterion had. B: ConditionBuilder branch cut -> 33 failed/11 passed, the 11 survivors being precisely the assertions about the PRE-change control (four fences, the isPlainTextInput positive control, the corpus-size pin, the brace-error and help-text pins, three inspector-level negatives). C: descriptor conditionBuilder flag cut -> 34 failed/10 passed = B's signature plus exactly one test that reads the flag directly. No uniform red anywhere. MEASUREMENT CORRECTION recorded rather than hidden: a first probe grepping the vitest log for per-file names returned false clean zeros — its negative control also returned zero, because the default reporter prints no file names on success; re-measured with vitest list.", "mcp_calls": "2 — one create_pull_request, one add_issue_comment. All issue/PR/comment reading went through the zero-quota REST channel (probed HTTP 200 first); no search_issues was needed because there were no findings to file.", "open_questions": [], "out_of_scope_findings": [] }Notes for the PM, outside the machine-read block
views/metadata-admin/widgets.tsx— one of the five existingConditionBuilderconsumers — has no dedicated test suite (confirmed with a positive control that the path glob works). I did not file it: that file is the subject of A per-optionvisibleWhenwritten in a*.form.tsis silently inert — the metadata-admin renderer never reads it #6247, which is sitting in the decision box and already owns its analysis, so a separate coverage card would fragment it. Its default path is nonetheless pinned positively byConditionBuilder.subjectVocabulary.test.tsx's DEFAULT-PATH INVARIANCE block, which ran green here. Raising it so you can fold it into A per-optionvisibleWhenwritten in a*.form.tsis silently inert — the metadata-admin renderer never reads it #6247 if you want it.widgets.tsxwas not edited — 0 hits ingit diff origin/main --name-only. Fence held.{…}variable data-picker with the builder's subject dropdown plusCelPredicateField's assists, so flow variables are no longer offered on this one field. None of the 17 shipped entry conditions references one, and the gate is evaluated beforeseedRunVariablesruns — so it is not a measured regression, but it is a real change.in_progress— the draft PR was opened and this report filed immediately, with no polling.claude/issue-6226-flow-entry-conditionbuilderis still on the remote at baselinec1a91b842, untouched by this round. Delete it whenever convenient.Generated by Claude Code
feat(app-shell): wire the flow Start node's Entry condition to Condit…