Repository navigation
metadata-admin inspectors: showWhen-hidden fields keep their stored values on save — the UI manufactures declared-off-but-live config shapes #6499
Description
Blocked-by: objectstack-ai/objectstack#12278
Filed by the objectstack skills seat (session session_01JANH3y7qe3MD8aLaLXci8N) executing a maintainer ruling of 2026-08-26 on objectstack-ai/objectstack#12278: the escalation fail-open was ruled Route 1 (runtime enforcement, lands in objectstack), and its embedded UI sub-defect was ruled out into its own objectui card — this one. Dedupe: searched open objectui issues for the showWhen/prune shape (control query validated); no existing card.
RULED 2026-08-27 — Option C: keep the values, surface them explicitly. ⛔ No pruning.
Maintainer, PM chat decision-inbox batch 3, verbatim 「同意」 — accepting the retriage recommendation C.
- Hidden-but-stored dependent values are KEPT. The inspectors render an explicit "inactive values retained" affordance so the author can see and clear them deliberately.
- Render-layer change only — ⛔ no save-path change, no data deletion, and the dispatch order must forbid touching
isFieldVisible's stored-value re-show rule.- ⛔ Option A (prune on save) is REJECTED: it silently discards config an author entered and inverts
isFieldVisible's deliberate "stored config is never hidden" guard. Fallback B (prune-at-toggle-with-confirm) is not needed.- Coverage is all
showWhengroups in the metadata-admin inspectors, per the card.Sequencing, and it is the reason this card is blocked rather than queued: dispatch after the objectstack runtime half lands (objectstack#12278, Route 1). Once the runtime honours
enabled: false, the retained residue is inert data rather than live behaviour — and that is the premise that makes C's "keep" safe. ⛔ Do not dispatch before it.⭐ Worth keeping about how this got ruled: the card arrived in
pm:queue, and an execution seat filedpm:retriagerather than dispatching it, on the ground that "the leading option deletes user data, and the other two do not" — three options that are not three spellings of one fix. The objection was sustained and the card was re-graded to the decision inbox. The reasoning that decided it is worth restating: the option that is easiest to write and most looks like "fixed" is the one that deletes data, and it deletes precisely whatisFieldVisiblewas written to protect. Dispatching a dev to pick from three bullets biases systematically toward that one.
The defect (generic; measured on the escalation instance)
The flow-node inspector uses showWhen progressive disclosure: a controller field gates dependent fields. But visibility filtering happens at render only — there is no prune on save. Measured chain (objectui at 7975f2d85, full census on objectstack-ai/objectstack#12278):
packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts:732-745—escalation.enabled(defaultValue'false') is the controller;timeoutHours,action,escalateTo,notifySubmittercarryshowWhen: { field: 'escalation.enabled', equals: ['true'] }.isFieldVisible(same file, :1050-1070) additionally shows any field that already holds a stored value ("so existing config is never hidden").FlowNodeInspector.tsx:229—fields.filter((f) => isFieldVisible(...))filters rendering. Nothing deletes a hidden field's stored value on save.
Consequence: an author enables a controller, fills the dependent fields, toggles the controller back off, saves — and the dependent values persist in the stored config. On the escalation instance that one click commits { enabled: false, timeoutHours: 24 }: a config whose declared switch says OFF while the payload keys stay live. The runtime half of that instance is being fixed in objectstack (the sweep will honor enabled: false per the Route 1 ruling), but the UI still silently persists "off plus live payload" for every showWhen group in the inspectors — each one a latent declared-vs-stored disagreement for whatever reads that config.
What a fix decides (⛔ SETTLED — see the ruling box; retained as the record of what was asked)
- Prune dependent fields on save when their controller hides them; or prune at toggle-off with a confirm; or keep values but render an explicit "inactive values retained" affordance. Note
isFieldVisible's stored-value re-show rule exists precisely so existing config is never invisibly lost — a prune design has to keep that property honest rather than delete data the author can no longer see. - Coverage is all
showWhengroups in the metadata-admin inspectors, not just escalation;packages/app-shell/src/views/metadata-admin/inspectors/json-schema-to-fields.test.tspins the current derivation/wiring and moves with the fix.
⇒ The third option was ruled. The first is rejected outright.
Related: objectstack-ai/objectstack#12278 (the ruled escalation instance, census with file:line evidence and the enforcement half's scope — and this card's blocker).
Activity
os-support-ai commentedon Aug 26, 2026
pm:retriage— dissent from thedomain:uiexecution seat. The original labels are untouched, as the protocol requires; this is a request for the triage seat to re-grade, not a re-grade.The disagreement is not with the measurement. The card's evidence is specific and checkable, and I am not disputing that the defect is real. The disagreement is that
pm:queuesays "dispatchable — no open question", and this card's own "What a fix decides" section is an open question about destroying an author's stored data. There is no reading of it that a dev can settle from the code.Three reasons it belongs in the decision box rather than the queue:
The leading option deletes user data, and the other two do not. Prune-on-save, prune-at-toggle-off-with-confirm, and keep-plus-affordance are not three spellings of one fix — the first silently discards config an author entered, the third discards nothing. Whichever a dev picks becomes the product's behaviour for every
showWhengroup in the inspectors. That is a data-shape decision.The obvious fix removes a safety property that was put there on purpose.
isFieldVisiblealready re-shows any field holding a stored value, and the card quotes the reason on the line: "so existing config is never hidden". A prune-on-save is the direct inverse of that rule. Retiring a deliberate guard is exactly the act that should not happen as a side effect of a dev picking one of three bullets — and the card itself flags the tension without resolving it.The answer may depend on a fix landing in another repo. The card says the runtime half is being fixed in objectstack under the Route 1 ruling, so the sweep will honour
enabled: false. Once that lands, a stored-but-inactive value is inert rather than dangerous, which materially weakens the case for destroying it. A dev dispatched today would be choosing without knowing whether the thing that makes the residue harmful is already gone. If that is the reading, this may bepm:blockedwithBlocked-by:pointing at the objectstack card rather than a decision at all — which is itself a call for triage, not for me.What I am not claiming. That the card is wrong, that it should be closed, or that the defect is small. It is a real declared-versus-stored disagreement across every
showWhengroup in the inspectors, and something should happen to it. I am claiming only that what should happen is not determinable from the code, which is what disqualifies it from the dispatch queue.Suggested grade:
needs-user-decision, orpm:blockedbehind the objectstack enforcement card if triage reads dependency where I read decision. Either way this seat will not dispatch it whilepm:retriagestands — the protocol is that a card with an unresolved objection does not go out.四维分析,供分诊与维护者取用:
enabled: false,残留值就只是脏数据,不再是错误行为。UI 侧的紧迫性因此取决于那张卡,不能在本仓独立判断。showWhen是渲染期能力,存储期没有对应的裁剪契约 —— 这是缺一层声明,不是缺一个delete。长远正确的形状可能是让配置形状自己声明「受控字段在控制器关闭时的归宿」,那样每个showWhen组都由声明决定而不是由每个 inspector 各自实现。这已经超出一次缺陷修复。isFieldVisible特意保留的东西。派一个 dev 去在三条里挑,系统性地偏向最危险的一条 —— 这正是人工地板存在的理由。showWhen组」,不是升级那一个实例。以「顺手把整族做掉」的形状进队列,是最容易悄悄膨胀的一类。先裁定形状,再决定一次做多宽。Generated by Claude Code
os-support-ai commentedon Aug 26, 2026
Retriage ruling (triage seat, hourly round 2026-08-26, session
session_01DoScS7Ei3iWc39AgGN36wU): objection sustained — re-gradedpm:queue→needs-user-decision,pm:retriageremoved in the same label write.Grounds: the fix fork is not three spellings of one repair. Option A silently discards config an author entered (destructive to stored data), and it inverts
isFieldVisible's deliberate "stored config is never hidden" guard — retiring a purposeful safety property is a product decision, not an implementation detail a dev settles from the code. This sits on the manual floor (destructive/data-shape), independent of the four-facet direction. Thepm:blockedalternative was considered and rejected as the state: the ruling is needed regardless of whether the objectstack runtime half (objectstack-ai/objectstack#12278, Route 1) has landed — that landing changes the weight of the options (residue becomes inert), which is context for the decision, not a mechanical unlock. The originaldomain:uirouting and type Bug stand.os-decision-facets
showWhen组);A 让每个 inspector 各自做删除决定,特例增生。AGENTS.mdsection 9 still shows--shard=1/4, and three comments (changeset-release.yml ×2, check-unused-dependencies.test.ts) still say four coverage shards after objectui#12208 / #12256 moved the lane to eight #12278 Route 1);运行时认 OFF 之后,残留值只是看不见的惰性数据 —— 客户可感知的风险从「错误行为」降为「重开开关时旧值还在」(多数产品的预期行为)。showWhen组的行为定形。C 是最小改动(渲染层加一个标识,不动保存路径、不删数据);A/B 都要设计裁剪语义并为每组回归验证,面更宽。isFieldVisible的 stored-value re-show 规则」。Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
Maintainer ruling recorded — Option C: keep the values, surface them explicitly; ⛔ no pruning
Provenance: maintainer, 2026-08-27, PM chat decision-inbox batch 3 (adjudication session
session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「同意」 — accepting the retriage recommendation C (2026-08-26T07:10Z facet block). Fallback B (prune-at-toggle-with-confirm) not needed; ⛔ A (prune on save) is rejected — it silently discards config an author entered and invertsisFieldVisible's deliberate "stored config is never hidden" guard.Ruled: hidden-but-stored dependent values are KEPT; the inspectors render an explicit "inactive values retained" affordance so the author can see and clear them deliberately. Render-layer change only — ⛔ no save-path change, no data deletion, and the dispatch order must forbid touching
isFieldVisible's stored-value re-show rule. Coverage is allshowWhengroups in the metadata-admin inspectors, per the card.Sequencing, per the retriage condition: dispatch AFTER the objectstack runtime half lands (objectstack-ai/objectstack#12278, Route 1) — once the runtime honours
enabled: false, the retained residue is inert data rather than live behaviour, which is the premise that makes C's "keep" safe.State:
needs-user-decision→pm:blocked,Blocked-by: objectstack-ai/objectstack#12278.Generated by Claude Code
huangyiirene commentedon Aug 28, 2026
解锁:
pm:blocked→pm:queue阻塞源 objectstack#12278 已关闭(2026-08-28T05:06:43Z,合并 PR objectstack#12737)。本卡
Blocked-by:边已放电。按本卡自己写的放电条件核对
本卡把阻塞条件写得很明确:
⛔ 本席没有凭 PR 标题判定,读了 diff。
approval-service.ts的runEscalations现在确实读这个键:⇒ 放电条件成立,本卡可进队列。但有两条实测限定,派发时必须带上。
ESCALATION_ENABLED_FLIP_CUTOFF_MS = 2026-08-28T00:00:00Z。在该时刻之前创建的请求快照,即使存了enabled: false也照旧升级 —— 因为翻转前的 executor 会把旧 schema 默认值false物化进node_config_json,「作者写的 false」和「默认值落下来的 false」在存储里逐字节相同。⇒ 本卡「retained residue 是惰性数据」的前提,对割点之后新开的请求成立;割点之前的存量请求随 pending 排空而自然退役。这不影响本卡的渲染层结论,但它是 C 方案「保留值是安全的」这句话的真实边界,⛔ 不要在实现里把它说成无条件。
⭐ 限定 2:同一个文件里新出现了一处漂移,本卡正文里没有
PR objectstack#12737 在加 enforcement 的同一次改动里把 spec 默认值翻了:
packages/spec/src/automation/approval.zod.ts——enabled: z.boolean().default(true)(原.default(false))packages/spec/authorable-defaults/automation.json——automation/ApprovalEscalation:enabled = true而 objectui 侧(实测于
babe956fb69238a6f135b25a8afcaa930950d7c0)仍是旧值:⇒ 实测结论:这两个字面量现在互相矛盾 —— 发布出去的 spec 默认
true,检查器声明默认false。⇒ 推论(未实测,留给实现者验证):对一个 config 里省略了
enabled的节点,检查器开关显示为关,而服务端按开处理;且showWhen: { field: 'escalation.enabled', equals: ['true'] }会把timeoutHours等依赖字段一并藏起来。若成立,这与本卡要修的「藏起来但存着」是方向相反的另一个缺陷 —— 声明的默认与生效的默认不一致。⛔ 本席不裁这条。它落在本卡将要编辑的同一个文件、同一次派发面上,所以作为派发输入挂在这里而不另立卡;若实现席位判定它超出本卡申报面,由该席位拆卡,不要顺手改掉也不要静默吞掉。
未变的部分
2026-08-27 的 Option C 裁决原样有效:保留值 + 显式「inactive values retained」提示,⛔ 不剪枝、不改保存路径、不动
isFieldVisible的 stored-value 重现规则。Generated by Claude Code
os-sales commentedon Aug 29, 2026
Claim:
domain:uiexecution seat, PM sessionsession_01CRJge11jso9TpXRWFt1Z49, round R14. Branch:claude/issue-6499-inactive-values-retained.The ruling is already given — this dispatch implements it, it does not re-open it
Maintainer, 2026-08-27, decision-inbox batch 3, verbatim 「同意」 — Option C. Quoting the ruling's own fences rather than paraphrasing them:
⇒ Your job is the affordance. ⛔ Not the save path, ⛔ not
isFieldVisible, ⛔ not pruning. If your measurement suggests the render-layer alone cannot express it, stop and report — do not reach for the save path.Unblocked, and the discharge was checked
Blocked-by: objectstack-ai/objectstack#12278is closed (2026-08-28T05:06:43Z, via objectstack#12737), and the unlock comment verified this card's own stated discharge condition rather than just the blocker's state.main(bf283414f) before implementing. The runtime half landed a day ago; this seat has been burned today by dispatching a card whose work had already shipped (#6598), so treat "the terrain may have moved" as the default, not the exception.Fences
packages/app-shell/src/views/metadata-admin/. Two sibling devs holdpackages/plugin-form/(The tabbed modal arm (ModalFormcontentLayout: 'tabbed') cannot carry a sectionvisibleWhenat all —FormFieldTabdeclares no predicate slot #6237) andpackages/components/src/renderers/basic/(finding(components): fivereadProps()copies object-spread a degenerate config bag — the third channel objectui#6752 / objectui#6760 left open #6783).packages/app-shell/src/views/metadata-admin/was measured (not modified) by finding(console): metadata-admin's 172,651 B eager closure is held by a REGISTRY CONTRACT, not by chunking — registerAppComponent takes a component value #6776 an hour ago; its worktree was reset and its branch is empty, so nothing of it is in your way — but if you find uncommitted state there, stop and report.content/docs/releases/.Deliverable
Add a changeset. Push and open a draft PR with
Fixes #6499. Report the coverage census (how manyshowWhengroups, and that you found them mechanically rather than by hand), and an ablation showing the affordance appears exactly when a hidden-but-stored value exists and not otherwise.Generated by Claude Code
claude commentedon Aug 29, 2026
os-dev-report
{ "issue": 6499, "status": "done", "branch": "claude/issue-6499-inactive-values-retained", "pr": "https://github.com/objectstack-ai/objectui/pull/6792", "premise_still_valid": true, "summary": "Implemented the 2026-08-27 Option C ruling as a render-layer affordance only. New pure read inactiveRetainedKind(field,node,fields) in flow-node-config.ts reports a field on screen ONLY because the isFieldVisible stored-value re-show rule fired, distinguishing 'controller-off' from 'no-controller' (the __legacy__ render-only keys, where no toggle exists to point the author at). FlowNodeConfigField renders the notice beside the control with a deliberate Clear value action (omitted when readOnly); clearing is an ordinary setField commit of undefined, byte-identical to the author emptying the control by hand. SHARPENING OF THE PREMISE: the residue was never actually hidden - isFieldVisible already re-shows any stored value, so those fields render as ordinary live-looking controls. The defect is inert config wearing a live control's clothes, not invisible config; the card's own wording implies the latter. isFieldVisible's stored-value clause is behaviourally unchanged - only its controller-resolution tail moved into a named helper (controllerAdmits) so the affordance and the visibility filter share one definition and cannot drift; ablation A4 is the tripwire proving the rejected prune shape cannot return silently. No save-path change, no deletion. Terrain re-verified: origin/main had moved past the dispatch's bf283414f to 107babef6 (my BASE), and the fenced metadata-admin dir was clean on arrival as predicted.", "tests": "ALL exit codes captured BEFORE any pipe (redirect-then-capture); each verdict quoted from the gate's own output, never from a bare $?. | UNION at the final commit abdbac92b: pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ EXIT=0 -- 'Test Files 218 passed (218)', 'Tests 2287 passed | 1 skipped (2288)'. | tsc --noEmit (packages/app-shell) EXIT=0. tsc -p tsconfig.test.json --noEmit --listFiles EXIT=0 AND both new test files verified present in the checked set (2 hits) - so the typecheck genuinely covers them rather than excluding *.test.ts. | pnpm --filter @object-ui/app-shell lint EXIT=0 -- gate prints '2791 problems (0 errors, 2791 warnings)'. Warnings are deliberately not failed on (lint.yml says so in a comment). | Gate set derived from my ACTUAL changed paths. objectui has NO scripts/pm/dispatch-gates.mjs - that script lives only in objectstack and, fed objectui paths, would have answered about the wrong repo - so the set was derived by hand from package.json + .github/workflows: check:i18n-keys EXIT=0, check:i18n-drift EXIT=0, check:designer-field-key-parity EXIT=0 ('designer-field-key-parity: OK'), check-changeset-presence EXIT=0, check-changeset-no-major EXIT=0, check-control-bytes EXIT=0, plus an independent grep -naP control-byte sweep of my touched files (clean). | LINT SCOPE MEASURED RATHER THAN ASSUMED: repo-wide 'eslint . --no-inline-config' ran in FULL - 3948 files (population from eslint's own config, count from --format json), 90 pre-existing errors across 75 files, 0 of them in any file this PR touches. eslint.config.js configures NO type-aware linting (0 hits for projectService / parserOptions / project:), so this diff structurally cannot move an untouched file's verdict. | CENSUS (mechanical, at runtime, not by grep and not by hand): 24 canonical FLOW_NODE_CONFIG keys parsed from the source table itself; 32 names enumerated (canonical + picker options + aliases); 146 descriptor fields; 33 carry showWhen; 9 (name,controller) buckets collapsing to 7 canonical groups; 5 distinct controllers. The type list is parsed from source rather than taken from FLOW_NODE_TYPE_OPTIONS because notify / parallel_gateway / join_gateway are real table keys the picker does not offer - using the option list alone silently missed them, which I caught and fixed mid-census. | CONTROL PROVING THE SCAN DISCRIMINATES: 24 of the 32 enumerated node types carry ZERO showWhen fields (create_record, connector_action, end, loop, ...). A scan that simply tagged everything would report 32 groups, not 8 buckets. The bucket count, the 33 gated-field total and the 24 ungated-type count are ASSERTED in the test, so any drift turns it red rather than quietly invalidating the census. | Both RUNTIME producers are covered, each with its own negative control: jsonSchemaToFlowFields (an engine-published configSchema whose nested object has an enabled: boolean) and connectorInputFields (the same, re-prefixed under connectorConfig.input) mint groups no source file contains; the controls (no enabled sibling / flat schema) yield 0 gated fields. Both funnel through the single isFieldVisible filter at FlowNodeInspector.tsx:229, which is why one predicate covers them. | ABLATION - run against the COMMITTED tree. No rebuild was needed, and none could have masked a stale read: packages/app-shell has NO dist/ at all and the tests import the mutated modules by RELATIVE path, so every mutation changed the outcome with no build step - which is itself the proof the tests read mutated source rather than an artifact. Every leg, mutation AND restore, was confirmed on disk by git hash-object against the HEAD blob (empty hash treated as FAILURE with a loud non-zero exit); each restore additionally asserted 'git diff HEAD' empty; restore used 'git checkout HEAD -- ABSOLUTE_PATH' built from git rev-parse --show-toplevel, under a trap on EXIT INT TERM. Baseline 25/25 green and post-restore baseline 25/25 green, so nothing was left mutated. | A1 (delete the notice block from FlowNodeConfigField): MOVES - 5 component tests red. DOES NOT MOVE - all 14 predicate tests stay green, so the layers are genuinely separable and the predicate suite is not accidentally covering rendering. | A2 (inactiveRetainedKind always returns null): MOVES - 7 predicate + 5 component red, every 'appears/flags' case. DOES NOT MOVE - 7 predicate + 6 component stay green: every 'does NOT appear' case, plus 'does not clear anything until the author clicks' and both re-show-rule guards. | A3 (remove the controller check so it fires whenever a value is stored): MOVES - only the discriminating negatives, 4 predicate + 2 component ('does NOT flag while the controller is on', 'an ordinary live field renders with no notice'). DOES NOT MOVE - 10 predicate + 9 component stay green, including EVERY 'appears' assertion, so A3 isolates the discrimination and nothing else. | A2 union A3 IS the 'exactly when' claim, each direction failing independently: A2 proves the affordance is not vacuous, A3 proves it is not indiscriminate. | A4 (delete the stored-value re-show rule from isFieldVisible - the shape Option A would have produced): MOVES - the guard test 'still shows a hidden-but-stored field (the rule the prune option would have inverted)' goes red, plus 3 predicate + 5 component. So the ruling's rejected option cannot be reintroduced silently.", "mcp_calls": "3 - issue_read get, issue_read get_comments, and one search_issues that FAILED with 'API rate limit already exceeded for user ID 319429713'. Everything else went through repo-scoped REST (open-issue list for dedupe, upstream spec file read, PR create, PR body read-back, this comment), so the GraphQL pool took only those three.", "channel_note": "DECLARED CHANNEL SWITCH. The REST /search/issues endpoint is BLOCKED at this proxy (HTTP 403: 'sessions are bound to their configured repositories. Use repository-scoped endpoints'), and the one targeted MCP search_issues fallback then failed on the GraphQL rate limit. Dedupe completed on a third channel: repo-scoped GET /repos/objectstack-ai/objectui/issues (3 pages, 240 open issues) + local grep. Controls validated that read - #6499 itself hit (1) and 'inspector' hit (2) - so a zero result would have been a real zero rather than a dead channel.", "open_questions": [], "out_of_scope_findings": [ "ALREADY FILED as #6620 (open, pm:on-hold, unassigned) - no duplicate created. It is the mirror defect the unlock comment predicted: @objectstack/spec flipped ApprovalEscalation.enabled to .default(true) while the inspector still declares defaultValue 'false'. I RE-MEASURED it and the unlock comment's inference does NOT yet reproduce on this tree: the INSTALLED @objectstack/spec is 17.2.0 with .default(false), so inspector and spec AGREE today; objectstack main does carry .default(true) (read via REST), so the next spec bump makes it live. #6620's body already states exactly that latency condition, so nothing needed adding. My change follows that fix automatically (inactiveRetainedKind resolves controllers through the same defaultValue); one assertion is deliberately coupled to the current literal, will flip to toBeNull() when #6620 lands, and carries a comment saying the failure is the intended signal.", "OBSERVATION, folded into #6620 rather than filed separately - a near-duplicate against an on-hold card would be noise; PM to split it out if it disagrees. escalation.notifySubmitter declares NO defaultValue in the inspector while the INSTALLED spec 17.2.0 already has notifySubmitter: z.boolean().default(true). Same class as #6620 but LIVE TODAY rather than latent: an unset key renders the checkbox unchecked while the runtime notifies. #6620 frames its fix as 'the toggle default follows the consumed spec default', which covers this field too.", "DISPATCH-ORDER CORRECTION, no card needed. The order warned that check:i18n-keys requires any author-facing string in packages/i18n/src/locales/en.ts with all ten packs for all-locales-key-parity. Measured, that is FALSE for this surface: metadata-admin/i18n.ts is registered in that gate's EXCLUDED_TRANSLATORS with forwardedScope covering this directory, engine.* keys are en+zh in that one file BY DESIGN, and nothing outside it reads ENGINE_STRINGS_EN/_ZH. Following the order literally would have added ten pack entries the gate does not want. check:i18n-keys passes as written." ] }Generated by Claude Code
claude commentedon Aug 29, 2026
ACCEPT — PR #6792
Verification. Check-runs table on head
abdbac92b:total_count=30, 30 rows, 27success+ 3skipped, zero non-terminal.Two corrections that are mine, recorded as mine
1. My dispatch order was wrong about a gate. The order warned that
check:i18n-keysrequires any author-facing string to land inpackages/i18n/src/locales/en.tswith all ten packs for all-locales-key-parity. Measured, that is false for this surface:metadata-admin/i18n.tsis registered in that gate'sEXCLUDED_TRANSLATORSwith aforwardedScopecovering this directory, theengine.*keys are en+zh in that one file by design, and nothing outside it readsENGINE_STRINGS_EN/_ZH. Following my order literally would have added ten pack entries the gate does not want.⛔ The failure mode is the same one this seat has hit repeatedly today: a gate requirement stated from memory rather than measured. Going forward, a gate claim in a dispatch order is either measured on the base or marked explicitly as unverified.
2. The card's premise was overstated, and the dispatch carried that wording. The residue was never actually hidden:
isFieldVisiblealready re-shows any stored value, so those fields render as ordinary, live-looking controls. The defect is inert config wearing a live control's clothes, not invisible config — which is a different and better-stated problem than the one the card described. Accepting the sharpened framing.What makes this report hold up
⭐ The census discriminates, and there is a control proving it. 24 of the 32 enumerated node types carry zero
showWhenfields. A scan that simply tagged everything would have reported 32 groups rather than 8 buckets — so the bucket count is a reading. Asserting the bucket count, the 33 gated-field total and the 24 ungated-type count in the test means drift turns it red instead of quietly invalidating the census.Also right: parsing the type list from the source table rather than from
FLOW_NODE_TYPE_OPTIONS, becausenotify/parallel_gateway/join_gatewayare real table keys the picker does not offer. Catching that mid-census and saying so is worth more than a clean-looking number.⭐ A4 is the pin I most wanted and did not ask for. Ablating the stored-value re-show rule — the shape the rejected Option A would have produced — turns a named guard red. That means the ruling's refused option cannot be reintroduced silently later. A ruling that only constrains the implementer at implementation time is worth much less than one with a tripwire behind it.
A2 ∪ A3 is the "exactly when" claim, each direction failing independently — A2 proves the affordance is not vacuous, A3 proves it is not indiscriminate. That is the right shape for a conditional affordance, and neither leg alone would have established it.
Also accepted: both runtime producers covered with their own negative controls; mutation and restore proven by blob hash against the HEAD blob rather than by an editor's exit code;
isFieldVisible's stored-value clause behaviourally unchanged, with only its controller-resolution tail extracted so the affordance and the filter cannot drift apart.On the out-of-scope observation — splitting it out, overruling the fold
The report folded
escalation.notifySubmitterinto #6620 rather than filing it, and explicitly left the call to me. I'm splitting it, and the reason is a state-machine one rather than a taste one:#6620 is
pm:on-hold. Its restart condition is a future@objectstack/specbump, because its own defect (escalation.enabled) is latent — installed spec and inspector agree today. ButnotifySubmitteris described as live today. Folding a live defect into a card parked behind an unrelated restart condition is exactly the shape this lane refuses: a bug held on hold is a bug hidden.Verified by this seat before splitting, so the new card does not rest on relay:
objectuiorigin/maind06059f24—packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts:770declaresescalation.notifySubmitterwith nodefaultValue.objectstackorigin/main7404925—packages/spec/src/automation/approval.zod.ts:636isnotifySubmitter: z.boolean().default(true), andauthorable-defaults/automation.json:17carries the same.@objectstack/specdid not resolve from the shared checkout, so the live-vs-latent distinction rests on the report's measurement, and the new card says so.Generated by Claude Code