Skip to content

feat(app-shell): Setup shows a time-triggered flow disabled by deployment policy with the platform's reason, not as a binding failure (objectui#9217) - #11271

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-9217-flow-policy-disabled-status
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-9217-flow-policy-disabled-status

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #9217
Clause-②: no

Setup › Packaged automation now shows the platform's reason for a packaged flow the engine has not armed. The sentence appears verbatim, in muted text, under the flow name. This is option B from the card's decision: the seat's answer is on the card, comment 5919298624.

Why

The page read only name and enabled from GET /api/v1/automation/_status. A time-triggered flow left unarmed by the deployment's scheduled-work switch (OS_AUTOMATION_SCHEDULED_WORK_ENABLED unset, the default in every posture) showed as a plain "On", the same as a flow that runs. A flow whose trigger failed to bind looked exactly the same. @objectstack/spec 17.5.0 now carries FlowRuntimeState.reason, and its contract says "Consumers RENDER it; ⛔ do not parse it".

What changed

  • packagedFlows.ts: FlowRuntimeStateRow declares reason?: unknown. joinPackagedFlows copies it onto the row only when it is a non-empty string. Otherwise the key is absent, the same way the engine omits it.
  • PackagedAutomationPage.tsx: a row with a reason renders it under the flow name as a muted paragraph (text-muted-foreground). There is no role=alert and no destructive class, and the text is never parsed or compared. A row without one renders exactly as before. The header's ADR-0126 §7.4 note gains one sentence recording the exception.
  • No dependency, no locale key and no copy of any platform sentence is added. The test file transcribes the platform's policy sentence as fixture text only, and it is byte-identical to SCHEDULED_WORK_DISABLED_REASON in @objectstack/types 17.5.0 (checked with a node equality against the installed package).
  • One changeset: @object-ui/app-shell minor.

Pins

  • packagedFlows.test.ts, describe "the unbound reason (objectui#9217)": a string reason passes through unchanged; the key is absent on rows that send none (older backend included); non-string or empty values (42, true, null, an object, an array, the empty string) are ignored.
  • PackagedAutomationPage.test.tsx, describe "the platform reason for an unbound flow": a policy row shows the platform sentence verbatim, muted, and not as an alert, with no "binding failed" wording added (acceptance ① and ③); a binding-failure row shows its own sentence the same way (acceptance ②); a row without a reason keeps exactly its old flow-cell text (bound, disabled and bare older-backend rows); a non-string reason is ignored.

Evidence (HEAD d1a24b8fb, every run after the final commit, heavy runs under os-verify-lock)

  • pnpm exec vitest run --reporter=verbose packages/app-shell/src/views/setup/ gave Test Files 5 passed (5) and Tests 65 passed (65).
  • Ablation (ablation-replace.mjs, WRAP mode). The reason paragraph was replaced with null (anchor 1 to 0, blob 3c9713a98d6a to 6d25c6141bfd). The page suite went to Tests 2 failed | 12 passed (14). The two red tests are exactly the policy row and the binding-failure row; "row without a reason" and "non-string reason" stayed green. Restore was proven: the blob equals HEAD 3c9713a98d6a and git diff HEAD is empty. The subject is imported from source, not from dist, so no dist preflight applies.
  • turbo run build --filter='@object-ui/app-shell^...' --concurrency=2: 28/28 tasks successful. Then pnpm --filter @object-ui/app-shell type-check, which runs tsc --noEmit && tsc -p tsconfig.test.json, exited 0 with 0 error TS. Both projects include the changed files; tsconfig.test.json lists both test files.
  • pnpm exec eslint on the 4 changed source/test files: 4 files linted, 0 errors, 0 warnings (counts from --format json).
  • node scripts/check-changeset-presence.mjs, node scripts/check-control-bytes.mjs, pnpm check:new-line-citations (0 new), pnpm changeset:check, pnpm check:pending-changeset-literals and pnpm check:changeset-claims all exited 0.
  • The diff adds a markdown changeset, so node scripts/markdown-test-inputs.mjs --audit was run (exit 0), and so was pnpm exec vitest run scripts/__tests__/: Test Files 177 passed | 2 skipped (179), Tests 5371 passed | 2 skipped (5373).
  • Not run locally: the i18n checks (no locale key and no t() call moved) and the full pnpm test / pnpm lint farm, which is left to CI.

Acceptance notes

  • Premise, measured before any code. Before this change the Setup page showed no binding state at all. A render probe fed it a policy row and a binding-failure row, both enabled, bound false and carrying a reason. Both rendered identically, as name, "On" and "Clone", with neither sentence and no alert. So a failure row gaining its platform sentence is new behaviour that acceptance ② asks for, not a regression. The seat's answer withdrew the dispatch's "any other reason keeps today's rendering" on that measurement.
  • Not in this PR: the Studio Automations rail's FlowStatusDot still titles every enabled unbound flow "Enabled · no trigger (run manually)", including a policy-unbound schedule flow. Its file, StudioDesignSurface.tsx, is held by objectui#11232. The seat has recorded the finding and will file it as a sub-issue of objectui#9217 once that file is free.

Generated by Claude Code

…m and muted (objectui#9217)

Setup > Packaged automation read only `name` and `enabled` from
`GET /automation/_status`, so a time-triggered flow the deployment's
scheduled-work switch left unarmed showed as a plain "On", identical to a
flow whose trigger failed to bind and to one that runs.

`packagedFlows.ts` now carries `FlowRuntimeState.reason` (spec 17.5.0)
when the row sends a non-empty string, and leaves the key absent otherwise.
`PackagedAutomationPage` renders that sentence verbatim under the flow
name, in muted text. It is not an alert and has no destructive class, and
the text is never parsed or compared. Rows without a reason, including
those from older backends, render exactly as before. The page header's
ADR-0126 section 7.4 note records the exception.

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3583.8 KB 3607.4 KB
Main entry chunk (gzip) 149.9 KB 350 KB
Entry file index-CSnuPSBd.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) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
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) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
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) 570.05KB 136.37KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 228.91KB 63.54KB
fields (index.js) 261.24KB 66.29KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 41.23KB 11.53KB
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.99KB 0.87KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 216.32KB 44.56KB
plugin-detail (index.js) 244.74KB 64.46KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.69KB 44.62KB
plugin-gantt (index.js) 173.03KB 43.07KB
plugin-grid (index.js) 231.37KB 63.54KB
plugin-kanban (index.js) 48.46KB 15.12KB
plugin-list (index.js) 116.42KB 28.89KB
plugin-map (index.js) 23.50KB 7.82KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.05KB 9.67KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
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.81KB 3.58KB
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) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
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 (expression.js) 0.20KB 0.18KB
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.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d1a24b8fbaa7c99b6742a5978eae03654e725eb4
Local-runs: none

PR #11271 on card #9217. Net diff against main at the head: 5 files, +196 −1 — packagedFlows.ts, PackagedAutomationPage.tsx, their two test files, and one changeset. Inputs: the card body and all 7 comments (dispatch shape 5748065370, the two unlock scans, claim 5918245328, first-leg report 5918495690, the seat's answer 5919298624, follow-up report 5919772237); the PR body, file list and diff; the head's check-runs with filter=latest. Context reads at the head through contents: the two changed source files, PackagedAutomationPage.test.tsx, packages/app-shell/src/index.ts, package.json, tsconfig.json, and StudioDesignSurface.tsx for importers. Nothing built, run or re-run.

Check-runs on the head (the gate verdicts): 43 latest runs, all completed at judgment — 40 success, 3 skipped (dependabot, Test (coverage), Test (coverage shard)), 0 failure, 0 in progress. The success set includes Type Check, Lint, Test and Test (shard 1/8) through (shard 8/8), Test (dist pins), Build & E2E, Live E2E (informational), Bundle Analysis, Pre-Install Import Graph Check, Docs Route Eager Closure Check, Control Byte Scan, Line Citation Gate, Shell Escape Residue Scan, Inert vi.mock Specifier Check, Spec Main Shape Gate, Governed Surface Queue Guard, and the five changeset checks named in ② below. Twelve of them were still in progress when this review began and were waited for (about 16 minutes), inside the brief's cap.

① Derived judgments

The binding shape is the seat's answer B (5919298624): a row carrying a string reason renders it verbatim under the flow name in muted text, no role=alert, no destructive class, never parsed; a row without one renders exactly as today; no dependency, no locale key, no copy of a platform sentence. Everything the diff implies, judged:

  1. The defensive read of reason — right. FlowRuntimeStateRow gains reason?: unknown, typed like its siblings; the row already accepted any key through its [key: string]: unknown index signature, so the accepted wire shape of a _status row does not move. joinPackagedFlows takes state.reason only when typeof reason === 'string' and reason.length is above zero, spreading { reason } in and {} otherwise — so the key is ABSENT, not undefined-valued, on every other row. packagedFlows.test.ts pins that with Object.keys(row) on a bound row, a bare { name } older-backend row, and six non-string or empty values (42, true, null, an object, an array, the empty string). No sentence is compared, matched or parsed: the one predicate is a type check plus a length check.

  2. The verbatim, muted render — right. PackagedAutomationPage.tsx lines 427–434 at the head: row.reason gates a p element classed mt-2 max-w-prose text-sm text-muted-foreground whose only child is {row.reason} as a React text node — no t(), no interpolation, no role, no text-destructive, no icon — placed under the code machine name and before the existing role=alert refusal paragraph, which is untouched. Read line by line at the head, neither source file matches, compares, slices or regex-tests a reason value anywhere. The only regex over the sentence in the diff is the TEST's not.toMatch(/binding failed/i) on the policy row, which asserts that the PAGE adds no failure wording of its own — the distinction the spec's own pin draws.

  3. No-reason rows byte-identical to before, older backends included — right. With the key absent, row.reason is undefined and React renders nothing for it, so a no-reason row's DOM is the pre-change DOM. PackagedAutomationPage.test.tsx pins the flow cell's textContent for a bound row, a disabled row and a bare older-backend row; packagedFlows.test.ts pins the joined object with toEqual on the three pre-existing keys. The dev's deviation — an empty-string reason treated as absent — is right: it is a length check, not a text comparison, and an empty muted paragraph would show the operator nothing.

  4. The header note — right. One sentence, wrapped over three comment lines (head lines 39–41), placed as "the one exception" directly under the ADR-0126 §7.4 paragraph that says the page shows no bound/status column. It says verbatim and muted, never parsed, never styled as an error. The dev flagged the three-line wrap as a deviation; it is one sentence.

  5. The policy fixture byte-identical to the platform constant — right; not a copy in SOURCE. POLICY_REASON in PackagedAutomationPage.test.tsx is the 517-character SCHEDULED_WORK_DISABLED_REASON of @objectstack/types 17.5.0, and the follow-up report measured node equality true. Judged against what the rule guards: dispatch step 4 ("rendered from the platform's string, never from a second copy of it in this repo"), the claim's file-surface line, and the seat's answer (listed with "no dependency, no locale key") all bind the RENDERING path. This copy lives in a *.test.tsx fixture that the package build's tsconfig.json excludes (**/*.test.tsx), so it ships in no dist and sits on no production path; it drives an input-equals-output assertion that holds for any sentence, and the byte identity buys realism only — the em dash, backticks, parentheses and apostrophe all pass getByText's exact match. If the platform rewords the constant, the rendering follows the platform and the pin still holds; only the fixture's docblock goes stale, harmlessly. The same file already transcribes the runtime's refusal strings character-for-character under the same discipline (its header, property 3), and the new docblock discloses the transcription as fixture text. BINDING_FAILED_REASON is the same kind of fixture. Not a breach.

  6. The ablation leg — right, and consistent with the diff on its own evidence. Report 5919772237 and the PR body: WRAP mode replaced the reason paragraph with null; the page suite went from 14 green to 2 failed | 12 passed, the two red being exactly the policy-row and binding-failure-row pins, while "row without a reason" and "non-string reason" stayed green (they assert an absence, so they must); anchor 1 to 0 and back to 1; restore proven by blob 6d25c6141bfd back to 3c9713a98d6a with git diff HEAD empty. That restore blob is the head blob of PackagedAutomationPage.tsx in the diff's own index line (28ebf8178a..3c9713a98d), so the restored file is the reviewed one. The joiner has its own three pins in packagedFlows.test.ts (pass-through, absent key, ignored values), each of which dropping the spread or the length check would redden. Judged on the report and that cross-check; not re-run here.

  7. Acceptance ①–③ (the card, ruling G follow-up) — met under B. ① The policy row shows the platform's sentence, which itself names OS_AUTOMATION_SCHEDULED_WORK_ENABLED, muted, no alert, no destructive class, no added "binding failed" wording, and the activation switch still reads the engine's enabled (pinned checked). ② A binding-failure row shows the platform's failure sentence the same way. The card's "still shows the failure" rested on a pre-existing failure rendering that the first leg measured absent (5918495690: both rows rendered identically as name, On, Clone) and the seat withdrew in 5919298624; the failure IS shown, in the platform's words. ③ No reason-code shape is invented; the string is rendered.

  8. Accept set and public surface — nothing moves. @object-ui/app-shell's one . export is dist/index.js; src/index.ts exports PackagedAutomationPage (a no-props component, signature unchanged) and nothing from packagedFlows.ts, so FlowRuntimeStateRow and PackagedFlowRow stay module-internal, and StudioDesignSurface.tsx does not import the module. package.json is untouched: no @objectstack/types, no new dependency. No locale key — the sentence is not routed through t(), which is the rule, not an omission. No schema, no accepted set, no export.

② Semver level

.changeset/9217-packaged-flow-unbound-reason.md declares '@object-ui/app-shell': minor, the only released package the diff touches. Right: the diff adds a visible feature (a new rendered paragraph on an exported page) with no removal and no change to any existing row's output — minor, not patch, not major. The changeset body names the observable change and the unchanged cases without pasting the platform sentence. Changeset Declaration, Changeset Bump Policy, Changeset Claim Re-read, Changeset Fixed Group Check and Changeset Overwrite Report are success on the head.

Clause-②: no — the PR body carries it, matching claim 5918245328, and it is right: no objectui schema, accepted set, published type or export moves (①.8).

③ Boundary flags

Dev flags from 5919772237 (and the first leg 5918495690), each answered:

  • The first-leg premise probe lived briefly inside the worktree (zzprobe9217.test.tsx): never staged or committed; the PR's file list is the five files above and carries no residue. Answered.
  • The header note is one sentence over three physical lines: read at the head; one sentence. Answered (①.4).
  • An empty-string reason is treated as absent: a length check, not a text comparison; pinned. Answered (①.1, ①.3).
  • The closure build ran through turbo run build with a scratchpad cache dir rather than pnpm --filter build: a process choice; the head's Build & E2E, Type Check, Lint and eight test shards are success, which is what the local run stood in for. Answered.
  • premise_still_valid: false in both legs: the card's rendering premise ("bound: false today reads as a problem" on the Setup page) was measured false, the seat withdrew its order line on it, and the acceptance that survived is what this PR implements. Answered by the seat; consistent with the diff.

open_questions: [] in the follow-up. The first leg's one question (A/B/C) was answered B by the seat in 5919298624, inside the card's own acceptance and the installed contract's own words ("Consumers RENDER it; do not parse it"); the diff implements B point for point. Nothing to escalate.

Carried note — not folded, not a finding against this head: the Studio Automations rail's FlowStatusDot titles a policy-unbound schedule flow "Enabled · no trigger (run manually)". Recorded by the seat in 5919298624; its file StudioDesignSurface.tsx is held by objectui#11232 (seat 1), and the seat files it as a sub-issue of #9217 once that file is free, following answer B. The card's body names the Setup surface (packagedFlows.ts) and acceptance ① names the Setup flows view, so this PR closes the card as written; the rail is the seat's follow-up.

Implemented-by: claude/issue-9217-flow-policy-disabled-status
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 21:26
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 1cb3732 Sep 30, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-9217-flow-policy-disabled-status branch September 30, 2026 21:45
huangyiirene pushed a commit that referenced this pull request Sep 30, 2026
Carries main through PR #11267, #11268, #11269, #11271, #11273 and #11274.
No conflicts.

Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…, mirror two chatbot keys, file eighteen runtime slots by name (objectui#6152, round 4) (objectstack-ai#11285)

Refs objectstack-ai#6152
Clause-②: yes (narrowing). The two mirrors widen `@object-ui/types/zod`
and the strict authoring face to keys the TypeScript face already
invited and the renderers read (`requestBody`, `floatingConfig`). The
four retirements (`label.content`, `report.chartConfig`,
`report.reportType`, `detail-view.autoDiscoverRelated`) narrow both
faces at once, under the startup no-gradualism rule. It is priced in
`.changeset/6152-unmirrored-round4.md` as `minor`, with the break
stated.

Round 4 of objectui#6152, dispatched under claim `5919877490` (the
`domain:devx` seat 2, session `session_01TdiauJaVCHuj45EzZGUxHh`, on the
maintainer's direct order). The rulings are the seat review
`5914490915`: Q1 B, Q2 A, Q3 B. ⛔ This is a draft from a dispatched dev:
do not mark it ready and do not enable auto-merge. The card stays open,
because `ObjectGridSchema`, the spec-derived pairs and three
`DataTableSchema` keys (below) are not this round's.

## What changed

1. **Q1 B: `LabelSchema.content` is retired.** It is `?: never` on
`form.ts` and a `retirementTombstone` on `form.zod.ts` that names the
kept spellings `text` and `label`. The `label` renderer drops its third
read limb (`schema.text || schema.label`). Re-measured: 0 documents
author it (JSON, Markdown JSON fences and typed literals); its only
author was a test in `form-renderers.test.tsx`, which now pins the
refusal. Which of `text` / `label` is canonical is not decided here.
2. **Q2 A: `DetailViewSchema` `activities`, `comments`, `history` and
`recordNavigation` go to `RuntimeOnlyDeclared` by name.** Each has a row
with its reason on `RuntimeOnlyNamedAllowList`, in round 3's shape. They
carry host-fetched runtime data, and `recordNavigation.onNavigate` is a
required function.
3. **Q3 B, under objectui#7654 ruling B: `displayMode` keeps its
TypeScript-only refusal.** It gets no zod arm, because stored designer
documents carry `displayMode: 'floating'` (pinned by
`chatbot-display-mode-retired.test.ts`). The ledger entries no longer
call the mirror half "owed"; the key stays listed by decision.
Separately, after a reader census for each:
- `ChatbotSchema.requestBody` is mirrored through the shared pick. All
three chatbot registrations read it (`body: schema.requestBody`).
- `ChatbotFloatingSchema.floatingConfig` is mirrored through a
module-private restatement of `FloatingChatbotConfig`. That restatement
judges the six members `FloatingChatbot` destructures, and carries the
`triggerIcon` tombstone's runtime half that the objectui#7654 tripwire
asked for.
4. **The three zero-read candidates are retired on both faces.** They
are `ReportComponentSchema.chartConfig`,
`ReportComponentSchema.reportType` and
`DetailViewSchema.autoDiscoverRelated`, each `?: never` plus a
`retirementTombstone`. `reportType`'s one in-code producer,
`specReportToPresentation` in `spec-report.ts`, no longer writes it, and
`LegacyReportPresentationLike` no longer declares it.
5. **`DataTableSchema`: 14 of 17 keys go to `RuntimeOnlyDeclared` by
name.** They are produced only in code, by `ObjectGrid`'s in-code
`data-table` node or `RelatedList`'s, and authored by no document. Three
keys fit none of the classes, so the round stopped at them (see
"Stopped" below).
6. **The pagination corpus fix, carried from round 3.**
`components-basic-pagination/with-item-count.json` no longer authors
`pageSize` / `totalItems`, which neither face declares and nothing
reads. The plaintext interface block on
`content/docs/components/basic/pagination.mdx` stops listing them, and
its section is retitled from "With Page Size" to "On a Later Page".
7. **Changesets.** One new: `.changeset/6152-unmirrored-round4.md`
(`@object-ui/types` minor, `@object-ui/components` minor, with the break
stated). Four dated, append-only corrections were added, each 2 lines
with frontmatter unchanged: `6639-objectgrid-title-mirrored.md` (the
stale "97 + 1 + 23" sentence),
`7654-floating-chatbot-trigger-icon-tombstone.md`,
`7655-chatbot-registration-authoring-faces.md` and
`9447-detail-view-handler-key-refusal.md`. Every other pending changeset
was re-read after the merges, including the 8 that landed since the
base, and none is made false by this round.

## Ledger totals, before and after

These are read by the file's own pins (`objectui#7279` derives every
figure) and re-counted with the TypeScript parser on the final head.

| ledger | before (`origin/main` `e420df3`) | after (`2cc4ab9`) |
| --- | --- | --- |
| `UnmirroredDeclared` | 9 entries / 44 keys: 3 / 14 spec-derived, 6 /
30 local | **6 / 20**: 3 / 14 spec-derived, 3 / 6 local |
| `RuntimeOnlyDeclared` | 4 / 9 | **4 / 27** |
| `RuntimeOnlyNamedAllowList` | 1 entry / 2 rows | 3 / 20 |
| cross-ledger | 2 of the 4 are in `UnmirroredDeclared`; union 11 = 9 +
2 | **1 of the 4**; union **9** = 6 + 3 |
| `KnownDrift` | 49 / 86 | 49 / 86, unchanged. This round does not edit
its total line; PR objectstack-ai#11269's 87 to 86 edit is kept as merged. |
| `WiderThanDeclared`, `MirroredUndeclared` | 3 / 3, 2 / 2 | unchanged |

The 24 keys that left `UnmirroredDeclared` moved by three routes, which
must not be added up as one repair: 2 were mirrored, 4 were retired, and
18 were reclassified by name. The reclassification is one move seen from
both ledgers: it is not new debt, and it mirrors nothing.

## Per-key table

| pair · key | measured (type-checker read census over 5548 sources;
authored census over tracked JSON, Markdown JSON fences and `type`
literals) | route |
| --- | --- | --- |
| `label` · `content` | 1 read (the third limb in `label.tsx`), 0
documents (1 test) | retired on both faces; the read is dropped |
| `report` · `chartConfig` | 0 typed reads. The untyped
`widget?.chartConfig` hits are plugin-dashboard's `DatasetWidget`
(`widget: any`, a dashboard widget), which plugin-report never imports.
0 documents | retired on both faces |
| `report` · `reportType` | 0 reads; one in-code producer
(`specReportToPresentation`); 0 documents | retired on both faces; the
producer write is removed |
| `detail-view` · `autoDiscoverRelated` | 0 reads (typed or untyped), 0
documents | retired on both faces |
| `chatbot` · `requestBody` | 3 reads (all three chatbot registrations)
| mirrored (shared pick) |
| `chatbot-floating` · `floatingConfig` | 1 read
(`floatingConfig={schema.floatingConfig}`); 6 of 7
`FloatingChatbotConfig` members read, and `triggerIcon` 0 | mirrored,
with a `FloatingChatbotConfig` restatement |
| `chatbot`, `chatbot-floating` · `displayMode` | 0 reads; stored
documents carry it | unchanged by ruling (TypeScript-only refusal) |
| `detail-view` · `activities` (7 reads), `comments` (4), `history` (7)
| host-fetched rows; 0 documents | `RuntimeOnlyDeclared` by name |
| `detail-view` · `recordNavigation` | 11 reads; its value carries a
required function | `RuntimeOnlyDeclared` by name |
| `data-table` · `disableInnerScroll`, `manualPagination`,
`manualSearch`, `manualSorting`, `page`, `rowActionDefs`,
`rowClassName`, `rowCount`, `rowStyle`, `search`, `selectionResetKey`,
`showAddRow`, `showSelectionCount`, `sort` | read by `data-table`;
produced only in code (`ObjectGrid`, `RelatedList`); 0 docs and 0
catalog documents | `RuntimeOnlyDeclared` by name; the reasons are on
the allow-list |
| `data-table` · `editable`, `singleClickEdit` | read; produced in code
by `ObjectGrid`; 0 documents, but `data-table.mdx` lists both and
teaches `editable: true` | **stopped**, ruling needed |
| `data-table` · `selectionStyle` | read; 0 producers and 0 documents (0
hits in objectstack too) | **stopped**, ruling needed |

## Runtime probe for the three zero-read retirements

AGENTS.md requires more than a source census, because leftover keys
reach a renderer as spread props. So the probe used the real
`SchemaRenderer` and the real registry, and varied only the key. It was
a one-off: the files were deleted afterwards and `git diff HEAD` is
empty.

- `report`: the DOM is identical with and without `chartConfig` plus
`reportType` (797 = 797 characters, byte-equal). Lit control: changing
`title` changes the DOM (805).
- `detail-view`: the DOM is identical with and without
`autoDiscoverRelated: true`, after normalising React's `useId` tokens.
The only raw difference was `radix-_r_5_` against `radix-_r_d_`. The
data-source and `fetch` call log is identical:
`getObjectSchema(account)` and two `security/explain` fetches. Lit
control: changing `title` changes the DOM (5943 against 5951).

## Ablations

The predictions were written at `4c272dc` before any run, and every leg
was restored with the blob equal to `HEAD` and `git diff HEAD` empty.

- **A1:** plant a `LabelSchema` literal carrying `content` into
`label.tsx`. Components `tsc --noEmit` exits 2 with exactly one TS2322
there (`'string' is not assignable to type 'undefined'`), as predicted.
This red direction also shows the consumer read the rebuilt `.d.ts`.
- **A2:** restore the four `zod/*.zod.ts` files to base `f61dab1`.
`unmirrored-declared-round4-6152.test.ts` reads `27 failed | 6 passed
(33)`, exactly as predicted, and `tsc -p tsconfig.test.json` exits 2
with its only error in the ledger reconciliation of
`zod-mirror-parity.test.ts`.
- **A3:** rename the allow-list row `rowCount` to `rowCountRenamed`.
`tsc -p tsconfig.test.json` exits 2 with exactly two TS2344, at the two
allow-list pins, as predicted.

## Verification on the final head `2cc4ab9`

This round resumed after two container restarts, so every run below was
redone on `2cc4ab9`. That head carries `origin/main` `e420df3`, merged
twice (never rebased): once through PR objectstack-ai#11275, and before that through
PRs objectstack-ai#11267, objectstack-ai#11268, objectstack-ai#11269, objectstack-ai#11271, objectstack-ai#11273 and objectstack-ai#11274. Each run went
through the shared verify lock with at most 2 workers.

- `@object-ui/types`: `type-check` (the `--noEmit`,
`tsconfig.examples.json` and `tsconfig.test.json` legs) exits 0; `vitest
run packages/types/` gives 299 files and 7498 tests passed; `build`
exits 0 (138 emitted files verified).
- Consumers, from the type-checker census: `type-check` exits 0 with 0
TS errors for `components`, `plugin-report`, `plugin-detail` and
`plugin-chatbot`. `vitest run packages/plugin-report/
packages/plugin-chatbot/` plus `form-renderers.test.tsx` gives 78 files,
827 passed and 7 skipped. All 228 `plugin-detail` test files, run in two
halves, give 227 files passed plus 1 skipped, and 2264 tests passed plus
8 skipped.
- Zod-face consumers: `vitest run examples/schema-catalog/
packages/cli/` plus the 17 app-shell files that import
`@object-ui/types/zod` or sit in `metadata-admin/previews/__tests__`
give 77 files and 2741 tests passed.
- Gates, all exit 0: `check:changeset-claims`,
`check:pending-changeset-literals`, `check-changeset-presence` (16
source files of 2 released packages, 1 changeset),
`check-changeset-no-major`, `check-changeset-fixed`,
`check-changeset-overwrite` (report-only; its 4 findings are the 4 dated
appends above), `check:control-bytes` (9717 files), `check:doc-fences`,
`check:doc-snippets` (697 of 697 blocks judged, 0 failed),
`check:doc-examples`, `check:doc-types`, `check:doc-example-ids`,
`check:doc-example-readers`, `check:skill-examples`,
`check:handler-key-reads`, `check:component-surface-parity`
(report-only), `check:designer-field-key-parity`,
`check:element-data-source-declaration`, `check:new-line-citations` (0
new), `check:prompt-keys`, `check:readme-exports`, `check:spec-symbols`,
`check:test-path-roots`, `check:unreferenced-sources` and
`check-type-check-coverage`. `doc-snippets` and `doc-examples` ran after
the scoped build their own `--build-filter` names (35 turbo tasks, all
successful).
- Lint, narrowed: `eslint --no-inline-config --format json` over the 16
lintable changed files gives 0 errors. All 16 were judged and 0 were
ignored, a population read from eslint's own output. The 39 warnings are
pre-existing `no-explicit-any`, and the diff adds no explicit `any`.
`eslint.config.js` declares no `parserOptions.project` or
`projectService`, and no custom rule in `eslint-rules/` reads the disk,
so this diff cannot move any untouched file's verdict. The repo-wide
`pnpm lint` is CI's.
- Governed surface: `check-governed-queue-guard.mjs --test` over the 23
paths reports NOT GOVERNED.
- NOT MEASURED:
- `check:sdui-registration-pins`: it needs a console build, and this
diff changes no registration.
  - The full `pnpm test` farm and repo-wide lint: CI's.
- `plugin-grid`: its code and tests are untouched. The 14 reclassified
keys moved between two test ledgers and no declaration changed.

## Stopped (ruling needed; reported in the card report with options)

- `DataTableSchema.editable` and `singleClickEdit`. `data-table` reads
both and `ObjectGrid` sets both in code, while no document authors them.
But `content/docs/components/complex/data-table.mdx` lists both in its
interface block and teaches `editable: true`. Filing them runtime-only
would contradict that page. Mirroring them would rest on no authored
document, and, from the source, an authored `editable: true` stages cell
edits locally and persists nothing without the `onRowSave` /
`onCellChange` function slots.
- `DataTableSchema.selectionStyle`. `data-table` reads it (`'always'` or
`'hover'`), and nothing authors or produces it in either repository.
- Noted, not narrowed: `ChatbotSchema.floatingConfig` has 0 reads on the
`chatbot` node (only `chatbot-floating` reads it), which makes it an
ADR-0049 narrowing candidate. The `chatbot` half of
`floating-chatbot-trigger-icon-retired.test.ts` pins its absence as a
tripwire.

## Parallel discipline

PR objectstack-ai#11269 and PR objectstack-ai#11273 edited `zod-mirror-parity.test.ts` in disjoint
hunks: the `KnownDrift` total line, the Kanban entry and one
`EXCLUSIONS` row. Both merges were clean. This round's ledger edits stay
in the `UnmirroredDeclared` / `RuntimeOnlyDeclared` /
`RuntimeOnlyNamedAllowList` blocks, their prose totals and its own
pairs' entries. Every prose total was reconciled to the measured count
above. The diff does not touch `zod/objectql.zod.ts`,
`zod/index.zod.ts`, `zod/nested-component-walk.ts`,
`strict-authoring-face.ts`, `zod/README.md`, `field-types.ts`, the
`DetailViewField` member, or `data-table.tsx` (read only).

## Acceptance notes

- 23 files, under the ~25-file size guard, so no pair was cut.
- Every pending changeset this round made false carries a dated append.
The ones it leaves true were re-read:
`7654-chatbot-display-mode-tombstone.md`, `6150-…`, `7804-…`, `6175-…`,
`7722-…`, `7947-…` and `8072-…`.

Session: `https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh`

---
_Generated by [Claude
Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio flow status must render "disabled by deployment policy" distinctly from a binding failure (follows objectstack#17396 ruling G)

2 participants