Skip to content

fix(app-shell): name retained-but-inactive values in the metadata-admin inspectors - #6792

Open
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-6499-inactive-values-retained
Open

fix(app-shell): name retained-but-inactive values in the metadata-admin inspectors#6792
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-6499-inactive-values-retained

Conversation

@claude

@claude claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #6499

Implements the maintainer ruling of 2026-08-27 (decision-inbox batch 3, verbatim 「同意」) — Option C: keep the values, name the state. Render-layer only: no save-path change, no data deletion, and isFieldVisible's stored-value re-show rule is behaviourally unchanged.

What was actually wrong on screen

showWhen gates rendering, and isFieldVisible additionally re-shows any field that already holds a stored value — deliberately, "so existing config is never hidden". Those two rules together mean the residue is not hidden at all: an author who enabled a controller, filled its dependents, then switched the controller back off keeps seeing those fields as ordinary, live-looking controls. The stored config and the switch beside it disagree, and nothing on screen said which one was in effect.

So the fix is not "reveal something hidden" — it is to stop rendering inert config in a live control's clothes.

The change

  • inactiveRetainedKind(field, node, fields) (flow-node-config.ts) — a pure read reporting a field on screen ONLY because the stored-value re-show rule fired. It distinguishes 'controller-off' (a real toggle the author can switch back on) from 'no-controller' — the __legacy__ render-only keys, where no toggle exists and telling the author to go find one would be a fresh lie on the very screen whose defect was showing inert config as live.
  • controllerAdmits() / hasStoredValue() split out of isFieldVisible so the affordance and the visibility filter share one definition of "the controller says yes". Duplicating it would let them drift, and the drift would be silent — the affordance would quietly stop annotating the fields it exists for. ⚠️ The stored-value re-show rule itself (the early return true) is untouched; only the controller-resolution tail moved into a named helper. The 40 pre-existing isFieldVisible assertions in flow-node-config.test.ts are the regression net, and ablation A4 below is the tripwire against the rejected option.
  • FlowNodeConfigField renders the notice beside the affected control with a deliberate "Clear value" action. Read-only inspectors show the notice without the action. Clearing is an ordinary field commit of undefined through the existing setField — byte-identical to the author emptying the control by hand.
  • Three engine.* strings in metadata-admin/i18n.ts, en + zh.

Coverage census — mechanical, with the control

Found by enumerating the descriptor tables at runtime, not by grep, and not by hand. The type list is parsed from the source table itself rather than from FLOW_NODE_TYPE_OPTIONS, because notify / parallel_gateway / join_gateway are real FLOW_NODE_CONFIG keys the picker does not offer — using the option list alone silently missed them.

canonical FLOW_NODE_CONFIG keys 24
node-type names enumerated (canonical ∪ picker options ∪ aliases) 32
descriptor fields across them 146
fields carrying showWhen 33 (25 canonical + 8 via the script_task / signal alias pairs)
showWhen groups 9 buckets → 7 canonical
distinct controllers 5

The 7 canonical groups: start::triggerType (9 dependents) · start::__legacy__ (1) · decision::__legacy__ (1) · script::__legacy__ (5) · approval::escalation.enabled (4) · wait::waitEventConfig.eventType (2) · boundary_event::boundaryConfig.eventType (3).

The control that proves the scan discriminates: 24 of the enumerated node types carry zero showWhen fields (create_record, connector_action, end, loop, …). A scan that simply tagged everything would report 32, not 8. This is asserted, not just observed — the coverage test fails if the bucket count, the gated-field total (33) or the ungated-type count (24) drifts.

Two runtime producers are covered too, and they mint groups no source file contains: jsonSchemaToFlowFields (any engine-published configSchema whose nested object has an enabled: boolean) and connectorInputFields (the same, re-prefixed under connectorConfig.input). Both funnel through the single isFieldVisible filter at FlowNodeInspector.tsx:229, which is why one predicate covers groups that do not exist statically. Each has its own negative control in the tests (a schema with no enabled sibling / a flat schema → 0 gated fields).

Ablation — the affordance appears exactly when a hidden-but-stored value exists, and not otherwise

Run against the committed tree. packages/app-shell has no dist/ at all and the tests import the mutated modules by relative path, so there is no stale-build hazard: every mutation changed the outcome with no rebuild step, which is itself the proof the tests read the mutated source. Every leg — mutation and restore — was confirmed by comparing git hash-object against the HEAD blob, never by an editor's exit code; each restore additionally asserted git diff HEAD empty. Baseline and post-restore both 25/25 green, so nothing was left mutated.

ablation assertions that MOVE assertions that do NOT move
A1 — delete the notice block from FlowNodeConfigField 5 component tests red all 14 predicate tests stay green — the layers are genuinely separable, and the predicate suite is not accidentally covering rendering
A2inactiveRetainedKind always returns null 7 predicate + 5 component red (every "appears/flags" case) 7 predicate + 6 component stay green: every "does NOT appear" case, does not clear anything until the author clicks, and both re-show-rule guards
A3 — remove the controller check (fire whenever a value is stored) only the 4 predicate + 2 component discriminating negatives red: does NOT flag while the controller is on, an ordinary live field renders with no notice 10 predicate + 9 component stay green — including every "appears" assertion, so A3 isolates the discrimination and nothing else
A4 — delete the stored-value re-show rule from isFieldVisible (the shape Option A would have produced) still shows a hidden-but-stored field (the rule the prune option would have inverted) goes red, plus 3 predicate + 5 component the ruling's rejected option cannot be reintroduced silently

A2 ∪ A3 is the "exactly when" claim, each direction failing independently: A2 proves the affordance is not vacuous, A3 proves it is not indiscriminate. A4 is the guard on the half the ruling forbade touching.

Commands and exit codes

Exit codes captured before any pipe (redirect-then-capture), and each verdict quoted from the gate's own output rather than from $?.

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
    both new test files present in the checked set (2 hits) - not excluded
pnpm --filter @object-ui/app-shell lint                              EXIT=0
    "2791 problems (0 errors, 2791 warnings)"
node scripts/check-changeset-presence.mjs                            EXIT=0
node scripts/check-changeset-no-major.mjs                            EXIT=0
node scripts/check-control-bytes.mjs                                 EXIT=0
pnpm run check:i18n-keys                                             EXIT=0
pnpm run check:i18n-drift                                            EXIT=0
pnpm run check:designer-field-key-parity                             EXIT=0

The union above was re-run on the final commit abdbac92b after the last edit, and this body cites that run.

Lint scope, stated rather than assumed: repo-wide eslint . --no-inline-config was run in full — 3948 files (population read from eslint's own config, count read 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.

⚠️ On check:i18n-keys: the dispatch order warned that author-facing strings need keys in packages/i18n/src/locales/en.ts across all ten packs. Measured, that does not apply here: metadata-admin/i18n.ts is a module-local label table registered in that gate's EXCLUDED_TRANSLATORS, with forwardedScope covering this directory. engine.* keys are en + zh in that one file by design, and the gate passes. Nothing external reads ENGINE_STRINGS_EN / _ZH, so there is no pack-parity obligation to satisfy — following the dispatch's instruction literally would have added ten pack entries the gate does not want.

Scope

Confined to packages/app-shell/src/views/metadata-admin/ and its tests; nothing in packages/plugin-form/ (#6237) or packages/components/src/renderers/basic/ (#6783). No content/docs/releases/ edit. The fenced directory was clean on arrival, as the dispatch predicted.

⚠️ Note for whoever lands #6620 (the mirror defect: spec flipped ApprovalEscalation.enabled to default(true) upstream, inspector still declares defaultValue: 'false'). Re-measured here on 2026-08-29 and it is still latent — installed @objectstack/spec is 17.2.0 with .default(false), so the two agree today; main upstream has .default(true), so the next spec bump makes it live. inactiveRetainedKind resolves controllers through the same defaultValue, so it follows that fix automatically. One assertion in flow-node-config.inactiveRetained.test.ts is deliberately coupled to the current literal and will flip to toBeNull() when #6620 lands; the assertion carries a comment saying so, so the failure reads as the intended signal rather than a break.


Generated by Claude Code

claude added 2 commits August 29, 2026 14:09
…f showing them as live

`showWhen` gates rendering only, and `isFieldVisible` additionally re-shows any
field that already holds a stored value — deliberately, "so existing config is
never hidden". So an author who enabled a controller, filled its dependents and
switched the controller back off keeps seeing those fields as ordinary, live
controls: the stored config and the switch beside it disagree, and nothing on
screen said which was in effect.

Per the maintainer ruling of 2026-08-27 (Option C) the values are KEPT and the
state is made explicit. Pruning on save was rejected outright — it silently
discards config an author entered and inverts the rule above.

- `inactiveRetainedKind()` — a pure read reporting a field on screen ONLY
  because the stored-value re-show rule fired, distinguishing 'controller-off'
  from 'no-controller' (the `__legacy__` render-only keys, where there is no
  toggle to point the author at).
- `controllerAdmits()` / `hasStoredValue()` split out of `isFieldVisible` so the
  affordance and the visibility filter share ONE definition and cannot drift.
  The stored-value re-show rule itself is unchanged.
- `FlowNodeConfigField` renders the notice with a deliberate "Clear value"
  action; read-only inspectors show the notice without it.

Coverage is pinned mechanically over all 33 gated fields in the descriptor
tables plus both runtime producers. Render-layer only: no save-path change and
no data deletion — clearing is an ordinary field commit.
…e assertion

The spec flipped ApprovalEscalation.enabled to default(true) upstream; once this
repo consumes a release carrying it, the inspector descriptor must follow and
this assertion flips to toBeNull(). Say so at the assertion so the failure reads
as the intended signal rather than a break.
@github-actions github-actions Bot added the tests label Aug 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 45 chunks) 3174.6 KB 3222.7 KB
Main entry chunk (gzip) 148.2 KB 350 KB
Entry file index-CmYkPDtN.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 511.75KB 116.33KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 240.93KB 60.76KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.92KB 12.93KB
plugin-charts (index.js) 64.68KB 18.35KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.48KB 34.51KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.46KB 62.46KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 133.03KB 32.64KB
plugin-gantt (index.js) 165.23KB 40.37KB
plugin-grid (index.js) 201.57KB 54.55KB
plugin-kanban (index.js) 53.14KB 14.64KB
plugin-list (index.js) 113.15KB 27.59KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 28.95KB 8.33KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.87KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 76.75KB 25.49KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.40KB 1.20KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Standing down on this PR's second queue ejection — deliberately, and here is exactly why

What happened. Enqueued 16:03Z, ejected 17:01Z by github-merge-queue[bot] with reason: CI_TIMEOUT — ~58 minutes, having reached 17 of 24 green with 3 still running and 2 still queued. It was five checks short of done when the queue's wait ceiling expired.

This is not this PR's failure, and that is measured rather than asserted. On the ejected queue head, zero checks failed — the counts were 17 success, 2 skipped, 3 in_progress, 2 queued. Nothing red. The PR itself is mergeable_state: clean and its own head has been green at 30/30 since before it was first enqueued.

The cause is runner contention, repo-wide. At the time this entry was running, the pool was carrying two merge-queue entries (24 jobs each, run speculatively in parallel — the second entry is not "waiting", it is taking runners) plus three pull requests at 29–30 jobs each: roughly 138 jobs against a pool that, measured at 16:30, had not started a single job on either queue head 27 minutes after they were enqueued. The sibling entry #6791 was ejected the same way an hour earlier, with the same reason code.

What I am doing instead of re-queueing immediately

Not re-queueing this PR right now, and not because it needs anything.

The queue currently holds exactly one entry (#6791, auto-rebased onto 26896c689 after this ejection). That is the strictly serial state, and it is the condition most likely to get something through. Adding this PR back would recreate the two-entry contention that has now produced two CI_TIMEOUT ejections in a row — and each ejection is not free: the re-queue rebases onto a new head, so #6791's previous 14 green checks were discarded and it is running from zero again.

⇒ This PR is re-queued as soon as #6791 merges, one entry at a time, until the pool recovers. Four other verified-green PRs from this seat (#6796, #6800, #6801, #6811) are being deliberately held out of the queue for the same reason. That hold is a throughput decision, not a review one — every one of them is green and accepted.

One re-run has already been spent on the sibling entry (#6791, re-queued once at 16:03 after its own timeout). I am not spending a second speculative re-queue on this PR while the condition that caused both ejections is still present. If a third ejection happens on a single-entry queue, that is a different signal and it belongs to the merge queue's own configuration — the merge_group check set and its wait ceiling — which is the escalation already recorded on #6082.


Generated by Claude Code

@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

⛔ 更正我上一条里的一句话:拥堵不是「repo-wide」,是本席自己造成的

上一条我写:

The cause is runner contention, repo-wide.

**测了之后,这句是错的。**17:26Z 采样最近更新的 14 个 open PR,逐个读 check-runs 统计处于 queued/in_progress 的 job:

PR 占用 job 是谁的
#6811 27 本席 R16
#6808 11 本席 R16
#6807 5 本席 R16
其余 11 个 open PR 0

⇒ **43 个在跑的 job 全部来自本席自己刚派发的那一轮。**样本里没有任何别的席位在占用 runner。

⚠️ 我第一次统计时把它们算成了"别人的",因为 dev 开的 PR 作者身份是 claude[bot] 而不是 issue 上的 assignee os-sales —— 同一个身份下我分不出席位,只能按分支/卡号认。这是我在两个读数上都该注意的事。

这改变了结论,不只是措辞

「把合并队列串行化」并不能串行化 runner 池:PR 的 CI 和 merge_group 的 CI 抢同一批机器。所以真正的约束不是队列深度,而是本席同时在飞的 CI 总量

⇒ 上一条里"队列只剩一条就是最可能通过的状态"这个推断不成立:队列确实只剩一条,而它 25 分钟里仍然 0 个 job 开跑,因为本席自己的三个 R16 PR 排在它前面。

仍然照旧执行的部分:一次只挂一个队列条目(那仍是对的,只是不充分),已绿的 PR 继续以 draft 形式按住(它们的 CI 已跑完,不占 runner)。

新认识:并发上限买到的是并行编写,但每一轮结束时会同时产生 3 份 PR CI,而那份负载会和本席自己的落地竞争。这是一个真实的取舍,已记在座位贴上,并会向维护者报告 —— ⛔ 不由本席单方面改并发上限,那是维护者 2026-08-29 的明令。

自我修正的边界:本席只采样了最近更新的 14 个 open PR,且看不到同组织其它仓对同一 runner 池的占用。⇒ 「没有别的席位在跑」的含义是「本次采样范围内没有」,不是「不存在」。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata-admin inspectors: showWhen-hidden fields keep their stored values on save — the UI manufactures declared-off-but-live config shapes

1 participant